From 72a560f0f713c983c11e47fc9383eafb93186d83 Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Fri, 13 Nov 2020 23:56:22 +0100 Subject: [PATCH] feat: API for total burn reward amount for address --- client/src/generated/apis/BurnchainApi.ts | 55 ++++++++++++++++ .../generated/models/BurnchainRewardsTotal.ts | 65 +++++++++++++++++++ client/src/generated/models/index.ts | 1 + .../burnchain/rewards-total.example.json | 4 ++ .../burnchain/rewards-total.schema.json | 17 +++++ docs/index.d.ts | 14 ++++ docs/openapi.yaml | 23 +++++++ src/api/routes/burnchain.ts | 30 +++++++++ src/datastore/common.ts | 3 + src/datastore/memory-store.ts | 6 ++ src/datastore/postgres-store.ts | 26 ++++++++ src/tests/api-tests.ts | 56 ++++++++++++++++ 12 files changed, 300 insertions(+) create mode 100644 client/src/generated/models/BurnchainRewardsTotal.ts create mode 100644 docs/entities/burnchain/rewards-total.example.json create mode 100644 docs/entities/burnchain/rewards-total.schema.json diff --git a/client/src/generated/apis/BurnchainApi.ts b/client/src/generated/apis/BurnchainApi.ts index 290584dad3..3dfd4f987f 100644 --- a/client/src/generated/apis/BurnchainApi.ts +++ b/client/src/generated/apis/BurnchainApi.ts @@ -18,6 +18,9 @@ import { BurnchainRewardListResponse, BurnchainRewardListResponseFromJSON, BurnchainRewardListResponseToJSON, + BurnchainRewardsTotal, + BurnchainRewardsTotalFromJSON, + BurnchainRewardsTotalToJSON, } from '../models'; export interface GetBurnchainRewardListRequest { @@ -31,6 +34,10 @@ export interface GetBurnchainRewardListByAddressRequest { offset?: number; } +export interface GetBurnchainRewardsTotalByAddressRequest { + address: string; +} + /** * BurnchainApi - interface * @@ -73,6 +80,22 @@ export interface BurnchainApiInterface { */ getBurnchainRewardListByAddress(requestParameters: GetBurnchainRewardListByAddressRequest): Promise; + /** + * Get the total burnchain (e.g. Bitcoin) rewards for the given recipient + * @summary Get total burnchain rewards for the given recipient + * @param {string} address Reward recipient address. Should either be in the native burnchain\'s format (e.g. B58 for Bitcoin), or if a STX principal address is provided it will be encoded as into the equivalent burnchain format + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof BurnchainApiInterface + */ + getBurnchainRewardsTotalByAddressRaw(requestParameters: GetBurnchainRewardsTotalByAddressRequest): Promise>; + + /** + * Get the total burnchain (e.g. Bitcoin) rewards for the given recipient + * Get total burnchain rewards for the given recipient + */ + getBurnchainRewardsTotalByAddress(requestParameters: GetBurnchainRewardsTotalByAddressRequest): Promise; + } /** @@ -156,4 +179,36 @@ export class BurnchainApi extends runtime.BaseAPI implements BurnchainApiInterfa return await response.value(); } + /** + * Get the total burnchain (e.g. Bitcoin) rewards for the given recipient + * Get total burnchain rewards for the given recipient + */ + async getBurnchainRewardsTotalByAddressRaw(requestParameters: GetBurnchainRewardsTotalByAddressRequest): Promise> { + if (requestParameters.address === null || requestParameters.address === undefined) { + throw new runtime.RequiredError('address','Required parameter requestParameters.address was null or undefined when calling getBurnchainRewardsTotalByAddress.'); + } + + const queryParameters: runtime.HTTPQuery = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/extended/v1/burnchain/rewards/{address}/total`.replace(`{${"address"}}`, encodeURIComponent(String(requestParameters.address))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => BurnchainRewardsTotalFromJSON(jsonValue)); + } + + /** + * Get the total burnchain (e.g. Bitcoin) rewards for the given recipient + * Get total burnchain rewards for the given recipient + */ + async getBurnchainRewardsTotalByAddress(requestParameters: GetBurnchainRewardsTotalByAddressRequest): Promise { + const response = await this.getBurnchainRewardsTotalByAddressRaw(requestParameters); + return await response.value(); + } + } diff --git a/client/src/generated/models/BurnchainRewardsTotal.ts b/client/src/generated/models/BurnchainRewardsTotal.ts new file mode 100644 index 0000000000..b668541728 --- /dev/null +++ b/client/src/generated/models/BurnchainRewardsTotal.ts @@ -0,0 +1,65 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Stacks 2.0 Blockchain API + * This is the documentation for the Stacks 2.0 Blockchain API. It is comprised of two parts; the Stacks Blockchain API and the Stacks Core API. [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/614feab5c108d292bffa#?env%5BStacks%20Blockchain%20API%5D=W3sia2V5Ijoic3R4X2FkZHJlc3MiLCJ2YWx1ZSI6IlNUMlRKUkhESE1ZQlE0MTdIRkIwQkRYNDMwVFFBNVBYUlg2NDk1RzFWIiwiZW5hYmxlZCI6dHJ1ZX0seyJrZXkiOiJibG9ja19pZCIsInZhbHVlIjoiMHgiLCJlbmFibGVkIjp0cnVlfSx7ImtleSI6Im9mZnNldCIsInZhbHVlIjoiMCIsImVuYWJsZWQiOnRydWV9LHsia2V5IjoibGltaXRfdHgiLCJ2YWx1ZSI6IjIwMCIsImVuYWJsZWQiOnRydWV9LHsia2V5IjoibGltaXRfYmxvY2siLCJ2YWx1ZSI6IjMwIiwiZW5hYmxlZCI6dHJ1ZX0seyJrZXkiOiJ0eF9pZCIsInZhbHVlIjoiMHg1NDA5MGMxNmE3MDJiNzUzYjQzMTE0ZTg4NGJjMTlhODBhNzk2MzhmZDQ0OWE0MGY4MDY4Y2RmMDAzY2RlNmUwIiwiZW5hYmxlZCI6dHJ1ZX0seyJrZXkiOiJjb250cmFjdF9pZCIsInZhbHVlIjoiU1RKVFhFSlBKUFBWRE5BOUIwNTJOU1JSQkdRQ0ZOS1ZTMTc4VkdIMS5oZWxsb193b3JsZFxuIiwiZW5hYmxlZCI6dHJ1ZX0seyJrZXkiOiJidGNfYWRkcmVzcyIsInZhbHVlIjoiYWJjIiwiZW5hYmxlZCI6dHJ1ZX0seyJrZXkiOiJjb250cmFjdF9hZGRyZXNzIiwidmFsdWUiOiJTVEpUWEVKUEpQUFZETkE5QjA1Mk5TUlJCR1FDRk5LVlMxNzhWR0gxIiwiZW5hYmxlZCI6dHJ1ZX0seyJrZXkiOiJjb250cmFjdF9uYW1lIiwidmFsdWUiOiJoZWxsb193b3JsZCIsImVuYWJsZWQiOnRydWV9LHsia2V5IjoiY29udHJhY3RfbWFwIiwidmFsdWUiOiJzdG9yZSIsImVuYWJsZWQiOnRydWV9LHsia2V5IjoiY29udHJhY3RfbWV0aG9kIiwidmFsdWUiOiJnZXQtdmFsdWUiLCJlbmFibGVkIjp0cnVlfV0=) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Total burnchain rewards made to a recipient + * @export + * @interface BurnchainRewardsTotal + */ +export interface BurnchainRewardsTotal { + /** + * The recipient address that received the burnchain rewards, in the format native to the burnchain (e.g. B58 encoded for Bitcoin) + * @type {string} + * @memberof BurnchainRewardsTotal + */ + reward_recipient: string; + /** + * The total amount of burnchain tokens rewarded to the recipient, in the smallest unit (e.g. satoshis for Bitcoin) + * @type {string} + * @memberof BurnchainRewardsTotal + */ + reward_amount: string; +} + +export function BurnchainRewardsTotalFromJSON(json: any): BurnchainRewardsTotal { + return BurnchainRewardsTotalFromJSONTyped(json, false); +} + +export function BurnchainRewardsTotalFromJSONTyped(json: any, ignoreDiscriminator: boolean): BurnchainRewardsTotal { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'reward_recipient': json['reward_recipient'], + 'reward_amount': json['reward_amount'], + }; +} + +export function BurnchainRewardsTotalToJSON(value?: BurnchainRewardsTotal | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'reward_recipient': value.reward_recipient, + 'reward_amount': value.reward_amount, + }; +} + + diff --git a/client/src/generated/models/index.ts b/client/src/generated/models/index.ts index d997eb3e74..e6735df4c5 100644 --- a/client/src/generated/models/index.ts +++ b/client/src/generated/models/index.ts @@ -8,6 +8,7 @@ export * from './Block'; export * from './BlockListResponse'; export * from './BurnchainReward'; export * from './BurnchainRewardListResponse'; +export * from './BurnchainRewardsTotal'; export * from './ContractInterfaceResponse'; export * from './ContractSourceResponse'; export * from './CoreNodeInfoResponse'; diff --git a/docs/entities/burnchain/rewards-total.example.json b/docs/entities/burnchain/rewards-total.example.json new file mode 100644 index 0000000000..bb5da954eb --- /dev/null +++ b/docs/entities/burnchain/rewards-total.example.json @@ -0,0 +1,4 @@ +{ + "reward_recipient": "1C56LYirKa3PFXFsvhSESgDy2acEHVAEt6", + "reward_amount": "18000" +} diff --git a/docs/entities/burnchain/rewards-total.schema.json b/docs/entities/burnchain/rewards-total.schema.json new file mode 100644 index 0000000000..8d58d28cb2 --- /dev/null +++ b/docs/entities/burnchain/rewards-total.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BurnchainRewardsTotal", + "description": "Total burnchain rewards made to a recipient", + "type": "object", + "required": ["reward_recipient", "reward_amount"], + "properties": { + "reward_recipient": { + "type": "string", + "description": "The recipient address that received the burnchain rewards, in the format native to the burnchain (e.g. B58 encoded for Bitcoin)" + }, + "reward_amount": { + "type": "string", + "description": "The total amount of burnchain tokens rewarded to the recipient, in the smallest unit (e.g. satoshis for Bitcoin)" + } + } +} diff --git a/docs/index.d.ts b/docs/index.d.ts index 1c539e6438..7bea666fe2 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -845,6 +845,20 @@ export interface BurnchainReward { reward_index: number; } +/** + * Total burnchain rewards made to a recipient + */ +export interface BurnchainRewardsTotal { + /** + * The recipient address that received the burnchain rewards, in the format native to the burnchain (e.g. B58 encoded for Bitcoin) + */ + reward_recipient: string; + /** + * The total amount of burnchain tokens rewarded to the recipient, in the smallest unit (e.g. satoshis for Bitcoin) + */ + reward_amount: string; +} + /** * Describes representation of a Type-0 Stacks 2.0 transaction. https://github.com/blockstack/stacks-blockchain/blob/master/sip/sip-005-blocks-and-transactions.md#type-0-transferring-an-asset */ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index dd2d2585c9..956c55e3ca 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -330,6 +330,29 @@ paths: $ref: ./api/burnchain/get-rewards.schema.json example: $ref: ./api/burnchain/get-rewards.example.json + /extended/v1/burnchain/rewards/{address}/total: + get: + summary: Get total burnchain rewards for the given recipient + description: Get the total burnchain (e.g. Bitcoin) rewards for the given recipient + tags: + - Burnchain + operationId: get_burnchain_rewards_total_by_address + parameters: + - name: address + in: path + description: Reward recipient address. Should either be in the native burnchain's format (e.g. B58 for Bitcoin), or if a STX principal address is provided it will be encoded as into the equivalent burnchain format + required: true + schema: + type: string + responses: + 200: + description: List of burnchain reward recipients and amounts + content: + application/json: + schema: + $ref: ./entities/burnchain/rewards-total.schema.json + example: + $ref: ./entities/burnchain/rewards-total.example.json /extended/v1/contract/{contract_id}: get: diff --git a/src/api/routes/burnchain.ts b/src/api/routes/burnchain.ts index 72a840f2d1..1890257373 100644 --- a/src/api/routes/burnchain.ts +++ b/src/api/routes/burnchain.ts @@ -3,6 +3,7 @@ import { addAsync, RouterWithAsync } from '@awaitjs/express'; import { BurnchainReward, BurnchainRewardListResponse, + BurnchainRewardsTotal, } from '@blockstack/stacks-blockchain-api-types'; import { DataStore } from '../../datastore/common'; @@ -85,5 +86,34 @@ export function createBurnchainRouter(db: DataStore): RouterWithAsync { res.json(response); }); + router.getAsync('/rewards/:address/total', async (req, res) => { + const { address } = req.params; + + let burnchainAddress: string | undefined = undefined; + const queryAddr = address.trim(); + if (isValidBitcoinAddress(queryAddr)) { + burnchainAddress = queryAddr; + } else { + const convertedAddr = tryConvertC32ToBtc(queryAddr); + if (convertedAddr) { + burnchainAddress = convertedAddr; + } + } + if (!burnchainAddress) { + res + .status(400) + .json({ error: `Address ${queryAddr} is not a valid Bitcoin or STX address.` }); + return; + } + + const queryResults = await db.getBurnchainRewardsTotal(burnchainAddress); + const response: BurnchainRewardsTotal = { + reward_recipient: queryResults.reward_recipient, + reward_amount: queryResults.reward_amount.toString(), + }; + // TODO: schema validation + res.json(response); + }); + return router; } diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 4060c43c6d..69b784ebf1 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -328,6 +328,9 @@ export interface DataStore extends DataStoreEventEmitter { limit: number; offset: number; }): Promise; + getBurnchainRewardsTotal( + burnchainRecipient: string + ): Promise<{ reward_recipient: string; reward_amount: bigint }>; getStxBalance(stxAddress: string): Promise; getStxBalanceAtBlock(stxAddress: string, blockHeight: number): Promise; diff --git a/src/datastore/memory-store.ts b/src/datastore/memory-store.ts index 1330092f96..b449cdb053 100644 --- a/src/datastore/memory-store.ts +++ b/src/datastore/memory-store.ts @@ -177,6 +177,12 @@ export class MemoryDataStore extends (EventEmitter as { new (): DataStoreEventEm throw new Error('Method not implemented.'); } + getBurnchainRewardsTotal( + burnchainRecipient: string + ): Promise<{ reward_recipient: string; reward_amount: bigint }> { + throw new Error('Method not implemented.'); + } + updateTx(tx: DbTx) { const txStored = { ...tx }; this.txs.set(tx.tx_id, { entry: txStored }); diff --git a/src/datastore/postgres-store.ts b/src/datastore/postgres-store.ts index 4a19e9a43d..98f7db1cc0 100644 --- a/src/datastore/postgres-store.ts +++ b/src/datastore/postgres-store.ts @@ -1132,6 +1132,32 @@ export class PgDataStore extends (EventEmitter as { new (): DataStoreEventEmitte } } + async getBurnchainRewardsTotal( + burnchainRecipient: string + ): Promise<{ reward_recipient: string; reward_amount: bigint }> { + const client = await this.pool.connect(); + try { + await client.query('BEGIN'); + const queryResults = await client.query<{ + amount: string; + }>( + ` + SELECT sum(reward_amount) amount + FROM burnchain_rewards + WHERE canonical = true AND reward_recipient = $1 + `, + [burnchainRecipient] + ); + const resultAmount = BigInt(queryResults.rows[0]?.amount ?? 0); + return { reward_recipient: burnchainRecipient, reward_amount: resultAmount }; + } catch (e) { + await client.query('ROLLBACK'); + throw e; + } finally { + client.release(); + } + } + async updateTx(client: ClientBase, tx: DbTx): Promise { const result = await client.query( ` diff --git a/src/tests/api-tests.ts b/src/tests/api-tests.ts index be45d724a8..b7e65d32ca 100644 --- a/src/tests/api-tests.ts +++ b/src/tests/api-tests.ts @@ -170,6 +170,62 @@ describe('api tests', () => { expect(JSON.parse(rewardResult.text)).toEqual(expectedResp1); }); + test('fetch burnchain total rewards for BTC address', async () => { + const addr = '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ'; + const reward1: DbBurnchainReward = { + canonical: true, + burn_block_hash: '0x1234', + burn_block_height: 200, + burn_amount: 2000n, + reward_recipient: addr, + reward_amount: 1000n, + reward_index: 0, + }; + const reward2: DbBurnchainReward = { + canonical: true, + burn_block_hash: '0x2234', + burn_block_height: 201, + burn_amount: 2000n, + reward_recipient: addr, + reward_amount: 1001n, + reward_index: 0, + }; + const reward3: DbBurnchainReward = { + canonical: true, + burn_block_hash: '0x3234', + burn_block_height: 202, + burn_amount: 2000n, + reward_recipient: addr, + reward_amount: 1002n, + reward_index: 0, + }; + await db.updateBurnchainRewards({ + burnchainBlockHash: reward1.burn_block_hash, + burnchainBlockHeight: reward1.burn_block_height, + rewards: [reward1], + }); + await db.updateBurnchainRewards({ + burnchainBlockHash: reward2.burn_block_hash, + burnchainBlockHeight: reward2.burn_block_height, + rewards: [reward2], + }); + await db.updateBurnchainRewards({ + burnchainBlockHash: reward3.burn_block_hash, + burnchainBlockHeight: reward3.burn_block_height, + rewards: [reward3], + }); + const rewardResult = await supertest(api.server).get( + `/extended/v1/burnchain/rewards/${addr}/total` + ); + expect(rewardResult.status).toBe(200); + expect(rewardResult.type).toBe('application/json'); + const expectedResp1 = { + reward_recipient: '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ', + reward_amount: '3003', + }; + expect(JSON.parse(rewardResult.text)).toEqual(expectedResp1); + }); + test('fetch burnchain rewards for BTC address', async () => { const addr1 = '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ'; const reward1: DbBurnchainReward = {