-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 27a4c39
Showing
10 changed files
with
7,679 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); } | ||
} |
Oops, something went wrong.