Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
CorpulentBrony committed Dec 11, 2021
0 parents commit 27a4c39
Show file tree
Hide file tree
Showing 10 changed files with 7,679 additions and 0 deletions.
6 changes: 6 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# DONUT STEEL PUBLIC LICENSE
**Version 2, 2018-03-28**

The item licensed is original content, do not steal pls kthx. <https://donutsteel.pl>

The above copyright notice, this permission notice, and the word "NIGGER" shall be included in all copies or substantial portions of the Software. <https://plusnigger.autism.exposed/>
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# api.mare.biz

The code for the $MARE API, a quick and dirty attempt to provide an interface to help speed the adoption of $MARE technology. Please see <https://mare.biz/> for more information about $MARE Bits.

[Releases](../../releases) correspond to versions of this site as published.

## Introduction

This API loosely implements the [JSON:API](https://jsonapi.org/) specification (except using and requiring the `application/vnd.api+json` MIME type, it is currently using `application/json`). It also only currently recognizes `GET` requests, and not `HEAD`. All parameters are currently passed through URL query strings (though supporting `POST` is in the plan at some point). The API can only be accessed over a secure HTTP connection at `https://api.mare.biz`. All responses are returned as JSON objects.

## Using the API

### Definitions

Some quick definitions that will make the rest of this document easier to understand (hopefully).

<dl>
<dt><abbr title="Ethereum Name Service">ENS</abbr></dt>
<dd>The <a href="//ens.domains/">Ethereum Name Service</a> is the most recognized NFT-based domain name service on Ethereum. Allows users to register domain names that resolve to cryptocurrency addresses. These domains usually end in <code>.eth</code> but they now support registering almost any real-world TLD as well.</dd>
<dt><abbr title="Unstoppable Name Service">UNS</abbr></dt>
<dd>The alternative to ENS is the Unstoppable Name Service run by <a href="//unstoppabledomains.com/">UnstoppableDomains</a>. This is another NFT-based domain name service that works on both Ethereum and Polygon. It allows users to register domain names that resolve to cryptocurrency addresses. These domains end in <code>.crypto</code>, <code>.x</code>, <code>.bitcoin</code>, <code>.wallet</code>, and many others.</dd>
<dt>Ethereum Address Checksum</dt>
<dd>Since Ethereum didn't have a method of checksumming addresses when it was released, many methods were eventually invented to do so. Currently, the most popular method is <a href="/ethereum/EIPs/blob/master/EIPS/eip-55.md">EIP-55</a> which uses the capitalization of the alpha components of the Ethereum address.</dd>
</dl>

### Return value

All returned objects correspond loosely to the JSON:API specification. Each one will have an `id` and a `type`. They may optionally have a `data` or `errors` object. The `data` object contains one or more `address` objects and the `errors` is an array of errors (if any).

### Address

Addresses represent a wallet or contract on the Ethereum or Polygon network.

#### `/balanceOf?address=<address>`

If a valid address is input, the `data` object will contain a representation of the `address` object. If more than one result is found, `data` will be an array of `address` objects.

When a valid address is input, the `id` field will be a 42 character hexadecimal string prefixed with `0x` representing the address on the blockchain. If the address was modified (either resolved via ENS or UNS or checksummed), the original input value will be located in one or more `meta.input` entries. If an invalid address is input, the `id` field will be an unmodified representation of the input value. The `type` will always be `address`.

The `meta.input` field will be populated with the original address(es) as entered if it was resolved or modified during checksum. The `meta.error` field may be populated with an `Invalid Address` error if the address is invalid.

For valid addresses, the `attributes.balances` object will contain one entry each for the `ethereum` and `polygon` chains along with a `total`. These are the address's $MARE balances represented as a string.
125 changes: 125 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use strict";

import { Address } from "./lib/Address.mjs";
import { Addresses } from "./lib/Addresses.mjs";
import { APIError } from "./lib/APIError.mjs";
import { APIResponse } from "./lib/APIResponse.mjs";
import { createAlchemyWeb3 } from "@alch/alchemy-web3";
import http from "http";
import { readFileSync } from "fs";

const Secrets = globalThis.JSON.parse(readFileSync("./secrets.json"));

class MareBitsAPI extends APIResponse {
static MARE_BITS_CONTRACT_ADDRESS = { ETHEREUM: "0xc5a1973e1f736e2ad991573f3649f4f4a44c3028", POLYGON: "0xb362a97ad06c907c4b575d3503fb9dc474498480" };
static MAX_SIMULTANEOUS_REQUESTS = 6;
static SERVER_URI = "https://api.mare.biz";
static ZERO_ADDRESS = "0x0000000000000000000000000000000000000000000000000000000000000000";

static #server;

static get web3() { return { ethereum: createAlchemyWeb3(Secrets.ALCHEMY_API_KEY_ETHEREUM), polygon: createAlchemyWeb3(Secrets.ALCHEMY_API_KEY_POLYGON) }; }

static listener(request, response) { return new this(request, response); }
static startListening() {
this.#server = http.createServer(this.listener.bind(this));
this.#server.listen(62737);
}

#isHeadWritten = false;
#request;
#requestUrl;
#response;
#status = 200;
#web3;

constructor(request, response) {
super();
[this.#request, this.#response] = [request, response];
this.#requestUrl = new globalThis.URL(this.#request.url, this.constructor.SERVER_URI);
let endpoint;

if (this.#request.method === "GET") {
this.#web3 = this.constructor.web3;

switch (this.#requestUrl.pathname) {
case "/balanceOf": endpoint = this.#balanceOf; break;
default:
endpoint = async () => {
this.addError(new APIError({ detail: `The \`${this.#requestUrl.pathname}\` endpoint is unsupported.`, title: "Unsupported Method", status: "404" }));
this.#status = 404;
};
}
} else
endpoint = async () => {
this.addError(new APIError({ detail: `Server does not support request method \`${this.#request.method}\``, title: "Unimplemented Request Method", status: "501" }));
this.#status = 501;
};
endpoint.call(this)
.catch(this.#errorHandler.bind(this))
.then(() => this.#end())
.catch(console.error);
}

async #balanceOf() {
const addresses = await this.#getAddresses();

if (addresses.length === 0)
return;
const dataArrayPromise = { ethereum: [], polygon: [] };
const promiseToGetBalance = async (balanceGetter) => {
const result = await balanceGetter.call(undefined);

if ("tokenBalances" in result && result.tokenBalances.length > 0 && "tokenBalance" in result.tokenBalances[0] && result.tokenBalances[0].tokenBalance != null)
return this.#web3.ethereum.utils.toBN(result.tokenBalances[0].tokenBalance);
return this.#web3.ethereum.utils.toBN("0");
};
let j = 0;
await addresses.forEach(async (address, i) => {
if (typeof(address.meta.error) !== "undefined")
return this.addError(new APIError({ detail: `Address \`${address.id}\` is not a valid address.`, title: "Invalid Address" }));

if (j >= this.constructor.MAX_SIMULTANEOUS_REQUESTS)
return this.addError(new APIError({ detail: `Too many simultaneous requests, unable to get balance for address \`${address}\`.`, title: "Too Many Simultaneous Requests" }));
dataArrayPromise.ethereum[i] = promiseToGetBalance(this.#web3.ethereum.alchemy.getTokenBalances.bind(undefined, address.id, [this.constructor.MARE_BITS_CONTRACT_ADDRESS.ETHEREUM]))
.catch(this.#errorHandler.bind(this));
dataArrayPromise.polygon[i] = promiseToGetBalance(this.#web3.polygon.alchemy.getTokenBalances.bind(undefined, address.id, [this.constructor.MARE_BITS_CONTRACT_ADDRESS.POLYGON]))
.catch(this.#errorHandler.bind(this));
j++;
});
const dataArray = { ethereum: await globalThis.Promise.all(dataArrayPromise.ethereum), polygon: await globalThis.Promise.all(dataArrayPromise.polygon) };
addresses.forEach((address, i) => {
if (typeof(dataArrayPromise.ethereum[i]) !== "undefined")
address.addAttribute({
balances: {
ethereum: this.#web3.ethereum.utils.fromWei(dataArray.ethereum[i]),
polygon: this.#web3.polygon.utils.fromWei(dataArray.polygon[i]),
total: this.#web3.ethereum.utils.fromWei(dataArray.ethereum[i].add(dataArray.polygon[i]))
}
});
this.addData(address);
});
}
#errorHandler(err) {
if (err instanceof globalThis.Error)
this.addError(new APIError({ detail: err.message, title: err.name }));
else
this.addError(new APIError({ detail: err.toString() }));
throw err;
}
#end() {
this.#writeHead();
this.#write(this);
this.#response.end();
}
#getAddresses() { return (new Addresses(this.#requestUrl.searchParams.getAll("address"))).normalize(this.#web3.ethereum); }
#write(message) { this.#response.write(globalThis.JSON.stringify(message)); }
#writeHead() {
if (this.#isHeadWritten)
return;
this.#response.writeHead(this.#status, { "Content-Type": "application/json", "X-Best-Pony": "Twilight Sparkle" });
this.#isHeadWritten = true;
}
}

MareBitsAPI.startListening();
70 changes: 70 additions & 0 deletions lib/APIError.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export class APIError {
#id;
#code;
#detail;
#links;
#meta;
#source;
#status;
#title;

constructor({ id, code, detail, links, meta, source, status, title } = {}) {
[this.#id, this.#code, this.#detail, this.#links, this.#meta, this.#source, this.#status, this.#title] = [id, code, detail, links, meta, source, status, title];
}
get [globalThis.Symbol.toStringTag]() { return "APIError"; }
get id() { return this.#id; }
get code() { return this.#code; }
get detail() { return this.#detail; }
get links() { return this.#links; }
get meta() { return this.#meta; }
get source() { return this.#source; }
get status() { return this.#status; }
get title() { return this.#title; }
set id(id) { this.#id = id.toString(); }
set code(code) { this.#code = code.toString(); }
set detail(detail) { this.#detail = detail.toString(); }
set status(status) { this.#status = status.toString(); }
set title(title) { this.#title = title.toString(); }
addLink(link) {
if (typeof(this.#links) === "undefined")
this.#links = link;
else
globalThis.Object.assign(this.#links, link);
return this;
}
addMeta(meta) {
if (typeof(this.#meta) === "undefined")
this.#meta = meta;
else
globalThis.Object.assign(this.#meta, meta);
return this;
}
addSource(source) {
if (typeof(this.#source) === "undefined")
this.#source = source;
else
globalThis.Object.assign(this.#source, source);
}
setId(id) {
this.id = id;
return this;
}
setCode(code) {
this.code = code;
return this;
}
setDetail(detail) {
this.detail = detail;
return this;
}
setStatus(status) {
this.status = status;
return this;
}
setTitle(title) {
this.title = title;
return this;
}
toJSON() { return { id: this.id, code: this.code, detail: this.detail, links: this.links, meta: this.meta, source: this.source, status: this.status, title: this.title }; }
toString() { return globalThis.JSON.stringify(this); }
}
51 changes: 51 additions & 0 deletions lib/APIResource.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export class APIResource {
#id;
#attributes;
#links;
#meta;
#relationships;

constructor({ id, attributes, links, meta, relationships }) { [this.#id, this.#attributes, this.#links, this.#meta, this.#relationships] = [id, attributes, links, meta, relationships]; }
get [globalThis.Symbol.toStringTag]() { return "APIResource"; }
get id() { return this.#id; }
get attributes() { return this.#attributes; }
get links() { return this.#links; }
get meta() { return this.#meta; }
get relationships() { return this.#relationships; }
get type() { return "undefined"; }
set id(id) { this.#id = id.toString(); }
addAttribute(attribute) {
if (typeof(this.#attributes) === "undefined")
this.#attributes = attribute;
else
globalThis.Object.assign(this.#attributes, attribute);
return this;
}
addLink(link) {
if (typeof(this.#links) === "undefined")
this.#links = link;
else
globalThis.Object.assign(this.#links, link);
return this;
}
addMeta(meta) {
if (typeof(this.#meta) === "undefined")
this.#meta = meta;
else
globalThis.Object.assign(this.#meta, meta);
return this;
}
addRelationship(relationship) {
if (typeof(this.#relationships) === "undefined")
this.#relationships = relationship;
else
globalThis.Object.assign(this.#relationships, relationship);
return this;
}
setId(id) {
this.id = id;
return this;
}
toJSON() { return { attributes: this.attributes, id: this.id, links: this.links, meta: this.meta, relationships: this.relationships, type: this.type }; }
toString() { return globalThis.JSON.stringify(this); }
}
56 changes: 56 additions & 0 deletions lib/APIResponse.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export class APIResponse {
#data;
#errors;
#included;
#links;
#meta;

constructor({ data, errors, included, links, meta } = {}) { [this.#data, this.#errors, this.#included, this.#links, this.#meta] = [data, errors, included, links, meta]; }
get [globalThis.Symbol.toStringTag]() { return "APIResponse"; }
get data() { return this.#data; }
get errors() { return this.#errors; }
get included() { return this.#included; }
get links() { return this.#links; }
get meta() { return this.#meta; }
addData(data) {
if (typeof(this.#data) === "undefined")
this.#data = data;
else if (globalThis.Array.isArray(this.#data))
this.#data.push(data);
else {
const oldData = this.#data;
this.#data = [oldData, data];
}
return this;
}
addError(error) {
if (typeof(this.#errors) === "undefined")
this.#errors = [error];
else
this.#errors.push(error);
return this;
}
addIncluded(included) {
if (typeof(this.#included) === "undefined")
this.#included = [included];
else
this.#included.push(included);
return this;
}
addLink(link) {
if (typeof(this.#links) === "undefined")
this.#links = link;
else
globalThis.Object.assign(this.#links, link);
return this;
}
addMeta(meta) {
if (typeof(this.#meta) === "undefined")
this.#meta = meta;
else
globalThis.Object.assign(this.#meta, meta);
return this;
}
toJSON() { return { data: this.data, errors: this.errors, included: this.included, links: this.links, meta: this.meta }; }
toString() { return globalThis.JSON.stringify(this); }
}
Loading

0 comments on commit 27a4c39

Please sign in to comment.