diff --git a/README.md b/README.md index 74029e7..7cf4087 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,12 @@ CF Workers + IttyRouter + micro-stacks + TypeScript ## Things to Note - uses simple typed responses and provides detailed error messages -- all `:cityname` routes accept three letter city names, e.g. mia, nyc -- all `:blockheight` routes always follow `:cityname` routes when required -- all additional parameters follow `:cityname` and `:blockheight` routes +- all CityCoin contract routes start with `:version` and `:cityname` + - e.g. `/v1/mia/mining/get-mining-stats-at-block/57934` +- `:version` accepts the major CityCoins contract version, e.g. v1, v2 +- `:cityname` routes accept three letter city names, e.g. mia, nyc +- all additional parameters follow the order of operations below + - `:blockheight > :cycleid > :userid > :address` - routes are structured the same as the contract functions and documentation ## Implementation @@ -24,17 +27,22 @@ Static assets in the `/static` folder are pushed to Cloudflare Workers KV store All other paths are passed to the IttyRouter in `handler.ts`. -The API is divided into three main sections: +The API is divided into three main folders: -- handlers: individual endpoints that get/caculate a value and return the result +- handlers: individual endpoints that get/calculate a value and return the result - lib: libraries to get or calculate data for handlers - types: type definitions for utilities and responses ### How to Add a City -- add new city config as constant in `/src/types/cities.ts` -- update getCityConfig in `cities.ts` with case for new city -- update enum in `/static/openapi.yml` for reusable parameters +- add new CityInfo constant in `/src/types/cities.ts` +- add new CityConfig constant in `/src/types/cities.ts` +- update the functions below in `cities.ts` with case for new city + - getCityInfo() + - getFullCityInfo() + - getCityConfig() + - getFullCityConfig() +- update cityname enum in `/static/openapi.yml` for reusable parameters ### How to Add an Endpoint @@ -42,17 +50,20 @@ The API is divided into three main sections: - all inputs must be checked or 400 - city config must resolve or 404 - any integers verified with `isStringAllDigits` or 400 - - response from getter or calcualation checked or 404 + - response from getter or calculation checked or 404 - returns successful response - (optional) add new getters in `/lib` - (optional) add new types in `/types` +- add new handler file to top-level export in `/src/handlers` + - e.g. `export { default as GetDateAtBlock } from './stacks/getdateatblock'` - add new handler file and route to `/src/handler.ts` - - Order of Operations: `:cityname > :blockheight > :cycleid > :userid > :address` + - if querying city data, starts with: `:version/:cityname/` + - order of operations: `:blockheight > :cycleid > :userid > :address` - add new endpoint to `/static/openapi.yml` - routes get added to the corresponding section - routes get tagged by their category (matches directory) - routes always use ref for parameters and responses - - reusable parameters and responses are at the bottom of the file + - reusable parameters, schemas and responses are at the bottom of the file **Special case:** if the response is a custom type, e.g. `MiningStatsAtBlock`, an example for the responses must be added manually to `/static/openapi.yml` @@ -81,9 +92,9 @@ A full list of routes and responses can be found in the [OpenAPI documentation]( Some quick examples: - [Get the current Stacks block height](https://api.citycoins.co/stacks/get-block-height) -- [Get the activation block height for MIA](https://api.citycoins.co/activation/get-activation-block/mia) -- [Get the mining stats at block 49000 for MIA](https://api.citycoins.co/mining/get-mining-stats-at-block/mia/49000) -- [Get the total supply for MIA](https://api.citycoins.co/token/get-total-supply/mia) +- [Get the activation block height for MIA](https://api.citycoins.co/v1/mia/activation/get-activation-block) +- [Get the mining stats at block 49000 for MIA](https://api.citycoins.co/v1/mia/mining/get-mining-stats-at-block/49000) +- [Get the total supply for MIA](https://api.citycoins.co/v2/mia/token/get-total-supply) > “Continuous effort, not strength or intelligence > is the key to unlocking our potential.” diff --git a/package-lock.json b/package-lock.json index 4f854ea..6905d47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "citycoins-api", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "citycoins-api", - "version": "1.0.0", + "version": "2.0.0", "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "^0.2.0", @@ -5514,9 +5514,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/mkdirp": { @@ -6702,9 +6702,9 @@ } }, "node_modules/urijs": { - "version": "1.19.8", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.8.tgz", - "integrity": "sha512-iIXHrjomQ0ZCuDRy44wRbyTZVnfVNLVo3Ksz1yxNyE5wV1IDZW2S5Jszy45DTlw/UdsnRT7DyDhIz7Gy+vJumw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "dev": true }, "node_modules/url-search-params": { @@ -11534,9 +11534,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { @@ -12403,9 +12403,9 @@ } }, "urijs": { - "version": "1.19.8", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.8.tgz", - "integrity": "sha512-iIXHrjomQ0ZCuDRy44wRbyTZVnfVNLVo3Ksz1yxNyE5wV1IDZW2S5Jszy45DTlw/UdsnRT7DyDhIz7Gy+vJumw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", "dev": true }, "url-search-params": { diff --git a/package.json b/package.json index 4e7a97c..290c3b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "citycoins-api", - "version": "1.1.0", + "version": "2.0.0", "description": "A simple API to interact with Stacks and CityCoins data.", "main": "dist/worker.js", "scripts": { diff --git a/src/handler.ts b/src/handler.ts deleted file mode 100644 index b9a7736..0000000 --- a/src/handler.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Router } from 'itty-router' -import Landing from './handlers/landing' -import Documentation from './handlers/documentation' -import GetStacksBlockHeight from './handlers/stacks/getstacksblockheight' -import GetBnsName from './handlers/stacks/getbnsname' -import GetStxBalance from './handlers/stacks/getstxbalance' -import GetActivationBlock from './handlers/activation/getactivationblock' -import GetRegisteredUsersNonce from './handlers/activation/getregisteredusersnonce' -import GetUser from './handlers/activation/getuser' -import GetUserId from './handlers/activation/getuserid' -import GetBlockWinnerId from './handlers/mining/getblockwinnerid' -import GetLastHighValueAtBlock from './handlers/mining/getlasthighvalueatblock' -import GetMinerAtBlock from './handlers/mining/getmineratblock' -import GetMiningStatsAtBlock from './handlers/mining/getminingstatsatblock' -import HasMinedAtBlock from './handlers/mining/hasminedatblock' -import CanClaimMiningReward from './handlers/miningclaims/canclaimminingreward' -import IsBlockWinner from './handlers/miningclaims/isblockwinner' -import GetFirstStacksBlockInRewardCycle from './handlers/stacking/getfirststacksblockinrewardcycle' -import GetRewardCycle from './handlers/stacking/getrewardcycle' -import GetStackerAtCycle from './handlers/stacking/getstackeratcycle' -import GetStackingStatsAtCycle from './handlers/stacking/getstackingstatsatcycle' -import StackingActiveAtCycle from './handlers/stacking/stackingactiveatcycle' -import GetStackingReward from './handlers/stackingclaims/getstackingreward' -import GetBalance from './handlers/token/getbalance' -import GetCoinbaseAmount from './handlers/token/getcoinbaseamount' -import GetCoinbaseThresholds from './handlers/token/getcoinbasethresholds' -import GetTotalSupply from './handlers/token/gettotalsupply' -import GetDecimals from './handlers/token/getdecimals' -import GetName from './handlers/token/getname' -import GetSymbol from './handlers/token/getsymbol' -import GetTokenUri from './handlers/token/gettokenuri' -import GetTokenUriJson from './handlers/token/gettokenurijson' -import GetPrices from './handlers/tools/getprices' -import ProofOfHodl from './handlers/tools/proofofhodl' - -const router = Router() - -router - // main landing pages - .get('/', Landing) - .get('/docs', Documentation) - // Stacks API wrappers - .get('/stacks/get-block-height', GetStacksBlockHeight) - .get('/stacks/get-bns-name/:address', GetBnsName) - .get('/stacks/get-stx-balance/:address', GetStxBalance) - // Activation functions - .get('/activation/get-activation-block/:cityname', GetActivationBlock) - .get('/activation/get-registered-users-nonce/:cityname', GetRegisteredUsersNonce) - .get('/activation/get-user/:cityname/:userid', GetUser) - .get('/activation/get-user-id/:cityname/:address', GetUserId) - // Mining functions - .get('/mining/get-block-winner-id/:cityname/:blockheight', GetBlockWinnerId) - .get('/mining/get-last-high-value-at-block/:cityname/:blockheight', GetLastHighValueAtBlock) - .get('/mining/get-miner-at-block/:cityname/:blockheight/:userid', GetMinerAtBlock) - .get('/mining/get-mining-stats-at-block/:cityname/:blockheight', GetMiningStatsAtBlock) - .get('/mining/has-mined-at-block/:cityname/:blockheight/:userid', HasMinedAtBlock) - // Mining claim functions - .get('/mining-claims/can-claim-mining-reward/:cityname/:blockheight/:address', CanClaimMiningReward) - .get('/mining-claims/is-block-winner/:cityname/:blockheight/:address', IsBlockWinner) - // Stacking functions - .get('/stacking/get-first-stacks-block-in-reward-cycle/:cityname/:cycleid', GetFirstStacksBlockInRewardCycle) - .get('/stacking/get-reward-cycle/:cityname/:blockheight', GetRewardCycle) - .get('/stacking/get-stacker-at-cycle/:cityname/:cycleid/:userid', GetStackerAtCycle) - .get('/stacking/get-stacking-stats-at-cycle/:cityname/:cycleid', GetStackingStatsAtCycle) - .get('/stacking/stacking-active-at-cycle/:cityname/:cycleid', StackingActiveAtCycle) - // Stacking claim functions - .get('/stacking-claims/get-stacking-reward/:cityname/:cycleid/:userid', GetStackingReward) - // Token functions - .get('/token/get-balance/:cityname/:address', GetBalance) - .get('/token/get-coinbase-amount/:cityname/:blockheight', GetCoinbaseAmount) - .get('/token/get-coinbase-thresholds/:cityname', GetCoinbaseThresholds) - .get('/token/get-decimals/:cityname', GetDecimals) - .get('/token/get-name/:cityname', GetName) - .get('/token/get-symbol/:cityname', GetSymbol) - .get('/token/get-token-uri/:cityname', GetTokenUri) - .get('/token/get-token-uri-json/:cityname', GetTokenUriJson) - .get('/token/get-total-supply/:cityname', GetTotalSupply) - // Tools - .get('/tools/prices/:cityname/:currency?', GetPrices) - .get('/tools/proof-of-hodl/:cityname/:address', ProofOfHodl) - .get('/tools/proof-of-hold/:cityname/:address', ProofOfHodl) - // Default route - .get('*', () => new Response("Resource not found, please check the URL.", { status: 404 })) - -export const handleRequest = (request: Request):Response => router.handle(request) diff --git a/src/handlers/activation.ts b/src/handlers/activation.ts new file mode 100644 index 0000000..84d7467 --- /dev/null +++ b/src/handlers/activation.ts @@ -0,0 +1,8 @@ +// activation exports + +export { default as GetActivationBlock } from './activation/getactivationblock' +export { default as GetActivationTarget } from './activation/getactivationtarget' +export { default as GetCityWallet } from './activation/getcitywallet' +export { default as GetRegisteredUsersNonce } from './activation/getregisteredusersnonce' +export { default as GetUser } from './activation/getuser' +export { default as GetUserId } from './activation/getuserid' diff --git a/src/handlers/activation/getactivationblock.ts b/src/handlers/activation/getactivationblock.ts index 89d0fef..8f0bd1f 100644 --- a/src/handlers/activation/getactivationblock.ts +++ b/src/handlers/activation/getactivationblock.ts @@ -1,29 +1,38 @@ import { Request as IttyRequest } from 'itty-router' import { getActivationBlock } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetActivationBlock = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let activationBlock: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get activation block - const activationBlock = await getActivationBlock(cityConfig) - // return response - const response: SingleValue = await createSingleValue(activationBlock) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + activationBlock = await getActivationBlock(cityConfig) + response = await createResponse(activationBlock, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetActivationBlock diff --git a/src/handlers/activation/getactivationtarget.ts b/src/handlers/activation/getactivationtarget.ts new file mode 100644 index 0000000..7ccadaf --- /dev/null +++ b/src/handlers/activation/getactivationtarget.ts @@ -0,0 +1,38 @@ +import { Request as IttyRequest } from 'itty-router' +import { getActivationTarget } from '../../lib/citycoins' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' +import { SingleValue } from '../../types/common' + +const GetActivationTarget = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let activationTarget: string + let response: SingleValue | boolean | number | string + // check inputs + const version = request.params?.version ?? undefined + const city = request.params?.cityname ?? undefined + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format + } + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + activationTarget = await getActivationTarget(cityConfig) + response = await createResponse(activationTarget, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(response)) +} + +export default GetActivationTarget diff --git a/src/handlers/activation/getcitywallet.ts b/src/handlers/activation/getcitywallet.ts new file mode 100644 index 0000000..ab9f4ae --- /dev/null +++ b/src/handlers/activation/getcitywallet.ts @@ -0,0 +1,38 @@ +import { Request as IttyRequest } from 'itty-router' +import { getCityWallet } from '../../lib/citycoins' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' +import { SingleValue } from '../../types/common' + +const GetCityWallet = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let cityWallet: string + let response: SingleValue | boolean | number | string + // check inputs + const version = request.params?.version ?? undefined + const city = request.params?.cityname ?? undefined + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format + } + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + cityWallet = await getCityWallet(cityConfig) + response = await createResponse(cityWallet, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(response)) +} + +export default GetCityWallet diff --git a/src/handlers/activation/getregisteredusersnonce.ts b/src/handlers/activation/getregisteredusersnonce.ts index 5deddd7..763bd5a 100644 --- a/src/handlers/activation/getregisteredusersnonce.ts +++ b/src/handlers/activation/getregisteredusersnonce.ts @@ -1,29 +1,40 @@ import { Request as IttyRequest } from 'itty-router' import { getRegisteredUsersNonce } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' -const GetRegisteredUsersNonce = async (request: IttyRequest): Promise => { +const GetRegisteredUsersNonce = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let registeredUsers: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get registered users - const registeredUsers = await getRegisteredUsersNonce(cityConfig) - // return response - const response: SingleValue = await createSingleValue(registeredUsers) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + registeredUsers = await getRegisteredUsersNonce(cityConfig) + response = await createResponse(registeredUsers, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetRegisteredUsersNonce diff --git a/src/handlers/activation/getuser.ts b/src/handlers/activation/getuser.ts index 9af86a6..f10c337 100644 --- a/src/handlers/activation/getuser.ts +++ b/src/handlers/activation/getuser.ts @@ -1,37 +1,45 @@ import { Request as IttyRequest } from 'itty-router' import { getUser } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse, isStringAllDigits } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetUser = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let userAddress: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const userId = request.params?.userid ?? undefined - if (city === undefined || userId === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined || userId === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // verify user ID is valid - if (!isStringAllDigits(userId)) { - return new Response(`User ID not specified or invalid`, { status: 400 }) - } - // get user STX address - const userAddress = await getUser(cityConfig, userId) - if (userAddress === null) { - return new Response(`User ID not found: ${userId}`, { status: 404 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (!isStringAllDigits(userId)) { + return new Response(`User ID not specified or invalid`, { status: 400 }) + } + userAddress = await getUser(cityConfig, userId) + if (userAddress === null) { + return new Response(`User ID not found: ${userId}`, { status: 404 }) + } + response = await createResponse(userAddress, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(userAddress) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetUser diff --git a/src/handlers/activation/getuserid.ts b/src/handlers/activation/getuserid.ts index d3d50cc..336b681 100644 --- a/src/handlers/activation/getuserid.ts +++ b/src/handlers/activation/getuserid.ts @@ -1,33 +1,42 @@ import { Request as IttyRequest } from 'itty-router' import { getUserId } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetUserId = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let userId: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const user = request.params?.address ?? undefined - if (city === undefined || user === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined || user === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get user ID - const userId = await getUserId(cityConfig, user) - if (userId === null) { - return new Response(`User not found: ${user}`, { status: 404 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + userId = await getUserId(cityConfig, user) + if (userId === null) { + return new Response(`Address not found: ${user}`, { status: 404 }) + } + response = await createResponse(userId, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(userId) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetUserId diff --git a/src/handlers/landing.ts b/src/handlers/landing.ts index bc0ba7e..b29fc3e 100644 --- a/src/handlers/landing.ts +++ b/src/handlers/landing.ts @@ -47,6 +47,14 @@ const landingHtml = ` + CityCoins API Logo @@ -56,9 +64,16 @@ const landingHtml = `

Things to Note

  • uses simple typed responses and provides detailed error messages
  • -
  • all :cityname routes accept three letter city names, e.g. mia, nyc
  • -
  • all :blockheight routes always follow :cityname routes when required
  • -
  • all additional parameters follow :cityname and :blockheight routes
  • +
  • + all CityCoin contract routes start with :version and :cityname
    + e.g. /v1/mia/mining/get-mining-stats-at-block/57934 +
  • +
  • :version accepts the major CityCoins contract version, e.g. v1, v2
  • +
  • :cityname routes accept three letter city names, e.g. mia, nyc
  • +
  • + all additional parameters follow the order of operations below
    + :blockheight > :cycleid > :userid > :address +
  • routes are structured the same as the contract functions and documentation

If you want to use this for your project, build a copy for yourself, or have any questions, please join the CityCoins Discord or file a GitHub Issue and reach out!

@@ -66,11 +81,11 @@ const landingHtml = `

A full list of routes and responses can be found in the OpenAPI documentation.


diff --git a/src/handlers/mining.ts b/src/handlers/mining.ts new file mode 100644 index 0000000..b2c5025 --- /dev/null +++ b/src/handlers/mining.ts @@ -0,0 +1,7 @@ +// mining exports + +export { default as GetBlockWinnerId } from './mining/getblockwinnerid' +export { default as GetLastHighValueAtBlock } from './mining/getlasthighvalueatblock' +export { default as GetMinerAtBlock } from './mining/getmineratblock' +export { default as GetMiningStatsAtBlock } from './mining/getminingstatsatblock' +export { default as HasMinedAtBlock } from './mining/hasminedatblock' diff --git a/src/handlers/mining/getblockwinnerid.ts b/src/handlers/mining/getblockwinnerid.ts index 4120016..df8eece 100644 --- a/src/handlers/mining/getblockwinnerid.ts +++ b/src/handlers/mining/getblockwinnerid.ts @@ -1,43 +1,59 @@ import { Request as IttyRequest } from 'itty-router' import { getBlockWinnerId } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetBlockWinnerId = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let winnerId: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined - if (city === undefined || blockHeight === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // get the block winner ID - const winnerId = await getBlockWinnerId(cityConfig, blockHeight) - if (winnerId === null) { - return new Response(`Block winner ID not found at block height: ${blockHeight}`, { status: 404 }) + winnerId = await getBlockWinnerId(cityConfig, blockHeight) + if (winnerId === null) { + return new Response( + `Block winner ID not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + response = await createResponse(winnerId, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(winnerId) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetBlockWinnerId diff --git a/src/handlers/mining/getlasthighvalueatblock.ts b/src/handlers/mining/getlasthighvalueatblock.ts index 2474095..c2a8f1a 100644 --- a/src/handlers/mining/getlasthighvalueatblock.ts +++ b/src/handlers/mining/getlasthighvalueatblock.ts @@ -1,43 +1,64 @@ import { Request as IttyRequest } from 'itty-router' import { getLastHighValueAtBlock } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' -const GetLastHighValueAtBlock = async (request: IttyRequest): Promise => { +const GetLastHighValueAtBlock = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let highValue: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined - if (city === undefined || blockHeight === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + // get current block height if specified + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + // verify block height is valid number + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // get last high value at block height - const highValue = await getLastHighValueAtBlock(cityConfig, blockHeight) - if (highValue === null) { - return new Response(`High value not found at block height: ${blockHeight}`, { status: 404 }) + // get last high value at block height + highValue = await getLastHighValueAtBlock(cityConfig, blockHeight) + if (highValue === null) { + return new Response( + `High value not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + response = await createResponse(highValue, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(highValue) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetLastHighValueAtBlock diff --git a/src/handlers/mining/getmineratblock.ts b/src/handlers/mining/getmineratblock.ts index b380c38..7c9e125 100644 --- a/src/handlers/mining/getmineratblock.ts +++ b/src/handlers/mining/getmineratblock.ts @@ -2,46 +2,61 @@ import { Request as IttyRequest } from 'itty-router' import { getMinerAtBlock } from '../../lib/citycoins' import { isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { MinerAtBlock } from '../../types/mining' const GetMinerAtBlock = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let minerAtBlock: MinerAtBlock // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined const userId = request.params?.userid ?? undefined - if (city === undefined || blockHeight === undefined || userId === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + const defaultStats = request.params?.default === 'true' ? true : false + if ( + version === undefined || + city === undefined || + blockHeight === undefined || + userId === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) - } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // verify user ID is valid - if (!isStringAllDigits(userId)) { - return new Response(`User ID not specified or invalid`, { status: 400 }) - } - // get miner info at block height - const minerAtBlock: MinerAtBlock = await getMinerAtBlock(cityConfig, blockHeight, userId) - if (minerAtBlock === null) { - return new Response(`Miner ${userId} not found at block height: ${blockHeight}`, { status: 404 }) + if (!isStringAllDigits(userId)) { + return new Response(`User ID not specified or invalid`, { status: 400 }) + } + minerAtBlock = await getMinerAtBlock( + cityConfig, + blockHeight, + userId, + defaultStats, + ) + if (minerAtBlock === null) { + return new Response( + `Miner ${userId} not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(minerAtBlock), { headers }) + return new Response(JSON.stringify(minerAtBlock)) } export default GetMinerAtBlock diff --git a/src/handlers/mining/getminingstatsatblock.ts b/src/handlers/mining/getminingstatsatblock.ts index d42d496..3783f7f 100644 --- a/src/handlers/mining/getminingstatsatblock.ts +++ b/src/handlers/mining/getminingstatsatblock.ts @@ -2,41 +2,57 @@ import { Request as IttyRequest } from 'itty-router' import { getMiningStatsAtBlock } from '../../lib/citycoins' import { isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { MiningStatsAtBlock } from '../../types/mining' -const GetMiningStatsAtBlock = async (request: IttyRequest): Promise => { +const GetMiningStatsAtBlock = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let miningStatsAtBlock: MiningStatsAtBlock // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined - if (city === undefined || blockHeight === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + const defaultStats = request.params?.default === 'true' ? true : false + if ( + version === undefined || + city === undefined || + blockHeight === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) - } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // get mining stats at block height - const miningStatsAtBlock: MiningStatsAtBlock = await getMiningStatsAtBlock(cityConfig, blockHeight) - if (miningStatsAtBlock === null) { - return new Response(`Mining stats not found at block height: ${blockHeight}`, { status: 404 }) + miningStatsAtBlock = await getMiningStatsAtBlock( + cityConfig, + blockHeight, + defaultStats, + ) + if (miningStatsAtBlock === null) { + return new Response( + `Mining stats not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(miningStatsAtBlock), { headers }) + return new Response(JSON.stringify(miningStatsAtBlock)) } export default GetMiningStatsAtBlock diff --git a/src/handlers/mining/hasminedatblock.ts b/src/handlers/mining/hasminedatblock.ts index 8108b12..a97ecde 100644 --- a/src/handlers/mining/hasminedatblock.ts +++ b/src/handlers/mining/hasminedatblock.ts @@ -1,48 +1,64 @@ import { Request as IttyRequest } from 'itty-router' import { hasMinedAtBlock } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const HasMinedAtBlock = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let minedAtBlock: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined const userId = request.params?.userid ?? undefined - if (city === undefined || blockHeight === undefined || userId === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined || + userId === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // verify user ID is valid - if (!isStringAllDigits(userId)) { - return new Response(`User ID not specified or invalid`, { status: 400 }) - } - // check if user mined at block - const minedAtBlock = await hasMinedAtBlock(cityConfig, blockHeight, userId) - if (minedAtBlock === null) { - return new Response(`Mining record for ${userId} not found at block height: ${blockHeight}`, { status: 404 }) + if (!isStringAllDigits(userId)) { + return new Response(`User ID not specified or invalid`, { status: 400 }) + } + minedAtBlock = await hasMinedAtBlock(cityConfig, blockHeight, userId) + if (minedAtBlock === null) { + return new Response( + `Mining record for ${userId} not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + response = await createResponse(minedAtBlock, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(minedAtBlock) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default HasMinedAtBlock diff --git a/src/handlers/miningclaims.ts b/src/handlers/miningclaims.ts new file mode 100644 index 0000000..98d1b33 --- /dev/null +++ b/src/handlers/miningclaims.ts @@ -0,0 +1,4 @@ +// mining claims exports + +export { default as CanClaimMiningReward } from './miningclaims/canclaimminingreward' +export { default as IsBlockWinner } from './miningclaims/isblockwinner' diff --git a/src/handlers/miningclaims/canclaimminingreward.ts b/src/handlers/miningclaims/canclaimminingreward.ts index f8830fb..ccb1e85 100644 --- a/src/handlers/miningclaims/canclaimminingreward.ts +++ b/src/handlers/miningclaims/canclaimminingreward.ts @@ -1,43 +1,62 @@ import { Request as IttyRequest } from 'itty-router' import { canClaimMiningReward } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' -const CanClaimMiningReward = async (request: IttyRequest): Promise => { +const CanClaimMiningReward = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let canClaimReward: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const blockHeight = request.params?.blockheight ?? undefined const user = request.params?.address ?? undefined - if (city === undefined || blockHeight === undefined || user === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined || + user === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // verify block height is valid value - if (!isStringAllDigits(blockHeight) && blockHeight !== 'current') { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (!isStringAllDigits(blockHeight) && blockHeight !== 'current') { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } + const currentBlockHeight = await getStacksBlockHeight() + // check that maturity window has passed + // or will default to false regardless of status + if (+blockHeight > +currentBlockHeight - 100 || blockHeight === 'current') { + return new Response( + `Invalid request, maturity window of 100 blocks has not passed`, + { status: 400 }, + ) + } + canClaimReward = await canClaimMiningReward(cityConfig, user, blockHeight) + response = await createResponse(canClaimReward, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - // get current block height - const currentBlockHeight = await getStacksBlockHeight() - // check that maturity window has passed - // or will default to false regardless of status - if (+blockHeight > +currentBlockHeight - 100 || blockHeight === 'current') { - return new Response(`Invalid request, maturity window of 100 blocks has not passed`, { status: 400 }) - } - // check if user won at given block height and can claim reward - const canClaimReward = await canClaimMiningReward(cityConfig, user, blockHeight) // return response - const response: SingleValue = await createSingleValue(canClaimReward) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } -export default CanClaimMiningReward \ No newline at end of file +export default CanClaimMiningReward diff --git a/src/handlers/miningclaims/isblockwinner.ts b/src/handlers/miningclaims/isblockwinner.ts index 8e7a163..f80c2cd 100644 --- a/src/handlers/miningclaims/isblockwinner.ts +++ b/src/handlers/miningclaims/isblockwinner.ts @@ -1,43 +1,60 @@ import { Request as IttyRequest } from 'itty-router' import { isBlockWinner } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const IsBlockWinner = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let blockWinner: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const blockHeight = request.params?.blockheight ?? undefined const user = request.params?.address ?? undefined - if (city === undefined || blockHeight === undefined || user === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined || + user === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // verify block height is valid value - if (!isStringAllDigits(blockHeight) && blockHeight !== 'current') { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (!isStringAllDigits(blockHeight) && blockHeight !== 'current') { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } + const currentBlockHeight = await getStacksBlockHeight() + // check that maturity window has passed + // or will default to false regardless of status + if (+blockHeight > +currentBlockHeight - 100 || blockHeight === 'current') { + return new Response( + `Invalid request, maturity window of 100 blocks has not passed`, + { status: 400 }, + ) + } + blockWinner = await isBlockWinner(cityConfig, user, blockHeight) + response = await createResponse(blockWinner, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - // get current block height - const currentBlockHeight = await getStacksBlockHeight() - // check that maturity window has passed - // or will default to false regardless of status - if (+blockHeight > +currentBlockHeight - 100 || blockHeight === 'current') { - return new Response(`Invalid request, maturity window of 100 blocks has not passed`, { status: 400 }) - } - // check if user won at given block height - const blockWinner = await isBlockWinner(cityConfig, user, blockHeight) // return response - const response: SingleValue = await createSingleValue(blockWinner) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } -export default IsBlockWinner \ No newline at end of file +export default IsBlockWinner diff --git a/src/handlers/stacking.ts b/src/handlers/stacking.ts new file mode 100644 index 0000000..cf1334a --- /dev/null +++ b/src/handlers/stacking.ts @@ -0,0 +1,7 @@ +// stacking exports + +export { default as GetFirstStacksBlockInRewardCycle } from './stacking/getfirststacksblockinrewardcycle' +export { default as GetRewardCycle } from './stacking/getrewardcycle' +export { default as GetStackerAtCycle } from './stacking/getstackeratcycle' +export { default as GetStackingStatsAtCycle } from './stacking/getstackingstatsatcycle' +export { default as StackingActiveAtCycle } from './stacking/stackingactiveatcycle' diff --git a/src/handlers/stacking/getfirststacksblockinrewardcycle.ts b/src/handlers/stacking/getfirststacksblockinrewardcycle.ts index 91bd11b..02c5a19 100644 --- a/src/handlers/stacking/getfirststacksblockinrewardcycle.ts +++ b/src/handlers/stacking/getfirststacksblockinrewardcycle.ts @@ -1,44 +1,61 @@ import { Request as IttyRequest } from 'itty-router' -import { getFirstStacksBlockInRewardCycle, getRewardCycle } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { + getFirstStacksBlockInRewardCycle, + getRewardCycle, +} from '../../lib/citycoins' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' -const GetFirstStacksBlockInRewardCycle = async (request: IttyRequest): Promise => { +const GetFirstStacksBlockInRewardCycle = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let firstBlockInCycle: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let cycle = request.params?.cycleid ?? undefined - if (city === undefined || cycle === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined || cycle === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current reward cycle if specified - if (cycle === 'current') { - const blockHeight = await getStacksBlockHeight() - cycle = await getRewardCycle(cityConfig, blockHeight) - } else { - // verify target cycle is valid - if (!isStringAllDigits(cycle)) { - return new Response(`Target cycle not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (cycle === 'current') { + const blockHeight = await getStacksBlockHeight() + cycle = await getRewardCycle(cityConfig, blockHeight) + } else { + if (!isStringAllDigits(cycle)) { + return new Response(`Target cycle not specified or invalid`, { + status: 400, + }) + } } - } - // get first stacks block in reward cycle - const firstBlockInCycle: string = await getFirstStacksBlockInRewardCycle(cityConfig, cycle) - if (firstBlockInCycle === null) { - return new Response(`Reward cycle not found: ${cycle}`, { status: 404 }) + firstBlockInCycle = await getFirstStacksBlockInRewardCycle( + cityConfig, + cycle, + ) + if (firstBlockInCycle === null) { + return new Response(`Reward cycle not found: ${cycle}`, { status: 404 }) + } + response = await createResponse(firstBlockInCycle, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(firstBlockInCycle) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetFirstStacksBlockInRewardCycle diff --git a/src/handlers/stacking/getrewardcycle.ts b/src/handlers/stacking/getrewardcycle.ts index ba9450e..741fc76 100644 --- a/src/handlers/stacking/getrewardcycle.ts +++ b/src/handlers/stacking/getrewardcycle.ts @@ -1,43 +1,59 @@ import { Request as IttyRequest } from 'itty-router' import { getRewardCycle } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetRewardCycle = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let rewardCycle: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined - if (city === undefined || blockHeight === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // get reward cycle at block height - const rewardCycle: string = await getRewardCycle(cityConfig, blockHeight) - if (rewardCycle === null) { - return new Response(`Reward cycle not found at block height: ${blockHeight}`, { status: 404 }) + rewardCycle = await getRewardCycle(cityConfig, blockHeight) + if (rewardCycle === null) { + return new Response( + `Reward cycle not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + response = await createResponse(rewardCycle, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(rewardCycle) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetRewardCycle diff --git a/src/handlers/stacking/getstackeratcycle.ts b/src/handlers/stacking/getstackeratcycle.ts index 8615e73..3b4b162 100644 --- a/src/handlers/stacking/getstackeratcycle.ts +++ b/src/handlers/stacking/getstackeratcycle.ts @@ -2,47 +2,62 @@ import { Request as IttyRequest } from 'itty-router' import { getRewardCycle, getStackerAtCycle } from '../../lib/citycoins' import { isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { StackerAtCycle } from '../../types/stacking' const GetStackerAtCycle = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let stackerAtCycle: StackerAtCycle // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let cycle = request.params?.cycleid ?? undefined const userId = request.params?.userid ?? undefined - if (city === undefined || cycle === undefined || userId === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + const defaultStats = request.params?.default === 'true' ? true : false + if ( + version === undefined || + city === undefined || + cycle === undefined || + userId === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) - } - // get current reward cycle if specified - if (cycle === 'current') { - const blockHeight = await getStacksBlockHeight() - cycle = await getRewardCycle(cityConfig, blockHeight) - } else { - // verify target cycle is valid - if (!isStringAllDigits(cycle)) { - return new Response(`Target cycle not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (cycle === 'current') { + const blockHeight = await getStacksBlockHeight() + cycle = await getRewardCycle(cityConfig, blockHeight) + } else { + if (!isStringAllDigits(cycle)) { + return new Response(`Target cycle not specified or invalid`, { + status: 400, + }) + } } - } - // verify user ID is valid - if (!isStringAllDigits(userId)) { - return new Response(`User ID not specified or invalid`, { status: 400 }) - } - // get stacker stats at cycle - const stackerAtCycle: StackerAtCycle = await getStackerAtCycle(cityConfig, cycle, userId) - if (stackerAtCycle === null) { - return new Response(`Stacker ${userId} not found at reward cycle: ${cycle}`, { status: 404 }) + if (!isStringAllDigits(userId)) { + return new Response(`User ID not specified or invalid`, { status: 400 }) + } + stackerAtCycle = await getStackerAtCycle( + cityConfig, + cycle, + userId, + defaultStats, + ) + if (stackerAtCycle === null) { + return new Response( + `Stacker ${userId} not found at reward cycle: ${cycle}`, + { status: 404 }, + ) + } + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(stackerAtCycle), { headers }) + return new Response(JSON.stringify(stackerAtCycle)) } export default GetStackerAtCycle diff --git a/src/handlers/stacking/getstackingstatsatcycle.ts b/src/handlers/stacking/getstackingstatsatcycle.ts index 2fcb7d5..5ba7a5d 100644 --- a/src/handlers/stacking/getstackingstatsatcycle.ts +++ b/src/handlers/stacking/getstackingstatsatcycle.ts @@ -2,42 +2,54 @@ import { Request as IttyRequest } from 'itty-router' import { getRewardCycle, getStackingStatsAtCycle } from '../../lib/citycoins' import { isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { StackingStatsAtCycle } from '../../types/stacking' -const GetStackingStatsAtCycle = async (request: IttyRequest): Promise => { +const GetStackingStatsAtCycle = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let stackingStatsAtCycle: StackingStatsAtCycle // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let cycle = request.params?.cycleid ?? undefined - if (city === undefined || cycle === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + const defaultStats = request.params?.default === 'true' ? true : false + if (version === undefined || city === undefined || cycle === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) - } - // get current reward cycle if specified - if (cycle === 'current') { - const blockHeight = await getStacksBlockHeight() - cycle = await getRewardCycle(cityConfig, blockHeight) - } else { - // verify target cycle is valid - if (!isStringAllDigits(cycle)) { - return new Response(`Target cycle not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (cycle === 'current') { + const blockHeight = await getStacksBlockHeight() + cycle = await getRewardCycle(cityConfig, blockHeight) + } else { + if (!isStringAllDigits(cycle)) { + return new Response(`Target cycle not specified or invalid`, { + status: 400, + }) + } } - } - // get stacking stats at cycle - const stackingStatsAtCycle: StackingStatsAtCycle = await getStackingStatsAtCycle(cityConfig, cycle) - if (stackingStatsAtCycle === null) { - return new Response(`Stacking stats not found at reward cycle: ${cycle}`, { status: 404 }) + stackingStatsAtCycle = await getStackingStatsAtCycle( + cityConfig, + cycle, + defaultStats, + ) + if (stackingStatsAtCycle === null) { + return new Response( + `Stacking stats not found at reward cycle: ${cycle}`, + { status: 404 }, + ) + } + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(stackingStatsAtCycle), { headers }) + return new Response(JSON.stringify(stackingStatsAtCycle)) } export default GetStackingStatsAtCycle diff --git a/src/handlers/stacking/stackingactiveatcycle.ts b/src/handlers/stacking/stackingactiveatcycle.ts index bb3fbed..0a268c3 100644 --- a/src/handlers/stacking/stackingactiveatcycle.ts +++ b/src/handlers/stacking/stackingactiveatcycle.ts @@ -1,44 +1,57 @@ import { Request as IttyRequest } from 'itty-router' import { getRewardCycle, stackingActiveAtCycle } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' -const StackingActiveAtCycle = async (request: IttyRequest): Promise => { +const StackingActiveAtCycle = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let activeAtCycle: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let cycle = request.params?.cycleid ?? undefined - if (city === undefined || cycle === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined || cycle === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current reward cycle if specified - if (cycle === 'current') { - const blockHeight = await getStacksBlockHeight() - cycle = await getRewardCycle(cityConfig, blockHeight) - } else { - // verify target cycle is valid - if (!isStringAllDigits(cycle)) { - return new Response(`Target cycle not specified or invalid`, { status: 400 }) + // get city configuration object + try { + cityConfig = await getCityConfig(city, version) + if (cycle === 'current') { + const blockHeight = await getStacksBlockHeight() + cycle = await getRewardCycle(cityConfig, blockHeight) + } else { + if (!isStringAllDigits(cycle)) { + return new Response(`Target cycle not specified or invalid`, { + status: 400, + }) + } } - } - // check if stacking is active at cycle - const activeAtCycle = await stackingActiveAtCycle(cityConfig, cycle) - if (activeAtCycle === null) { - return new Response(`Stacking info not found at cycle: ${cycle}`, { status: 404 }) + activeAtCycle = await stackingActiveAtCycle(cityConfig, cycle) + if (activeAtCycle === null) { + return new Response(`Stacking info not found at cycle: ${cycle}`, { + status: 404, + }) + } + response = await createResponse(activeAtCycle, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(activeAtCycle) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default StackingActiveAtCycle diff --git a/src/handlers/stackingclaims.ts b/src/handlers/stackingclaims.ts new file mode 100644 index 0000000..f422a0b --- /dev/null +++ b/src/handlers/stackingclaims.ts @@ -0,0 +1,3 @@ +// stacking claims exports + +export { default as GetStackingReward } from './stackingclaims/getstackingreward' diff --git a/src/handlers/stackingclaims/getstackingreward.ts b/src/handlers/stackingclaims/getstackingreward.ts index 5cccfbd..d5b3547 100644 --- a/src/handlers/stackingclaims/getstackingreward.ts +++ b/src/handlers/stackingclaims/getstackingreward.ts @@ -1,47 +1,62 @@ import { Request as IttyRequest } from 'itty-router' import { getRewardCycle, getStackingReward } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetStackingReward = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let stackingReward: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const cycle = request.params?.cycleid ?? undefined const userId = request.params?.userid ?? undefined - if (city === undefined || cycle === undefined || userId === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + cycle === undefined || + userId === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // verify target cycle is a valid value - if (!isStringAllDigits(cycle) && cycle !== 'current') { - return new Response(`Cycle ID not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + if (!isStringAllDigits(cycle) && cycle !== 'current') { + return new Response(`Cycle ID not specified or invalid`, { status: 400 }) + } + const currentBlockHeight = await getStacksBlockHeight() + const currentCycle = await getRewardCycle(cityConfig, currentBlockHeight) + if ( + (+cycle >= +currentCycle || cycle === 'current') && + !cityConfig.core.shutdown + ) { + return new Response(`Invalid request, cycle still active or in future`, { + status: 400, + }) + } + if (!isStringAllDigits(userId)) { + return new Response(`User ID not specified or invalid`, { status: 400 }) + } + stackingReward = await getStackingReward(cityConfig, cycle, userId) + response = await createResponse(stackingReward, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - // get current cycle - const currentBlockHeight = await getStacksBlockHeight() - const currentCycle = await getRewardCycle(cityConfig, currentBlockHeight) - // check that cycle is in the past - if (+cycle >= +currentCycle || cycle === 'current') { - return new Response(`Invalid request, cycle still active or in future`, { status: 400 }) - } - // verify user ID is valid - if (!isStringAllDigits(userId)) { - return new Response(`User ID not specified or invalid`, { status: 400 }) - } - // get stacking reward for user at cycle - const stackingReward = await getStackingReward(cityConfig, cycle, userId) // return response - const response: SingleValue = await createSingleValue(stackingReward) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetStackingReward diff --git a/src/handlers/stacks.ts b/src/handlers/stacks.ts new file mode 100644 index 0000000..5a2ac86 --- /dev/null +++ b/src/handlers/stacks.ts @@ -0,0 +1,6 @@ +// stacks exports + +export { default as GetBnsName } from './stacks/getbnsname' +export { default as GetDateAtBlock } from './stacks/getdateatblock' +export { default as GetStacksBlockHeight } from './stacks/getstacksblockheight' +export { default as GetStxBalance } from './stacks/getstxbalance' diff --git a/src/handlers/stacks/getbnsname.ts b/src/handlers/stacks/getbnsname.ts index 767801e..57812c6 100644 --- a/src/handlers/stacks/getbnsname.ts +++ b/src/handlers/stacks/getbnsname.ts @@ -1,27 +1,39 @@ import { Request as IttyRequest } from 'itty-router' -import { createSingleValue } from '../../lib/common' +import { createResponse } from '../../lib/common' import { getBnsName } from '../../lib/stacks' import { SingleValue } from '../../types/common' const GetBnsName = async (request: IttyRequest): Promise => { + let bnsNames: string + let response: SingleValue | boolean | number | string // check inputs const address = request.params?.address ?? undefined if (address === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get Stacks block height from API - const bnsNames: string = await getBnsName(address) - .catch(() => { return '' }) - if (bnsNames === '' || bnsNames === 'undefined') { - return new Response(`BNS name(s) not found for address: ${address}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // return response - const response: SingleValue = await createSingleValue(bnsNames) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + bnsNames = await getBnsName(address) + if (bnsNames === 'undefined') { + return new Response(`BNS name(s) not found for address: ${address}`, { + status: 404, + }) + } + response = await createResponse(bnsNames, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetBnsName diff --git a/src/handlers/stacks/getdateatblock.ts b/src/handlers/stacks/getdateatblock.ts new file mode 100644 index 0000000..5901166 --- /dev/null +++ b/src/handlers/stacks/getdateatblock.ts @@ -0,0 +1,43 @@ +import { Request as IttyRequest } from 'itty-router' +import { createResponse, isStringAllDigits } from '../../lib/common' +import { getDateAtBlock, getStacksBlockHeight } from '../../lib/stacks' +import { SingleValue } from '../../types/common' + +const GetDateAtBlock = async (request: IttyRequest): Promise => { + let dateAtBlock: string + let response: SingleValue | boolean | number | string + // check inputs + let blockHeight = request.params?.blockHeight ?? undefined + if (blockHeight === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format + } + // get/calculate response + try { + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } + } + dateAtBlock = await getDateAtBlock(blockHeight) + response = await createResponse(dateAtBlock, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(response)) +} + +export default GetDateAtBlock diff --git a/src/handlers/stacks/getstacksblockheight.ts b/src/handlers/stacks/getstacksblockheight.ts index 51333aa..e8a4da2 100644 --- a/src/handlers/stacks/getstacksblockheight.ts +++ b/src/handlers/stacks/getstacksblockheight.ts @@ -1,21 +1,29 @@ -import { createSingleValue } from '../../lib/common' +import { Request as IttyRequest } from 'itty-router' +import { createResponse } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' import { SingleValue } from '../../types/common' -const GetStacksBlockHeight = async (): Promise => { +const GetStacksBlockHeight = async ( + request: IttyRequest, +): Promise => { + let currentBlockHeight: string + let response: SingleValue | boolean | number | string + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format + } // get Stacks block height from API - const currentBlockHeight: string = await getStacksBlockHeight() - .catch(() => { return '' }) - if (currentBlockHeight === '') { - return new Response(`Stacks block height not found, please try again`, { status: 404 }) + try { + currentBlockHeight = await getStacksBlockHeight() + response = await createResponse(currentBlockHeight, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(currentBlockHeight) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetStacksBlockHeight diff --git a/src/handlers/stacks/getstxbalance.ts b/src/handlers/stacks/getstxbalance.ts index 34889d5..51681c1 100644 --- a/src/handlers/stacks/getstxbalance.ts +++ b/src/handlers/stacks/getstxbalance.ts @@ -1,27 +1,34 @@ import { Request as IttyRequest } from 'itty-router' -import { createSingleValue } from '../../lib/common' +import { createResponse } from '../../lib/common' import { getStxBalance } from '../../lib/stacks' import { SingleValue } from '../../types/common' const GetStxBalance = async (request: IttyRequest): Promise => { + let stxBalance: string + let response: SingleValue | boolean | number | string // check inputs const address = request.params?.address ?? undefined if (address === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get Stacks balance in uSTX from API - const stxBalance: string = await getStxBalance(address) - .catch(() => { return '' }) - if (stxBalance === '') { - return new Response(`Stacks balance not found for address: ${address}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // return response - const response: SingleValue = await createSingleValue(stxBalance) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + stxBalance = await getStxBalance(address) + response = await createResponse(stxBalance, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetStxBalance diff --git a/src/handlers/token.ts b/src/handlers/token.ts new file mode 100644 index 0000000..356d7f3 --- /dev/null +++ b/src/handlers/token.ts @@ -0,0 +1,13 @@ +// token exports + +export { default as GetBalance } from './token/getbalance' +export { default as GetCoinbaseAmount } from './token/getcoinbaseamount' +export { default as GetCoinbaseAmounts } from './token/getcoinbaseamounts' +export { default as GetCoinbaseThresholds } from './token/getcoinbasethresholds' +export { default as GetDecimals } from './token/getdecimals' +export { default as GetName } from './token/getname' +export { default as GetSymbol } from './token/getsymbol' +export { default as GetTokenUri } from './token/gettokenuri' +export { default as GetTokenUriJson } from './token/gettokenurijson' +export { default as GetTotalSupply } from './token/gettotalsupply' +export { default as GetFullTotalSupply } from './token/getfulltotalsupply' diff --git a/src/handlers/token/getbalance.ts b/src/handlers/token/getbalance.ts index 235e9d4..6a73816 100644 --- a/src/handlers/token/getbalance.ts +++ b/src/handlers/token/getbalance.ts @@ -1,34 +1,39 @@ import { Request as IttyRequest } from 'itty-router' import { getBalance } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetBalance = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let balance: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const user = request.params?.address ?? undefined - if (city === undefined || user === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined || user === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get CityCoin balance - const balance = await getBalance(cityConfig, user) - .catch(() => { return '' }) - if (balance === '') { - return new Response(`User not found: ${user}`, { status: 404 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + balance = await getBalance(cityConfig, user) + response = await createResponse(balance, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(balance) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetBalance diff --git a/src/handlers/token/getcoinbaseamount.ts b/src/handlers/token/getcoinbaseamount.ts index 1e9dbaa..e2f389f 100644 --- a/src/handlers/token/getcoinbaseamount.ts +++ b/src/handlers/token/getcoinbaseamount.ts @@ -1,43 +1,62 @@ import { Request as IttyRequest } from 'itty-router' import { getCoinbaseAmount } from '../../lib/citycoins' -import { createSingleValue, isStringAllDigits } from '../../lib/common' +import { createResponse, isStringAllDigits } from '../../lib/common' import { getStacksBlockHeight } from '../../lib/stacks' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetCoinbaseAmount = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let coinbaseAmount: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined let blockHeight = request.params?.blockheight ?? undefined - if (city === undefined || blockHeight === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if ( + version === undefined || + city === undefined || + blockHeight === undefined + ) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get current block height if specified - if (blockHeight === 'current') { - blockHeight = await getStacksBlockHeight() - } else { - // verify block height is valid number - if (!isStringAllDigits(blockHeight)) { - return new Response(`Block height not specified or invalid`, { status: 400 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + // get current block height if specified + if (blockHeight === 'current') { + blockHeight = await getStacksBlockHeight() + } else { + // verify block height is valid number + if (!isStringAllDigits(blockHeight)) { + return new Response(`Block height not specified or invalid`, { + status: 400, + }) + } } - } - // get coinbase thresholds - const coinbaseAmount: string = await getCoinbaseAmount(cityConfig, blockHeight) - if (coinbaseAmount === null) { - return new Response(`Coinbase amount not found at block height: ${blockHeight}`, { status: 404 }) + // get coinbase thresholds + coinbaseAmount = await getCoinbaseAmount(cityConfig, blockHeight) + if (coinbaseAmount === null) { + return new Response( + `Coinbase amount not found at block height: ${blockHeight}`, + { status: 404 }, + ) + } + response = await createResponse(coinbaseAmount, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const response: SingleValue = await createSingleValue(coinbaseAmount) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(response), { headers }) + return new Response(JSON.stringify(response)) } export default GetCoinbaseAmount diff --git a/src/handlers/token/getcoinbaseamounts.ts b/src/handlers/token/getcoinbaseamounts.ts new file mode 100644 index 0000000..120e3de --- /dev/null +++ b/src/handlers/token/getcoinbaseamounts.ts @@ -0,0 +1,29 @@ +import { Request as IttyRequest } from 'itty-router' +import { getCoinbaseAmounts } from '../../lib/citycoins' +import { CityConfig, getCityConfig } from '../../types/cities' +import { CoinbaseAmounts } from '../../types/token' + +const GetCoinbaseAmounts = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let coinbaseAmounts: CoinbaseAmounts + // check inputs + const version = request.params?.version ?? undefined + const city = request.params?.cityname ?? undefined + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + coinbaseAmounts = await getCoinbaseAmounts(cityConfig) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(coinbaseAmounts)) +} + +export default GetCoinbaseAmounts diff --git a/src/handlers/token/getcoinbasethresholds.ts b/src/handlers/token/getcoinbasethresholds.ts index 4f21a5d..baef5a9 100644 --- a/src/handlers/token/getcoinbasethresholds.ts +++ b/src/handlers/token/getcoinbasethresholds.ts @@ -1,30 +1,36 @@ import { Request as IttyRequest } from 'itty-router' import { getCoinbaseThresholds } from '../../lib/citycoins' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { CoinbaseThresholds } from '../../types/token' -const GetCoinbaseThresholds = async (request: IttyRequest): Promise => { +const GetCoinbaseThresholds = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + let coinbaseThresholds: CoinbaseThresholds // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) - } - // get coinbase thresholds - const coinbaseThresholds: CoinbaseThresholds = await getCoinbaseThresholds(cityConfig) - if (coinbaseThresholds === null) { - return new Response(`Coinbase thresholds not found for city: ${city}`, { status: 404 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + coinbaseThresholds = await getCoinbaseThresholds(cityConfig) + if (coinbaseThresholds === null) { + return new Response(`Coinbase thresholds not found for city: ${city}`, { + status: 404, + }) + } + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(coinbaseThresholds), { headers }) + return new Response(JSON.stringify(coinbaseThresholds)) } export default GetCoinbaseThresholds diff --git a/src/handlers/token/getdecimals.ts b/src/handlers/token/getdecimals.ts index b27c170..1b0a3e9 100644 --- a/src/handlers/token/getdecimals.ts +++ b/src/handlers/token/getdecimals.ts @@ -1,29 +1,38 @@ import { Request as IttyRequest } from 'itty-router' import { getDecimals } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetDecimals = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let decimals: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get SIP-010 decimals - const decimals: string = await getDecimals(cityConfig) - // return response - const response: SingleValue = await createSingleValue(decimals) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + decimals = await getDecimals(cityConfig) + response = await createResponse(decimals, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetDecimals diff --git a/src/handlers/token/getfulltotalsupply.ts b/src/handlers/token/getfulltotalsupply.ts new file mode 100644 index 0000000..925b9ff --- /dev/null +++ b/src/handlers/token/getfulltotalsupply.ts @@ -0,0 +1,44 @@ +import { Request as IttyRequest } from 'itty-router' +import { getTotalSupply } from '../../lib/citycoins' +import { createResponse, MICRO_UNITS } from '../../lib/common' +import { getCityConfig, getCityInfo } from '../../types/cities' +import { SingleValue } from '../../types/common' + +const GetFullTotalSupply = async (request: IttyRequest): Promise => { + let totalSupply = 0 + let response: SingleValue | boolean | number | string + // check inputs + const city = request.params?.cityname ?? undefined + if (city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format + } + // get/calculate response + try { + const cityInfo = await getCityInfo(city) + for await (const version of cityInfo.versions) { + const config = await getCityConfig(city, version) + const supply = await getTotalSupply(config) + console.log(city, version, supply) + totalSupply += version === 'v1' ? +supply * MICRO_UNITS : +supply + } + response = await createResponse( + (totalSupply / MICRO_UNITS).toFixed(6), + format, + ) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(response)) +} + +export default GetFullTotalSupply diff --git a/src/handlers/token/getname.ts b/src/handlers/token/getname.ts index 2bdc3c9..bd63af2 100644 --- a/src/handlers/token/getname.ts +++ b/src/handlers/token/getname.ts @@ -1,29 +1,38 @@ import { Request as IttyRequest } from 'itty-router' import { getName } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetName = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let name: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get SIP-010 name - const name: string = await getName(cityConfig) - // return response - const response: SingleValue = await createSingleValue(name) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + name = await getName(cityConfig) + response = await createResponse(name, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetName diff --git a/src/handlers/token/getsymbol.ts b/src/handlers/token/getsymbol.ts index e0438d3..f3ed838 100644 --- a/src/handlers/token/getsymbol.ts +++ b/src/handlers/token/getsymbol.ts @@ -1,29 +1,38 @@ import { Request as IttyRequest } from 'itty-router' import { getSymbol } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetSymbol = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let symbol: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get SIP-010 symbol - const symbol: string = await getSymbol(cityConfig) - // return response - const response: SingleValue = await createSingleValue(symbol) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + symbol = await getSymbol(cityConfig) + response = await createResponse(symbol, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetSymbol diff --git a/src/handlers/token/gettokenuri.ts b/src/handlers/token/gettokenuri.ts index 6e4d8c8..d797129 100644 --- a/src/handlers/token/gettokenuri.ts +++ b/src/handlers/token/gettokenuri.ts @@ -1,29 +1,38 @@ import { Request as IttyRequest } from 'itty-router' import { getTokenUri } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetTokenUri = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let tokenUri: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get SIP-010 token uri - const tokenUri: string = await getTokenUri(cityConfig) - // return response - const response: SingleValue = await createSingleValue(tokenUri) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + tokenUri = await getTokenUri(cityConfig) + response = await createResponse(tokenUri, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetTokenUri diff --git a/src/handlers/token/gettokenurijson.ts b/src/handlers/token/gettokenurijson.ts index d57657a..c997e11 100644 --- a/src/handlers/token/gettokenurijson.ts +++ b/src/handlers/token/gettokenurijson.ts @@ -1,29 +1,30 @@ import { Request as IttyRequest } from 'itty-router' import { getTokenUri } from '../../lib/citycoins' -import { getCityConfig } from '../../types/cities' -import { TokenUri } from '../../types/token' +import { CityConfig, getCityConfig } from '../../types/cities' const GetTokenUriJson = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let tokenUri: string + let tokenUriJson: string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + tokenUri = await getTokenUri(cityConfig) + tokenUriJson = await fetch(tokenUri).then((res) => res.json()) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - // get SIP-010 token uri URL - const tokenUri: string = await getTokenUri(cityConfig) - // get JSON from URL - const tokenUriJson: TokenUri = await fetch(tokenUri).then(res => res.json()) // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(tokenUriJson), { headers }) + return new Response(JSON.stringify(tokenUriJson)) } export default GetTokenUriJson diff --git a/src/handlers/token/gettotalsupply.ts b/src/handlers/token/gettotalsupply.ts index 9e3be7e..da7cd0b 100644 --- a/src/handlers/token/gettotalsupply.ts +++ b/src/handlers/token/gettotalsupply.ts @@ -1,29 +1,38 @@ import { Request as IttyRequest } from 'itty-router' import { getTotalSupply } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' -import { getCityConfig } from '../../types/cities' +import { createResponse } from '../../lib/common' +import { CityConfig, getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const GetTotalSupply = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let totalSupply: string + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // get total supply - const totalSupply: string = await getTotalSupply(cityConfig) - // return response - const response: SingleValue = await createSingleValue(totalSupply) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + totalSupply = await getTotalSupply(cityConfig) + response = await createResponse(totalSupply, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default GetTotalSupply diff --git a/src/handlers/tools.ts b/src/handlers/tools.ts new file mode 100644 index 0000000..7f05545 --- /dev/null +++ b/src/handlers/tools.ts @@ -0,0 +1,9 @@ +// tools exports + +export { default as GetCityConfiguration } from './tools/getcityconfiguration' +export { default as GetFullCityConfiguration } from './tools/getfullcityconfiguration' +export { default as GetPrices } from './tools/getprices' +export { default as ProofOfHodl } from './tools/proofofhodl' +export { default as GetCityInfo } from './tools/getcityinfo' +export { default as GetCityList } from './tools/getcitylist' +export { default as GetFullCityInfo } from './tools/getfullcityinfo' diff --git a/src/handlers/tools/getcityconfiguration.ts b/src/handlers/tools/getcityconfiguration.ts new file mode 100644 index 0000000..54044d5 --- /dev/null +++ b/src/handlers/tools/getcityconfiguration.ts @@ -0,0 +1,27 @@ +import { Request as IttyRequest } from 'itty-router' +import { CityConfig, getCityConfig } from '../../types/cities' + +const GetCityConfiguration = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityConfig + // check inputs + const version = request.params?.version ?? undefined + const city = request.params?.cityname ?? undefined + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // get/calculate response + try { + cityConfig = await getCityConfig(city, version) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(cityConfig)) +} + +export default GetCityConfiguration diff --git a/src/handlers/tools/getcityinfo.ts b/src/handlers/tools/getcityinfo.ts new file mode 100644 index 0000000..cc20210 --- /dev/null +++ b/src/handlers/tools/getcityinfo.ts @@ -0,0 +1,24 @@ +import { Request as IttyRequest } from 'itty-router' +import { CityInfo, getCityInfo } from '../../types/cities' + +const GetCityInfo = async (request: IttyRequest): Promise => { + let cityInfo: CityInfo + // check inputs + const city = request.params?.cityname ?? undefined + if (city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // get/calculate response + try { + cityInfo = await getCityInfo(city) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(cityInfo)) +} + +export default GetCityInfo diff --git a/src/handlers/tools/getcitylist.ts b/src/handlers/tools/getcitylist.ts new file mode 100644 index 0000000..580403d --- /dev/null +++ b/src/handlers/tools/getcitylist.ts @@ -0,0 +1,16 @@ +import { getCityList } from '../../types/cities' + +const GetCityList = async (): Promise => { + let cityList: string[] + // get/calculate response + try { + cityList = await getCityList() + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(cityList)) +} + +export default GetCityList diff --git a/src/handlers/tools/getfullcityconfiguration.ts b/src/handlers/tools/getfullcityconfiguration.ts new file mode 100644 index 0000000..78094df --- /dev/null +++ b/src/handlers/tools/getfullcityconfiguration.ts @@ -0,0 +1,26 @@ +import { Request as IttyRequest } from 'itty-router' +import { CityVersions, getFullCityConfig } from '../../types/cities' + +const GetFullCityConfiguration = async ( + request: IttyRequest, +): Promise => { + let cityConfig: CityVersions + // check inputs + const city = request.params?.cityname ?? undefined + if (city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) + } + // get/calculate response + try { + cityConfig = await getFullCityConfig(city) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(cityConfig)) +} + +export default GetFullCityConfiguration diff --git a/src/handlers/tools/getfullcityinfo.ts b/src/handlers/tools/getfullcityinfo.ts new file mode 100644 index 0000000..40faa17 --- /dev/null +++ b/src/handlers/tools/getfullcityinfo.ts @@ -0,0 +1,16 @@ +import { CityList, getFullCityInfo } from '../../types/cities' + +const GetFullCityInfo = async (): Promise => { + let cityInfo: CityList + // get/calculate response + try { + cityInfo = await getFullCityInfo() + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) + } + // return response + return new Response(JSON.stringify(cityInfo)) +} + +export default GetFullCityInfo diff --git a/src/handlers/tools/getprices.ts b/src/handlers/tools/getprices.ts index 0f7920d..07550b4 100644 --- a/src/handlers/tools/getprices.ts +++ b/src/handlers/tools/getprices.ts @@ -1,42 +1,36 @@ import { Request as IttyRequest } from 'itty-router' import { getCGPrice } from '../../lib/prices' -import { getCityConfig } from '../../types/cities' +import { CityConfig, getCityConfig } from '../../types/cities' import { Prices } from '../../types/common' const GetPrices = async (request: IttyRequest): Promise => { + let cityConfig: CityConfig + let tokenName: string + let prices: Prices // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const currency = request.params?.currency ?? undefined - let cityConfig - let tokenName - if (city === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // hack to allow for stx - if (city === 'stx') { - tokenName = 'blockstack' - } else { - // get city configuration object - cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // get/calculate response + try { + if (city === 'stx') { + tokenName = 'blockstack' + } else { + cityConfig = await getCityConfig(city, version) + tokenName = cityConfig.token.tokenName } - tokenName = cityConfig.tokenName - } - // get CoinGecko price - const prices: Prices = await getCGPrice(tokenName, currency) - .catch(() => { return { - "coingecko": 0, - }}) - if (prices.coingecko === 0 || prices.coingecko === undefined) { - return new Response(`CoinGecko price not found for city: ${city} and currency: ${currency}`, { status: 404 }) + prices = await getCGPrice(tokenName, currency) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } // return response - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', - } - return new Response(JSON.stringify(prices), { headers }) + return new Response(JSON.stringify(prices)) } export default GetPrices diff --git a/src/handlers/tools/proofofhodl.ts b/src/handlers/tools/proofofhodl.ts index 839fb5e..383f7ba 100644 --- a/src/handlers/tools/proofofhodl.ts +++ b/src/handlers/tools/proofofhodl.ts @@ -1,30 +1,41 @@ import { Request as IttyRequest } from 'itty-router' import { getProofOfHodl } from '../../lib/citycoins' -import { createSingleValue } from '../../lib/common' +import { createResponse } from '../../lib/common' import { getCityConfig } from '../../types/cities' import { SingleValue } from '../../types/common' const ProofOfHodl = async (request: IttyRequest): Promise => { + let cityConfig + let hodl + let response: SingleValue | boolean | number | string // check inputs + const version = request.params?.version ?? undefined const city = request.params?.cityname ?? undefined const user = request.params?.address ?? undefined - if (city === undefined || user === undefined) { - return new Response(`Invalid request, missing parameter(s)`, { status: 400 }) + if (version === undefined || city === undefined || user === undefined) { + return new Response(`Invalid request, missing parameter(s)`, { + status: 400, + }) } - // get city configuration object - const cityConfig = await getCityConfig(city) - if (cityConfig.deployer === '') { - return new Response(`City name not found: ${city}`, { status: 404 }) + // check response output format + let format = 'json' + const { query } = request + if (Object.prototype.hasOwnProperty.call(query, 'format')) { + if (query?.format !== undefined) format = query.format } - // check if user is hodling - const hodl = await getProofOfHodl(cityConfig, user) - // return response - const response: SingleValue = await createSingleValue(hodl) - const headers = { - 'Access-Control-Allow-Origin': '*', - 'Content-Type': 'application/json', + // get/calculate response + try { + // get city configuration object + cityConfig = await getCityConfig(city, version) + // check if user is hodling + hodl = await getProofOfHodl(cityConfig, user) + response = await createResponse(hodl, format) + } catch (err) { + if (err instanceof Error) return new Response(err.message, { status: 404 }) + return new Response(String(err), { status: 404 }) } - return new Response(JSON.stringify(response), { headers }) + // return response + return new Response(JSON.stringify(response)) } export default ProofOfHodl diff --git a/src/index.ts b/src/index.ts index 972d8ac..f6a73b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,93 @@ -import { handleRequest } from './handler' -import { getAssetFromKV, MethodNotAllowedError, NotFoundError } from '@cloudflare/kv-asset-handler' +import { Router } from 'itty-router' +import { + getAssetFromKV, + MethodNotAllowedError, + NotFoundError, +} from '@cloudflare/kv-asset-handler' +import Landing from './handlers/landing' +import Documentation from './handlers/documentation' +import * as Stacks from './handlers/stacks' +import * as Activation from './handlers/activation' +import * as Mining from './handlers/mining' +import * as MiningClaims from './handlers/miningclaims' +import * as Stacking from './handlers/stacking' +import * as StackingClaims from './handlers/stackingclaims' +import * as Token from './handlers/token' +import * as Tools from './handlers/tools' -addEventListener('fetch', (event) => { - if (matchDownload(event.request.url)) { - // check if item should be downloaded from KV - event.respondWith(returnDownload(event)) - } else { - // otherwise route the request to itty-router - event.respondWith(handleRequest(event.request)) - } -}) +const router = Router() + +// prettier-ignore +router + // main landing pages + .get('/', Landing) + .get('/docs', Documentation) + // Stacks API wrappers + .get('/stacks/get-block-height', Stacks.GetStacksBlockHeight) + .get('/stacks/get-bns-name/:address', Stacks.GetBnsName) + .get('/stacks/get-stx-balance/:address', Stacks.GetStxBalance) + .get('/stacks/get-date-at-block/:blockHeight', Stacks.GetDateAtBlock) + // Activation functions + .get('/:version/:cityname/activation/get-activation-block', Activation.GetActivationBlock) + .get('/:version/:cityname/activation/get-activation-target', Activation.GetActivationTarget) + .get('/:version/:cityname/activation/get-city-wallet', Activation.GetCityWallet) + .get('/:version/:cityname/activation/get-registered-users-nonce', Activation.GetRegisteredUsersNonce) + .get('/:version/:cityname/activation/get-user/:userid', Activation.GetUser) + .get('/:version/:cityname/activation/get-user-id/:address', Activation.GetUserId) + // Mining functions + .get('/:version/:cityname/mining/get-block-winner-id/:blockheight', Mining.GetBlockWinnerId) + .get('/:version/:cityname/mining/get-last-high-value-at-block/:blockheight', Mining.GetLastHighValueAtBlock) + .get('/:version/:cityname/mining/get-miner-at-block/:blockheight/:userid/:default?', Mining.GetMinerAtBlock) + .get('/:version/:cityname/mining/get-mining-stats-at-block/:blockheight/:default?', Mining.GetMiningStatsAtBlock) + .get('/:version/:cityname/mining/has-mined-at-block/:blockheight/:userid', Mining.HasMinedAtBlock) + // Mining claim functions + .get('/:version/:cityname/mining-claims/can-claim-mining-reward/:blockheight/:address', MiningClaims.CanClaimMiningReward) + .get('/:version/:cityname/mining-claims/is-block-winner/:blockheight/:address', MiningClaims.IsBlockWinner) + // Stacking functions + .get('/:version/:cityname/stacking/get-first-stacks-block-in-reward-cycle/:cycleid', Stacking.GetFirstStacksBlockInRewardCycle) + .get('/:version/:cityname/stacking/get-reward-cycle/:blockheight', Stacking.GetRewardCycle) + .get('/:version/:cityname/stacking/get-stacker-at-cycle/:cycleid/:userid/:default?', Stacking.GetStackerAtCycle) + .get('/:version/:cityname/stacking/get-stacking-stats-at-cycle/:cycleid/:default?', Stacking.GetStackingStatsAtCycle) + .get('/:version/:cityname/stacking/stacking-active-at-cycle/:cycleid', Stacking.StackingActiveAtCycle) + // Stacking claim functions + .get('/:version/:cityname/stacking-claims/get-stacking-reward/:cycleid/:userid', StackingClaims.GetStackingReward) + // Token functions + .get('/:version/:cityname/token/get-balance/:address', Token.GetBalance) + .get('/:version/:cityname/token/get-coinbase-amount/:blockheight', Token.GetCoinbaseAmount) + .get('/:version/:cityname/token/get-coinbase-amounts', Token.GetCoinbaseAmounts) + .get('/:version/:cityname/token/get-coinbase-thresholds', Token.GetCoinbaseThresholds) + .get('/:version/:cityname/token/get-decimals', Token.GetDecimals) + .get('/:version/:cityname/token/get-name', Token.GetName) + .get('/:version/:cityname/token/get-symbol', Token.GetSymbol) + .get('/:version/:cityname/token/get-token-uri', Token.GetTokenUri) + .get('/:version/:cityname/token/get-token-uri-json', Token.GetTokenUriJson) + .get('/:version/:cityname/token/get-total-supply', Token.GetTotalSupply) + .get('/token/get-total-supply/:cityname', Token.GetFullTotalSupply) // legacy route for old API integrations + .get('/:cityname/token/get-total-supply', Token.GetFullTotalSupply) + // Tools + .get('/tools/get-city-list', Tools.GetCityList) + .get('/:cityname/tools/get-city-info', Tools.GetCityInfo) + .get('/tools/get-full-city-info', Tools.GetFullCityInfo) + .get('/:version/:cityname/tools/get-city-configuration', Tools.GetCityConfiguration) + .get('/:cityname/tools/get-full-city-configuration', Tools.GetFullCityConfiguration) + .get('/:version/:cityname/tools/get-prices/:currency?', Tools.GetPrices) + .get('/:version/:cityname/tools/proof-of-hodl/:address', Tools.ProofOfHodl) + .get('/:version/:cityname/tools/proof-of-hold/:address', Tools.ProofOfHodl) + // Default route + .all('*', (request) => new Response(`Resource not found, please check the URL: ${request.url}`, { status: 404 })) + +export const handleRequest = async (request: Request): Promise => { + const response: Response = await router.handle(request) + const newResponse = new Response(response.body, response) + newResponse.headers.append('Access-Control-Allow-Origin', '*') + newResponse.headers.append( + 'Access-Control-Allow-Methods', + 'GET, HEAD, POST, OPTIONS', + ) + newResponse.headers.append('Access-Control-Max-Age', '86400') + newResponse.headers.append('CityCoins-API', '2.0.0') + return newResponse +} // returns true if the URL matches a given string function matchDownload(target: string): boolean { @@ -41,3 +119,13 @@ async function returnDownload(event: FetchEvent): Promise { } } } + +addEventListener('fetch', (event) => { + if (matchDownload(event.request.url)) { + // check if item should be downloaded from KV + event.respondWith(returnDownload(event)) + } else { + // then route the request to itty-router + console.info(event.respondWith(handleRequest(event.request))) + } +}) diff --git a/src/lib/citycoins.ts b/src/lib/citycoins.ts index c99f34e..479f8d3 100644 --- a/src/lib/citycoins.ts +++ b/src/lib/citycoins.ts @@ -1,9 +1,10 @@ import { fetchReadOnlyFunction } from 'micro-stacks/api' +import { validateStacksAddress } from 'micro-stacks/crypto' import { standardPrincipalCV, uintCV } from 'micro-stacks/clarity' import { CityConfig } from '../types/cities' import { MinerAtBlock, MiningStatsAtBlock } from '../types/mining' import { StackerAtCycle, StackingStatsAtCycle } from '../types/stacking' -import { CoinbaseThresholds } from '../types/token' +import { CoinbaseAmounts, CoinbaseThresholds } from '../types/token' import { STACKS_NETWORK } from './common' import { getStacksBlockHeight } from './stacks' @@ -11,321 +12,542 @@ import { getStacksBlockHeight } from './stacks' // ACTIVATION FUNCTIONS ////////////////////////////////////////////////// -export async function getActivationBlock(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-activation-block', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer - }, true) +export async function getActivationBlock( + cityConfig: CityConfig, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-activation-block', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getRegisteredUsersNonce(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-registered-users-nonce', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer - }, true) +export async function getActivationTarget( + cityConfig: CityConfig, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-activation-target', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getUser(cityConfig: CityConfig, id: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-user', - functionArgs: [uintCV(id)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getCityWallet(cityConfig: CityConfig): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.auth.name, + functionName: 'get-city-wallet', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getUserId(cityConfig: CityConfig, address: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-user-id', - functionArgs: [standardPrincipalCV(address)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getRegisteredUsersNonce( + cityConfig: CityConfig, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-registered-users-nonce', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) +} + +export async function getUser( + cityConfig: CityConfig, + id: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-user', + functionArgs: [uintCV(id)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) +} + +export async function getUserId( + cityConfig: CityConfig, + address: string, +): Promise { + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-user-id', + functionArgs: [standardPrincipalCV(address)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } ////////////////////////////////////////////////// // MINING FUNCTIONS ////////////////////////////////////////////////// -export async function getBlockWinnerId(cityConfig: CityConfig, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-block-winner-id', - functionArgs: [uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getBlockWinnerId( + cityConfig: CityConfig, + blockHeight: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-block-winner-id', + functionArgs: [uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getMiningStatsAtBlock(cityConfig: CityConfig, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-mining-stats-at-block', - functionArgs: [uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getMiningStatsAtBlock( + cityConfig: CityConfig, + blockHeight: string, + defaultStats = false, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: defaultStats + ? 'get-mining-stats-at-block-or-default' + : 'get-mining-stats-at-block', + functionArgs: [uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getMinerAtBlock(cityConfig: CityConfig, blockHeight: string, userId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-miner-at-block', - functionArgs: [uintCV(blockHeight), uintCV(userId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getMinerAtBlock( + cityConfig: CityConfig, + blockHeight: string, + userId: string, + defaultStats = false, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: defaultStats + ? 'get-miner-at-block-or-default' + : 'get-miner-at-block', + functionArgs: [uintCV(blockHeight), uintCV(userId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getLastHighValueAtBlock(cityConfig: CityConfig, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-last-high-value-at-block', - functionArgs: [uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getLastHighValueAtBlock( + cityConfig: CityConfig, + blockHeight: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-last-high-value-at-block', + functionArgs: [uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function hasMinedAtBlock(cityConfig: CityConfig, blockHeight: string, userId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'has-mined-at-block', - functionArgs: [uintCV(blockHeight), uintCV(userId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function hasMinedAtBlock( + cityConfig: CityConfig, + blockHeight: string, + userId: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'has-mined-at-block', + functionArgs: [uintCV(blockHeight), uintCV(userId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } ////////////////////////////////////////////////// // MINING CLAIM FUNCTIONS ////////////////////////////////////////////////// -export async function canClaimMiningReward(cityConfig: CityConfig, address: string, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'can-claim-mining-reward', - functionArgs: [standardPrincipalCV(address), uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function canClaimMiningReward( + cityConfig: CityConfig, + address: string, + blockHeight: string, +): Promise { + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'can-claim-mining-reward', + functionArgs: [standardPrincipalCV(address), uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function isBlockWinner(cityConfig: CityConfig, address: string, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'is-block-winner', - functionArgs: [standardPrincipalCV(address), uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function isBlockWinner( + cityConfig: CityConfig, + address: string, + blockHeight: string, +): Promise { + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'is-block-winner', + functionArgs: [standardPrincipalCV(address), uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } ////////////////////////////////////////////////// // STACKING FUNCTIONS ////////////////////////////////////////////////// -export async function getStackingStatsAtCycle(cityConfig: CityConfig, cycleId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-stacking-stats-at-cycle', - functionArgs: [uintCV(cycleId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getStackingStatsAtCycle( + cityConfig: CityConfig, + cycleId: string, + defaultStats = false, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: defaultStats + ? 'get-stacking-stats-at-cycle-or-default' + : 'get-stacking-stats-at-cycle', + functionArgs: [uintCV(cycleId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getStackerAtCycle(cityConfig: CityConfig, cycleId: string, userId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-stacker-at-cycle', - functionArgs: [uintCV(cycleId), uintCV(userId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getStackerAtCycle( + cityConfig: CityConfig, + cycleId: string, + userId: string, + defaultStats = false, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: defaultStats + ? 'get-stacker-at-cycle-or-default' + : 'get-stacker-at-cycle', + functionArgs: [uintCV(cycleId), uintCV(userId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getRewardCycle(cityConfig: CityConfig, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-reward-cycle', - functionArgs: [uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getRewardCycle( + cityConfig: CityConfig, + blockHeight: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-reward-cycle', + functionArgs: [uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getFirstStacksBlockInRewardCycle(cityConfig: CityConfig, cycleId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-first-stacks-block-in-reward-cycle', - functionArgs: [uintCV(cycleId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getFirstStacksBlockInRewardCycle( + cityConfig: CityConfig, + cycleId: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-first-stacks-block-in-reward-cycle', + functionArgs: [uintCV(cycleId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function stackingActiveAtCycle(cityConfig: CityConfig, cycleId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'stacking-active-at-cycle', - functionArgs: [uintCV(cycleId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function stackingActiveAtCycle( + cityConfig: CityConfig, + cycleId: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'stacking-active-at-cycle', + functionArgs: [uintCV(cycleId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } ////////////////////////////////////////////////// // STACKING CLAIM FUNCTIONS ////////////////////////////////////////////////// -export async function getStackingReward(cityConfig: CityConfig, cycleId: string, userId: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-stacking-reward', - functionArgs: [uintCV(userId), uintCV(cycleId)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getStackingReward( + cityConfig: CityConfig, + cycleId: string, + userId: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-stacking-reward', + functionArgs: [uintCV(userId), uintCV(cycleId)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } ////////////////////////////////////////////////// // TOKEN FUNCTIONS ////////////////////////////////////////////////// -export async function getCoinbaseThresholds(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-coinbase-thresholds', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getCoinbaseAmounts( + cityConfig: CityConfig, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-coinbase-amounts', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) +} + +export async function getCoinbaseThresholds( + cityConfig: CityConfig, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-coinbase-thresholds', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getCoinbaseAmount(cityConfig: CityConfig, blockHeight: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.coreContract, - functionName: 'get-coinbase-amount', - functionArgs: [uintCV(blockHeight)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getCoinbaseAmount( + cityConfig: CityConfig, + blockHeight: string, +): Promise { + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.core.name, + functionName: 'get-coinbase-amount', + functionArgs: [uintCV(blockHeight)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } export async function getName(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-name', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-name', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } export async function getSymbol(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-symbol', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-symbol', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } export async function getDecimals(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-decimals', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-decimals', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } -export async function getBalance(cityConfig: CityConfig, address: string): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-balance', - functionArgs: [standardPrincipalCV(address)], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) +export async function getBalance( + cityConfig: CityConfig, + address: string, +): Promise { + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-balance', + functionArgs: [standardPrincipalCV(address)], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } export async function getTotalSupply(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-total-supply', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-total-supply', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } export async function getTokenUri(cityConfig: CityConfig): Promise { - return fetchReadOnlyFunction({ - contractAddress: cityConfig.deployer, - contractName: cityConfig.tokenContract, - functionName: 'get-token-uri', - functionArgs: [], - network: STACKS_NETWORK, - senderAddress: cityConfig.deployer, - }, true) + return fetchReadOnlyFunction( + { + contractAddress: cityConfig.deployer, + contractName: cityConfig.token.name, + functionName: 'get-token-uri', + functionArgs: [], + network: STACKS_NETWORK, + senderAddress: cityConfig.deployer, + }, + true, + ) } ////////////////////////////////////////////////// // UTILITY FUNCTIONS ////////////////////////////////////////////////// -export async function getProofOfHodl(cityConfig: CityConfig, address: string): Promise { - // check if the user has a balance - // if so, return true - const balance = await getBalance(cityConfig, address) - .catch(() => { return '' }) - if (+balance > 0) { - return true - } else { - // check if the user is stacking in the current cycle - // if so, return true - const userId = await getUserId(cityConfig, address) - .catch(() => { return '' }) - if (userId === null || userId === '') { return false } +export async function getProofOfHodl( + cityConfig: CityConfig, + address: string, +): Promise { + let userId: string + let balance: string + let stacker: StackerAtCycle + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } + try { + // check if user has a balance + userId = await getUserId(cityConfig, address) + if (userId === null) { + throw new Error(`Address not found: ${address}`) + } + balance = await getBalance(cityConfig, address) + if (+balance > 0) return true const currentBlock = await getStacksBlockHeight() const currentCycle = await getRewardCycle(cityConfig, currentBlock) - const stacker = await getStackerAtCycle(cityConfig, currentCycle, userId) - if (stacker === null) { return false } else { return true } + // check if user is stacking at current cycle + stacker = await getStackerAtCycle(cityConfig, currentCycle, userId) + if (stacker === null) { + return false + } else { + return true + } + } catch (err) { + if (err instanceof Error) throw new Error(err.message) + throw new Error(String(err)) } } diff --git a/src/lib/common.ts b/src/lib/common.ts index b48910d..08d8490 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -1,12 +1,33 @@ -import { StacksMainnet } from "micro-stacks/network" -import { SingleValue } from "../types/common"; +import { StacksMainnet } from 'micro-stacks/network' +import { SingleValue } from '../types/common' -export const STACKS_NETWORK = new StacksMainnet(); +export const STACKS_NETWORK = new StacksMainnet() -export async function createSingleValue(value: string | boolean): Promise { +export const MICRO_UNITS = 1000000 + +// converts "value" into "value: value" +export async function createSingleValue( + value: boolean | number | string, +): Promise { return { value: value } } +// formatter for responses in all handlers +export async function createResponse( + value: boolean | number | string, + format = 'json', +): Promise { + // return only the value + if (format === 'raw') { + return value + } + // default, return value: value + if (format === 'json') { + return await createSingleValue(value) + } + throw new Error(`Unrecognized output format: ${format}`) +} + // fix for isNaN not being reliable export function isStringAllDigits(value: string): boolean { return value.match(/^[0-9]+$/g) !== null diff --git a/src/lib/prices.ts b/src/lib/prices.ts index 6a52457..2ff1c00 100644 --- a/src/lib/prices.ts +++ b/src/lib/prices.ts @@ -1,17 +1,20 @@ -import { CGSimplePrice, Prices } from "../types/common"; +import { CGSimplePrice, Prices } from '../types/common' -export async function getCGPrice(tokenName: string, currency?: string): Promise { - currency = currency ?? "usd"; +export async function getCGPrice( + tokenName: string, + currency?: string, +): Promise { + currency = currency ?? 'usd' // https://www.coingecko.com/api/documentations/v3#/simple/get_simple_price const tokenId = tokenName === 'newyorkcitycoin' ? 'nycccoin' : tokenName const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=${currency}` const response = await fetch(url) if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`) + throw new Error(`CoinGecko: ${response.status} ${response.statusText}`) } const json: CGSimplePrice = await response.json() const prices: Prices = { - "coingecko": json[tokenId][currency] + coingecko: json[tokenId][currency], } return prices } diff --git a/src/lib/stacks.ts b/src/lib/stacks.ts index dca40e3..63ad3e2 100644 --- a/src/lib/stacks.ts +++ b/src/lib/stacks.ts @@ -1,37 +1,64 @@ import { STACKS_NETWORK } from './common' +import { validateStacksAddress } from 'micro-stacks/crypto' export async function getStacksBlockHeight(): Promise { const url = `${STACKS_NETWORK.getCoreApiUrl()}/v2/info` return fetch(url) - .then(response => { + .then((response) => { if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`) } return response.json() as Promise<{ stacks_tip_height: string }> }) - .then(data => { return data.stacks_tip_height }) + .then((data) => { + return data.stacks_tip_height + }) } export async function getStxBalance(address: string): Promise { + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } const url = `${STACKS_NETWORK.getCoreApiUrl()}/extended/v1/address/${address}/stx` return fetch(url) - .then(response => { + .then((response) => { if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`) } return response.json() as Promise<{ balance: string }> }) - .then(data => { return data.balance }) + .then((data) => { + return data.balance + }) } export async function getBnsName(address: string): Promise { + if (!validateStacksAddress(address)) { + throw new Error(`Invalid Stacks address: ${address}`) + } const url = `${STACKS_NETWORK.getCoreApiUrl()}/v1/addresses/stacks/${address}` return fetch(url) - .then(response => { + .then((response) => { if (!response.ok) { throw new Error(`${response.status} ${response.statusText}`) } return response.json() as Promise<{ names: string }> }) - .then(data => { return String(data.names[0]) }) + .then((data) => { + return String(data.names[0]) + }) +} + +export async function getDateAtBlock(blockHeight: string): Promise { + const url = `${STACKS_NETWORK.getCoreApiUrl()}/extended/v1/block/by_height/${blockHeight}` + return fetch(url) + .then((response) => { + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`) + } + return response.json() as Promise<{ burn_block_time_iso: string }> + }) + .then((data) => { + return data.burn_block_time_iso + }) } diff --git a/src/types/cities.ts b/src/types/cities.ts index 96b02f5..418a9fa 100644 --- a/src/types/cities.ts +++ b/src/types/cities.ts @@ -1,67 +1,235 @@ +// CONFIGURATION + +export interface CityList { + [name: string]: CityInfo +} + +export interface CityInfo { + fullName: string + logo: string + versions: string[] + currentVersion: string +} + +export interface CityVersions { + [version: string]: CityConfig +} + export interface CityConfig { - deployer: string, - authContract: string, - coreContract: string, - tokenContract: string, - tokenDisplayName: string, - tokenName: string, - tokenSymbol: string, -} - -const emptyConfig: CityConfig = { - deployer: '', - authContract: '', - coreContract: '', - tokenContract: '', - tokenDisplayName: '', - tokenName: '', - tokenSymbol: '', -} - -const miaConfig: CityConfig = { - deployer: "SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27", - authContract: "miamicoin-auth", - coreContract: "miamicoin-core-v1", - tokenContract: "miamicoin-token", - tokenDisplayName: "MiamiCoin", - tokenName: "miamicoin", - tokenSymbol: "MIA", -}; - -const nycConfig: CityConfig = { - deployer: "SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5", - authContract: "newyorkcitycoin-auth", - coreContract: "newyorkcitycoin-core-v1", - tokenContract: "newyorkcitycoin-token", - tokenDisplayName: "NewYorkCityCoin", - tokenName: "newyorkcitycoin", - tokenSymbol: "NYC", -}; - -export async function getCityConfig(city: string): Promise { - switch (city.toLowerCase()) { - case "mia": - return miaConfig - case "nyc": - return nycConfig + cityName: string + deployed: boolean + deployer: string + auth: AuthContract + core: CoreContract + token: TokenContract +} + +export interface AuthContract { + name: string + initialized: boolean +} + +export interface CoreContract { + name: string + activated: boolean + startBlock?: number + shutdown: boolean + shutdownBlock?: number +} + +export interface TokenContract { + name: string + activated: true + activationBlock?: number + displayName: string + tokenName: string + symbol: string + decimals: number + logo: string + uri: string +} + +// MIAMICOIN + +const miaInfo: CityInfo = { + fullName: 'Miami', + logo: 'https://cdn.citycoins.co/brand/MIA_Miami/Coins/SVG/MiamiCoin_StandAlone_Coin.svg', + versions: ['v1', 'v2'], + currentVersion: 'v2', +} + +const miaConfig: CityVersions = { + v1: { + cityName: 'Miami', + deployed: true, + deployer: 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27', + auth: { + name: 'miamicoin-auth', + initialized: true, + }, + core: { + name: 'miamicoin-core-v1', + activated: false, + startBlock: 24497, + shutdown: true, + shutdownBlock: 58917, + }, + token: { + name: 'miamicoin-token', + activated: true, + activationBlock: 24497, + displayName: 'MiamiCoin', + tokenName: 'miamicoin', + symbol: 'MIA', + decimals: 0, + logo: 'https://cdn.citycoins.co/logos/miamicoin.png', + uri: 'https://cdn.citycoins.co/metadata/miamicoin.json', + }, + }, + v2: { + cityName: 'Miami', + deployed: true, + deployer: 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R', + auth: { + name: 'miamicoin-auth-v2', + initialized: true, + }, + core: { + name: 'miamicoin-core-v2', + activated: true, + startBlock: 58921, + shutdown: false, + }, + token: { + name: 'miamicoin-token-v2', + activated: true, + activationBlock: 24497, + displayName: 'MiamiCoin', + tokenName: 'miamicoin', + symbol: 'MIA', + decimals: 6, + logo: 'https://cdn.citycoins.co/logos/miamicoin.png', + uri: 'https://cdn.citycoins.co/metadata/miamicoin.json', + }, + }, +} + +// NEWYORKCITYCOIN + +const nycInfo: CityInfo = { + fullName: 'New York City', + logo: 'https://cdn.citycoins.co/brand/NYC_NewYorkCity/Coins/SVG/CC_NYCCoin_StandAloneCoin.svg', + versions: ['v1', 'v2'], + currentVersion: 'v2', +} + +const nycConfig: CityVersions = { + v1: { + cityName: 'New York City', + deployed: true, + deployer: 'SP2H8PY27SEZ03MWRKS5XABZYQN17ETGQS3527SA5', + auth: { + name: 'newyorkcitycoin-auth', + initialized: true, + }, + core: { + name: 'newyorkcitycoin-core-v1', + activated: false, + startBlock: 37449, + shutdown: true, + shutdownBlock: 58922, + }, + token: { + name: 'newyorkcitycoin-token', + activated: true, + activationBlock: 37449, + displayName: 'NewYorkCityCoin', + tokenName: 'newyorkcitycoin', + symbol: 'NYC', + decimals: 0, + logo: 'https://cdn.citycoins.co/logos/newyorkcitycoin.png', + uri: 'https://cdn.citycoins.co/metadata/newyorkcitycoin.json', + }, + }, + v2: { + cityName: 'New York City', + deployed: true, + deployer: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11', + auth: { + name: 'newyorkcitycoin-auth-v2', + initialized: true, + }, + core: { + name: 'newyorkcitycoin-core-v2', + activated: true, + startBlock: 58925, + shutdown: false, + }, + token: { + name: 'newyorkcitycoin-token-v2', + activated: true, + activationBlock: 37449, + displayName: 'NewYorkCityCoin', + tokenName: 'newyorkcitycoin', + symbol: 'NYC', + decimals: 6, + logo: 'https://cdn.citycoins.co/logos/newyorkcitycoin.png', + uri: 'https://cdn.citycoins.co/metadata/newyorkcitycoin.json', + }, + }, +} + +// GETTERS + +export const cityList = ['mia', 'nyc'] + +export async function getCityList(): Promise { + return cityList +} + +export async function getCityInfo(city: string): Promise { + switch (city) { + case 'mia': + return miaInfo + case 'nyc': + return nycInfo default: - return emptyConfig + throw new Error(`Invalid city name: ${city}`) } } -/* IDEAS - -interface CitySettings { - config: CityConfig, - cityName: string; - cityWallet: string; - deployed: boolean; - activated: boolean; - startBlock?: number; +export async function getFullCityInfo(): Promise { + return { + mia: miaInfo, + nyc: nycInfo, + } } -// cityLogo: imported SVG? -// https://stackoverflow.com/questions/44717164/unable-to-import-svg-files-in-typescript -// another name: CityDetails +export async function getCityConfig( + city: string, + version: string, +): Promise { + version = version.toLowerCase() + switch (city.toLowerCase()) { + case 'mia': + if (Object.prototype.hasOwnProperty.call(miaConfig, version)) + return miaConfig[version] + break + case 'nyc': + if (Object.prototype.hasOwnProperty.call(nycConfig, version)) + return nycConfig[version] + break + } + throw new Error(`Invalid city name or version: ${city} ${version}`) +} -*/ \ No newline at end of file +export async function getFullCityConfig(city: string): Promise { + switch (city.toLowerCase()) { + case 'mia': + return miaConfig + case 'nyc': + return nycConfig + default: + throw new Error(`Invalid city name: ${city}`) + } +} diff --git a/src/types/common.ts b/src/types/common.ts index 3b64e74..1e03e65 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,6 +1,5 @@ - export interface SingleValue { - value: string | boolean + value: boolean | number | string } export interface Prices { diff --git a/src/types/mining.ts b/src/types/mining.ts index 3e5e6e1..6952d07 100644 --- a/src/types/mining.ts +++ b/src/types/mining.ts @@ -1,15 +1,14 @@ - export interface MiningStatsAtBlock { - minersCount: number, - amount: number, - amountToCity: number, - amountToStackers: number, - rewardClaimed: boolean, + minersCount: number + amount: number + amountToCity: number + amountToStackers: number + rewardClaimed: boolean } export interface MinerAtBlock { - ustx: number, - lowValue: number, - highValue: number, - winner: boolean, + ustx: number + lowValue: number + highValue: number + winner: boolean } diff --git a/src/types/stacking.ts b/src/types/stacking.ts index 4f86ba4..61e6391 100644 --- a/src/types/stacking.ts +++ b/src/types/stacking.ts @@ -1,10 +1,9 @@ - export interface StackingStatsAtCycle { - amountUstx: number, - amountToken: number, + amountUstx: number + amountToken: number } export interface StackerAtCycle { - amountStacked: number, - toReturn: number, + amountStacked: number + toReturn: number } diff --git a/src/types/token.ts b/src/types/token.ts index 9d523ac..66a32ac 100644 --- a/src/types/token.ts +++ b/src/types/token.ts @@ -1,14 +1,23 @@ +export interface CoinbaseAmounts { + coinbaseAmount1: number + coinbaseAmount2: number + coinbaseAmount3: number + coinbaseAmount4: number + coinbaseAmount5: number + coinbaseAmountBonus: number + coinbaseAmountDefault: number +} export interface CoinbaseThresholds { - coinbaseThreshold1: number, - coinbaseThreshold2: number, - coinbaseThreshold3: number, - coinbaseThreshold4: number, - coinbaseThreshold5: number, + coinbaseThreshold1: number + coinbaseThreshold2: number + coinbaseThreshold3: number + coinbaseThreshold4: number + coinbaseThreshold5: number } export interface TokenUri { - name: string, - description: string, - image: string, + name: string + description: string + image: string } diff --git a/static/openapi.yml b/static/openapi.yml index 0563889..cb86f87 100644 --- a/static/openapi.yml +++ b/static/openapi.yml @@ -2,7 +2,7 @@ openapi: 3.0.2 info: title: CityCoins API description: A simple API to interact with Stacks and CityCoins data. See the [CityCoins documentation site](https://docs.citycoins.co/developer-resources/api) or [GitHub for more info.](https://github.com/citycoins/api) - version: 1.1.0 + version: 2.0.0 servers: - url: https://api.citycoins.co description: Main @@ -53,6 +53,8 @@ paths: description: Get the current Stacks block height tags: - Stacks + parameters: + - $ref: '#/components/parameters/format' responses: "200": $ref: '#/components/responses/200SuccessJsonNumber' @@ -66,6 +68,7 @@ paths: tags: - Stacks parameters: + - $ref: '#/components/parameters/format' - $ref: '#/components/parameters/address' responses: "200": @@ -82,6 +85,7 @@ paths: tags: - Stacks parameters: + - $ref: '#/components/parameters/format' - $ref: '#/components/parameters/address' responses: "200": @@ -91,13 +95,50 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /activation/get-activation-block/{cityname}: + /stacks/get-date-at-block/{blockheight}: + get: + summary: Get Date at Block Height + description: Get the ISO-formatted date for a given Stacks block height + tags: + - Stacks + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/blockheight' + responses: + "200": + $ref: '#/components/responses/200SuccessJsonString' + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/activation/get-activation-block: get: summary: Get Activation Block + description: Get the Stacks block height the token contract was activated at for a given CityCoin + tags: + - Activation + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' + - $ref: '#/components/parameters/cityname' + responses: + "200": + $ref: '#/components/responses/200SuccessJsonString' + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/activation/get-activation-target: + get: + summary: Get Activation Target description: Get the Stacks block height the core contract was activated at for a given CityCoin tags: - Activation parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -107,13 +148,33 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /activation/get-registered-users-nonce/{cityname}: + /{version}/{cityname}/activation/get-city-wallet: + get: + summary: Get City Wallet Address + description: Get the city wallet address set in the auth contract for a given CityCoin and version. + tags: + - Activation + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' + - $ref: '#/components/parameters/cityname' + responses: + "200": + $ref: '#/components/responses/200SuccessJsonString' + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/activation/get-registered-users-nonce: get: summary: Get Registered Users description: Get the total number of users assigned an ID from the core contract for a given CityCoin tags: - Activation parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -123,13 +184,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /activation/get-user/{cityname}/{userid}: + /{version}/{cityname}/activation/get-user/{userid}: get: summary: Get User description: Get the Stacks address for a given CityCoin and user ID tags: - Activation parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/userid' responses: @@ -140,13 +203,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /activation/get-user-id/{cityname}/{address}: + /{version}/{cityname}/activation/get-user-id/{address}: get: summary: Get User ID description: Get the CityCoins user ID from the core contract for a given CityCoin and Stacks address tags: - Activation parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/address' responses: @@ -157,13 +222,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining/get-block-winner-id/{cityname}/{blockheight}: + /{version}/{cityname}/mining/get-block-winner-id/{blockheight}: get: summary: Get Block Winner ID description: Gets the winning user ID for a given blockheight only *after the miner claims the reward* tags: - Mining parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' responses: @@ -174,15 +241,17 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining/get-mining-stats-at-block/{cityname}/{blockheight}: + /{version}/{cityname}/mining/get-mining-stats-at-block/{blockheight}/{default}: get: summary: Get Mining Stats description: Get the mining stats for a given CityCoin and Stacks block height tags: - Mining parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' + - $ref: '#/components/parameters/default' responses: "200": description: Success @@ -214,16 +283,18 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining/get-miner-at-block/{cityname}/{blockheight}/{userid}: + /{version}/{cityname}/mining/get-miner-at-block/{blockheight}/{userid}/{default}: get: summary: Get Miner Info description: Get the miner details for a given CityCoin, Stacks block height, and user ID. tags: - Mining parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' - $ref: '#/components/parameters/userid' + - $ref: '#/components/parameters/default' responses: "200": description: Success @@ -252,13 +323,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining/get-last-high-value-at-block/{cityname}/{blockheight}: + /{version}/{cityname}/mining/get-last-high-value-at-block//{blockheight}: get: summary: Get Last High Value at Block description: Gets the last high value at the specified block used to determine the winner by the VRF, and increases with each miner commitment. tags: - Mining parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' responses: @@ -269,13 +342,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining/has-mined-at-block/{cityname}/{blockheight}/{userid}: + /{version}/{cityname}/mining/has-mined-at-block/{blockheight}/{userid}: get: summary: Has Mined at Block description: Check if a the user has mined with a given CityCoin, Stacks block height, and user ID. tags: - Mining parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' - $ref: '#/components/parameters/userid' @@ -287,13 +362,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining-claims/can-claim-mining-reward/{cityname}/{blockheight}/{address}: + /{version}/{cityname}/mining-claims/can-claim-mining-reward/{blockheight}/{address}: get: summary: Can Claim Mining Reward description: Returns a boolean value indicating if the user's principal won and is eligible to claim the block reward at a given block height. tags: - Mining Claims parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' - $ref: '#/components/parameters/address' @@ -305,13 +382,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /mining-claims/is-block-winner/{cityname}/{blockheight}/{address}: + /{version}/{cityname}/mining-claims/is-block-winner/{blockheight}/{address}: get: summary: Is Block Winner description: Returns a boolean value indicating if the user's principal won at a given block height. tags: - Mining Claims parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' - $ref: '#/components/parameters/address' @@ -323,15 +402,17 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /stacking/get-stacking-stats-at-cycle/{cityname}/{cycleid}: + /{version}/{cityname}/stacking/get-stacking-stats-at-cycle/{cycleid}/{default}: get: summary: Get Stacking Stats description: Get the stacking stats for a given CityCoin and reward cycle ID tags: - Stacking parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/cycleid' + - $ref: '#/components/parameters/default' responses: "200": description: Success @@ -354,16 +435,18 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /stacking/get-stacker-at-cycle/{cityname}/{cycleid}/{userid}: + /{version}/{cityname}/stacking/get-stacker-at-cycle/{cycleid}/{userid}/{default}: get: summary: Get Stacker Info description: Get the amount stacked and amount to return for a given CityCoin, reward cycle ID, and user ID. tags: - Stacking parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/cycleid' - $ref: '#/components/parameters/userid' + - $ref: '#/components/parameters/default' responses: "200": description: Success @@ -386,13 +469,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /stacking/get-reward-cycle/{cityname}/{blockheight}: + /{version}/{cityname}/stacking/get-reward-cycle/{blockheight}: get: summary: Get Reward Cycle description: Get the reward cycle for a given CityCoin and Stacks block height tags: - Stacking parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' responses: @@ -403,13 +488,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /stacking/get-first-stacks-block-in-reward-cycle/{cityname}/{cycleid}: + /{version}/{cityname}/stacking/get-first-stacks-block-in-reward-cycle/{cycleid}: get: summary: Get First Block In Reward Cycle description: Get the first Stacks block in a reward cycle for the given CityCoin and reward cycle ID tags: - Stacking parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/cycleid' responses: @@ -420,13 +507,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /stacking/stacking-active-at-cycle/{cityname}/{cycleid}: + /{version}/{cityname}/stacking/stacking-active-at-cycle/{cycleid}: get: summary: Stacking Active at Cycle description: Returns a boolean value indicating if stacking is active at a given reward cycle, meaning a positive number of CityCoins are Stacked for that cycle. tags: - Stacking parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/cycleid' responses: @@ -437,7 +526,7 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /stacking-claims/get-stacking-reward/{cityname}/{cycleid}/{userid}: + /{version}/{cityname}/stacking-claims/get-stacking-reward/{cycleid}/{userid}: get: summary: Get Stacking Reward description: | @@ -448,6 +537,8 @@ paths: tags: - Stacking Claims parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/cycleid' - $ref: '#/components/parameters/userid' @@ -459,13 +550,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-coinbase-amount/{cityname}/{blockheight}: + /{version}/{cityname}/token/get-coinbase-amount/{blockheight}: get: summary: Get Coinbase Amount description: Get the coinbase amount for a given CityCoin and Stacks block height tags: - Token parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' responses: @@ -476,13 +569,60 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-coinbase-thresholds/{cityname}: + /{version}/{cityname}/token/get-coinbase-amounts: + get: + summary: Get Coinbase Amounts + description: Gets the coinbase amounts for each coinbase threshold for a given CityCoin + tags: + - Token + parameters: + - $ref: '#/components/parameters/version' + - $ref: '#/components/parameters/cityname' + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + properties: + coinbaseAmount1: + type: string + coinbaseAmount2: + type: string + coinbaseAmount3: + type: string + coinbaseAmount4: + type: string + coinbaseAmount5: + type: string + coinbaseAmountBonus: + type: string + coinbaseAmountDefault: + type: string + examples: + "0": + value: + coinbaseAmount1: "100000000000" + coinbaseAmount2: "50000000000" + coinbaseAmount3: "25000000000" + coinbaseAmount4: "12500000000" + coinbaseAmount5: "6250000000" + coinbaseAmountBonus: "250000000000" + coinbaseAmountDefault: "3125000000" + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/token/get-coinbase-thresholds: get: summary: Get Coinbase Thresholds description: Get the coinbase thresholds (Stacks block heights) for a given CityCoin tags: - Token parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/blockheight' responses: @@ -506,23 +646,24 @@ paths: examples: "0": value: - coinbaseThreshold1: "234497" - coinbaseThreshold2: "444497" - coinbaseThreshold3: "654497" - coinbaseThreshold4: "864497" - coinbaseThreshold5: "1074497" + coinbaseThreshold1: "59497" + coinbaseThreshold2: "109497" + coinbaseThreshold3: "209497" + coinbaseThreshold4: "409497" + coinbaseThreshold5: "809497" "400": $ref: '#/components/responses/400BadRequest' "404": $ref: '#/components/responses/404NotFound' - /token/get-name/{cityname}: + /{version}/{cityname}/token/get-name: get: summary: Get Token Name description: Get the SIP-010 name for a given CityCoin tags: - Token parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -532,13 +673,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-symbol/{cityname}: + /{versions}/{cityname}/token/get-symbol: get: summary: Get Token Symbol description: Get the SIP-010 symbol for a given CityCoin tags: - Token parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -548,13 +691,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-decimals/{cityname}: + /{version}/{cityname}/token/get-decimals: get: summary: Get Token Decimals description: Get the SIP-010 decimals for a given CityCoin tags: - Token parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -564,13 +709,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-balance/{cityname}/{address}: + /{version}/{cityname}/token/get-balance/{address}: get: summary: Get Token Balance description: Get the token balance for a given CityCoin and Stacks address tags: - Token parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/address' responses: @@ -581,13 +728,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-total-supply/{cityname}: + /{version}/{cityname}/token/get-total-supply: get: - summary: Get Total Supply - description: Get the SIP-010 total supply for a given CityCoin + summary: Get Total Supply (single) + description: Get the SIP-010 total supply for a given CityCoin and version tags: - Token parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -597,13 +746,58 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-token-uri/{cityname}: + /{cityname}/token/get-total-supply: + get: + summary: Get Total Supply (all) + description: | + Get the SIP-010 total supply for a given CityCoin across all versions. + + Note: This will return the value in "CityCoins" not "micro-CityCoins" with 6 decimal places added to the calculation. + tags: + - Token + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/cityname' + responses: + "200": + $ref: '#/components/responses/200SuccessJsonString' + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /token/get-total-supply/{cityname}: + get: + summary: Get Total Supply (legacy) + description: | + Get the SIP-010 total supply for a given CityCoin across all versions. + + Note: This will return the value in "CityCoins" not "micro-CityCoins" with 6 decimal places added to the calculation. + + This endpoint is deprecated in favor of the new format `/{cityname}/token/get-total-supply` but will remain active until all known external services are converted. + deprecated: true + tags: + - Token + parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/cityname' + responses: + "200": + $ref: '#/components/responses/200SuccessJsonString' + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/token/get-token-uri: get: summary: Get Token URI description: Get the token URI URL for a given CityCoin tags: - Token parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -613,13 +807,14 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /token/get-token-uri-json/{cityname}: + /{version}/{cityname}/token/get-token-uri-json: get: summary: Get Token URI JSON description: Get the contents of the token URI URL for a given CityCoin tags: - Token parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' responses: "200": @@ -646,7 +841,207 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /tools/prices/{cityname}/{currency}: + /tools/get-city-list: + get: + summary: Get City List + description: Get the list of city short names supported by the API. + tags: + - Tools + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/citylist' + examples: + "0": + value: ["mia", "nyc"] + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{cityname}/tools/get-city-info: + get: + summary: Get City Info + description: Gets the basic info about a city including available and current contract versions. + tags: + - Tools + parameters: + - $ref: '#/components/parameters/cityname' + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/cityinfo' + examples: + "0": + value: + fullName: "Miami" + logo: "https://cdn.citycoins.co/brand/MIA_Miami/Coins/SVG/MiamiCoin_StandAlone_Coin.svg" + versions: ["v1", "v2"] + currentVersion: "v2" + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /tools/get-full-city-info: + get: + summary: Get Full City Info + description: Gets the full city information data for all known cities supported by the API. + tags: + - Tools + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/cityinfolist' + examples: + "0": + value: + "mia": + fullName: "Miami" + logo: "https://cdn.citycoins.co/brand/MIA_Miami/Coins/SVG/MiamiCoin_StandAlone_Coin.svg" + versions: ["v1", "v2"] + currentVersion: "v2" + "nyc": + fullName: "New York City" + logo: "https://cdn.citycoins.co/brand/NYC_NewYorkCity/Coins/SVG/CC_NYCCoin_StandAloneCoin.svg" + versions: ["v1", "v2"] + currentVersion: "v2" + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/tools/get-city-configuration: + get: + summary: Get City Configuration + description: Get the city configuration data depending on the select version and city name. + tags: + - Tools + parameters: + - $ref: '#/components/parameters/version' + - $ref: '#/components/parameters/cityname' + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + items: + $ref: '#/components/schemas/cityconfig' + examples: + "0": + value: + cityName: "Miami" + deployed: true + deployer: "SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27" + auth: + name: "miamicoin-auth" + initialized: true + core: + name: "miamicoin-core-v1" + activated: false + startBlock: 24497 + shutdown: true + shutdownBlock: 58917 + token: + name: "miamicoin-token" + activated: true + activationBlock: 24497 + displayName: "MiamiCoin" + tokenName: "miamicoin" + symbol: "MIA" + decimals: 0 + logo: "https://cdn.citycoins.co/logos/miamicoin.png" + uri: "https://cdn.citycoins.co/metadata/miamicoin.json" + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{cityname}/tools/get-full-city-configuration: + get: + summary: Get Full City Configuration + description: Gets the full city configuration data including all versions depending on the select city name. + tags: + - Tools + parameters: + - $ref: '#/components/parameters/cityname' + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + properties: + version: + type: string + additionalProperties: + $ref: '#/components/schemas/cityconfig' + examples: + "0": + value: + "v1": + cityName: "Miami" + deployed: true + deployer: "SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27" + auth: + name: "miamicoin-auth" + initialized: true + core: + name: "miamicoin-core-v1" + activated: false + startBlock: 24497 + shutdown: true + shutdownBlock: 58917 + token: + name: "miamicoin-token" + activated: true + activationBlock: 24497 + displayName: "MiamiCoin" + tokenName: "miamicoin" + symbol: "MIA" + decimals: 0 + logo: "https://cdn.citycoins.co/logos/miamicoin.png" + uri: "https://cdn.citycoins.co/metadata/miamicoin.json" + "v2": + cityName: "Miami" + deployed: true + deployer: "SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R" + auth: + name: "miamicoin-auth-v2" + initialized: true + core: + name: "miamicoin-core-v2" + activated: false + startBlock: 58921 + shutdown: false + token: + name: "miamicoin-token-v2" + activated: true + activationBlock: 24497 + displayName: "MiamiCoin" + tokenName: "miamicoin" + symbol: "MIA" + decimals: 6 + logo: "https://cdn.citycoins.co/logos/miamicoin.png" + uri: "https://cdn.citycoins.co/metadata/miamicoin.json" + "400": + $ref: '#/components/responses/400BadRequest' + "404": + $ref: '#/components/responses/404NotFound' + + /{version}/{cityname}/tools/get-prices/{currency}: get: summary: Get Prices description: | @@ -656,6 +1051,7 @@ paths: tags: - Tools parameters: + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/currency' responses: @@ -677,13 +1073,15 @@ paths: "404": $ref: '#/components/responses/404NotFound' - /tools/proof-of-hodl/{cityname}/{address}: + /{version}/{cityname}/tools/proof-of-hodl/{address}: get: summary: Check Proof of Hodl description: Check whether the user is holding or stacking a given CityCoin at a given address tags: - Tools parameters: + - $ref: '#/components/parameters/format' + - $ref: '#/components/parameters/version' - $ref: '#/components/parameters/cityname' - $ref: '#/components/parameters/address' responses: @@ -743,6 +1141,104 @@ components: schema: type: string description: The user ID stored in a CityCoins core contract for a Stacks address + version: + in: path + name: version + required: true + schema: + type: string + enum: [v1, v2] + description: The major CityCoins contract version. + default: + in: path + name: default + required: true + schema: + type: string + enum: ["true", "false"] + description: (Optional) Toggle calling the `or-default` version of the function which returns an empty value instead of an error. Defaults to `false`. + format: + in: query + name: format + schema: + type: string + enum: ["json", "raw"] + description: (Optional) The output format for the response. Defaults to `json`. + # Reusable objects + schemas: + citylist: + type: array + items: + type: string + enum: [mia, nyc] + cityinfo: + type: object + properties: + fullName: + type: string + logo: + type: string + versions: + $ref: '#/components/schemas/citylist' + currentVersion: + type: string + cityinfolist: + type: object + properties: + cityname: + $ref: '#/components/schemas/citylist' + additionalProperties: + $ref: '#/components/schemas/cityinfo' + cityconfig: + type: object + properties: + cityName: + type: string + deployed: + type: boolean + deployer: + type: string + auth: + type: object + properties: + name: + type: string + initialized: + type: boolean + core: + type: object + properties: + name: + type: string + activated: + type: boolean + startBlock: + type: number + shutdown: + type: boolean + shutdownBlock: + type: number + token: + type: object + properties: + name: + type: string + activated: + type: boolean + activationBlock: + type: number + displayName: + type: string + tokenName: + type: string + symbol: + type: string + decimals: + type: number + logo: + type: string + uri: + type: string #------------------------------- # Reusable responses #-------------------------------