From ce097581ab69aed91897f2b174804b2a4a4ab7d4 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Tue, 16 Jan 2024 17:49:35 -0600 Subject: [PATCH 1/8] feat: new endpoint --- src/api/routes/v2/schemas.ts | 11 ++++++++ src/api/routes/v2/smart-contracts.ts | 38 ++++++++++++++++++++++++++++ src/datastore/common.ts | 7 +++++ src/datastore/pg-store-v2.ts | 37 +++++++++++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 src/api/routes/v2/smart-contracts.ts diff --git a/src/api/routes/v2/schemas.ts b/src/api/routes/v2/schemas.ts index 5be6a3b7e5..cd880a3c4a 100644 --- a/src/api/routes/v2/schemas.ts +++ b/src/api/routes/v2/schemas.ts @@ -125,3 +125,14 @@ const BlockParamsSchema = Type.Object( ); export type BlockParams = Static; export const CompiledBlockParams = ajv.compile(BlockParamsSchema); + +const SmartContractStatusParamsSchema = Type.Object( + { + contract_id: Type.Array( + Type.RegExp(/^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$/) + ), + }, + { additionalProperties: false } +); +export type SmartContractStatusParams = Static; +export const CompiledSmartContractStatusParams = ajv.compile(SmartContractStatusParamsSchema); diff --git a/src/api/routes/v2/smart-contracts.ts b/src/api/routes/v2/smart-contracts.ts new file mode 100644 index 0000000000..13c3d6dbb0 --- /dev/null +++ b/src/api/routes/v2/smart-contracts.ts @@ -0,0 +1,38 @@ +import * as express from 'express'; +import { PgStore } from '../../../datastore/pg-store'; +import { getETagCacheHandler, setETagCacheHeaders } from '../../controllers/cache-controller'; +import { asyncHandler } from '../../async-handler'; +import { NakamotoBlockListResponse } from 'docs/generated'; +import { + validRequestQuery, + BlockPaginationQueryParams, + CompiledSmartContractStatusParams, + SmartContractStatusParams, +} from './schemas'; +import { parseDbNakamotoBlock } from './helpers'; + +export function createV2SmartContractsRouter(db: PgStore): express.Router { + const router = express.Router(); + const cacheHandler = getETagCacheHandler(db); + + router.get( + '/status', + cacheHandler, + asyncHandler(async (req, res) => { + if (!validRequestQuery(req, res, CompiledSmartContractStatusParams)) return; + const query = req.query as SmartContractStatusParams; + + const results = await db.v2.getSmartContractStatus(query); + // const response: NakamotoBlockListResponse = { + // limit, + // offset, + // total, + // results: results.map(r => parseDbNakamotoBlock(r)), + // }; + setETagCacheHeaders(res); + res.json(response); + }) + ); + + return router; +} diff --git a/src/datastore/common.ts b/src/datastore/common.ts index af08f0b4dc..98f1222d11 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1554,3 +1554,10 @@ export enum IndexesState { Off = 0, On = 1, } + +export interface DbSmartContractStatus { + contract_id: string; + tx_id: string; + status: string; + block_height?: number; +} diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index 53b63f4aca..60d6d81c61 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -6,6 +6,7 @@ import { TransactionLimitParamSchema, BlockParams, BlockPaginationQueryParams, + SmartContractStatusParams, } from '../api/routes/v2/schemas'; import { InvalidRequestError, InvalidRequestErrorType } from '../errors'; import { normalizeHashString } from '../helpers'; @@ -16,6 +17,8 @@ import { DbTx, TxQueryResult, DbBurnBlock, + DbTxTypeId, + DbSmartContractStatus, } from './common'; import { BLOCK_COLUMNS, parseBlockQueryResult, TX_COLUMNS, parseTxQueryResult } from './helpers'; @@ -230,4 +233,38 @@ export class PgStoreV2 extends BasePgStoreModule { if (blockQuery.count > 0) return blockQuery[0]; }); } + + async getSmartContractStatus(args: SmartContractStatusParams): Promise { + return await this.sqlTransaction(async sql => { + const statusArray: DbSmartContractStatus[] = []; + + // Search confirmed txs. + const confirmed = await sql` + SELECT DISTINCT ON (smart_contract_contract_id) smart_contract_contract_id, tx_id, block_height, status + FROM txs + WHERE type_id IN ${sql([DbTxTypeId.SmartContract, DbTxTypeId.VersionedSmartContract])} + AND smart_contract_contract_id IN ${sql(args.contract_id)} + AND canonical = TRUE + AND microblock_canonical = TRUE + ORDER BY smart_contract_contract_id, block_height DESC, microblock_sequence DESC, tx_index DESC + `; + statusArray.push(...confirmed); + if (confirmed.count < args.contract_id.length) { + // Search mempool txs. + const confirmedIds = confirmed.map(c => c.contract_id); + const remainingIds = args.contract_id.filter(c => !confirmedIds.includes(c)); + const mempool = await sql` + SELECT DISTINCT ON (smart_contract_contract_id) smart_contract_contract_id, tx_id, status + FROM mempool_txs + WHERE pruned = FALSE + AND type_id IN ${sql([DbTxTypeId.SmartContract, DbTxTypeId.VersionedSmartContract])} + AND smart_contract_contract_id IN ${sql(remainingIds)} + ORDER BY smart_contract_contract_id + `; + statusArray.push(...mempool); + } + + return statusArray; + }); + } } From 4029cc2224af8c65e5d78f699c3f62b77fdaecc3 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Tue, 16 Jan 2024 17:54:14 -0600 Subject: [PATCH 2/8] docs: openapi --- .../get-smart-contracts-status.example.json | 19 ++++++++++++ .../get-smart-contracts-status.schema.json | 9 ++++++ .../smart-contract-status.example.json | 6 ++++ .../smart-contract-status.schema.json | 29 ++++++++++++++++++ docs/generated.d.ts | 27 +++++++++++++++++ docs/openapi.yaml | 30 +++++++++++++++++++ 6 files changed, 120 insertions(+) create mode 100644 docs/api/smart-contracts/get-smart-contracts-status.example.json create mode 100644 docs/api/smart-contracts/get-smart-contracts-status.schema.json create mode 100644 docs/entities/smart-contracts/smart-contract-status.example.json create mode 100644 docs/entities/smart-contracts/smart-contract-status.schema.json diff --git a/docs/api/smart-contracts/get-smart-contracts-status.example.json b/docs/api/smart-contracts/get-smart-contracts-status.example.json new file mode 100644 index 0000000000..203809be0c --- /dev/null +++ b/docs/api/smart-contracts/get-smart-contracts-status.example.json @@ -0,0 +1,19 @@ +[ + { + "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1", + "status": "success", + "tx_id": "0x8542d28e427256ea3c29dcd8793222891999ceff4ef1bb062e2f21cb6def6884", + "block_height": 111021 + }, + { + "contract_id": "SP1JTCR202ECC6333N7ZXD7MK7E3ZTEEE1MJ73C60.name-registrar", + "status": "success", + "tx_id": "0x6e1114cce8c6f2e9c8130f9acd75d67bb667ae584f882acdd2db6dd74e6cbe5e", + "block_height": 113010 + }, + { + "contract_id": "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1", + "status": "pending", + "tx_id": "0x10bdcf10ffee72994f493ac36760f4e95a76c8471370182fd4705c2153dc173d" + } +] diff --git a/docs/api/smart-contracts/get-smart-contracts-status.schema.json b/docs/api/smart-contracts/get-smart-contracts-status.schema.json new file mode 100644 index 0000000000..51697aab13 --- /dev/null +++ b/docs/api/smart-contracts/get-smart-contracts-status.schema.json @@ -0,0 +1,9 @@ +{ + "description": "GET request that returns the deployment status of multiple smart contracts", + "additionalProperties": false, + "title": "SmartContractsStatusResponse", + "type": "array", + "items": { + "$ref": "../../entities/smart-contracts/smart-contract-status.schema.json" + } +} diff --git a/docs/entities/smart-contracts/smart-contract-status.example.json b/docs/entities/smart-contracts/smart-contract-status.example.json new file mode 100644 index 0000000000..ac5cb17bbb --- /dev/null +++ b/docs/entities/smart-contracts/smart-contract-status.example.json @@ -0,0 +1,6 @@ +{ + "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1", + "status": "success", + "tx_id": "0x8542d28e427256ea3c29dcd8793222891999ceff4ef1bb062e2f21cb6def6884", + "block_height": 111021 +} diff --git a/docs/entities/smart-contracts/smart-contract-status.schema.json b/docs/entities/smart-contracts/smart-contract-status.schema.json new file mode 100644 index 0000000000..116cf8c65b --- /dev/null +++ b/docs/entities/smart-contracts/smart-contract-status.schema.json @@ -0,0 +1,29 @@ +{ + "title": "SmartContractStatus", + "description": "Deployment status of a smart contract", + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "tx_id", + "contract_id" + ], + "properties": { + "status": { + "type": "string", + "description": "Smart contract deployment transaction status" + }, + "tx_id": { + "type": "string", + "description": "Deployment transaction ID" + }, + "contract_id": { + "type": "string", + "description": "Smart contract ID" + }, + "block_height": { + "type": "integer", + "description": "Height of the transaction confirmation block" + } + } +} diff --git a/docs/generated.d.ts b/docs/generated.d.ts index f9952d5cfa..b0317899ac 100644 --- a/docs/generated.d.ts +++ b/docs/generated.d.ts @@ -93,6 +93,7 @@ export type SchemaMergeRootStub = | SearchSuccessResult | TxSearchResult | SearchResult + | SmartContractsStatusResponse | PoolDelegationsResponse | { [k: string]: unknown | undefined; @@ -177,6 +178,7 @@ export type SchemaMergeRootStub = | RosettaSyncStatus | TransactionIdentifier | RosettaTransaction + | SmartContractStatus | PoolDelegation | NonFungibleTokenHistoryEventWithTxId | NonFungibleTokenHistoryEventWithTxMetadata @@ -689,6 +691,10 @@ export type SearchSuccessResult = * complete search result for terms */ export type SearchResult = SearchErrorResult | SearchSuccessResult; +/** + * GET request that returns the deployment status of multiple smart contracts + */ +export type SmartContractsStatusResponse = SmartContractStatus[]; /** * Describes an event from the history of a Non-Fungible Token */ @@ -3059,6 +3065,27 @@ export interface TxSearchResult { metadata?: Transaction; }; } +/** + * Deployment status of a smart contract + */ +export interface SmartContractStatus { + /** + * Smart contract deployment transaction status + */ + status: string; + /** + * Deployment transaction ID + */ + tx_id: string; + /** + * Smart contract ID + */ + contract_id: string; + /** + * Height of the transaction confirmation block + */ + block_height?: number; +} /** * GET request that returns stacking pool member details for a given pool (delegator) principal */ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 4013f02212..b5fd162265 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -834,6 +834,36 @@ paths: example: $ref: ./api/transaction/get-transactions.example.json + /extended/v2/smart-contracts/status: + get: + summary: Get smart contracts status + description: | + Retrieves the deployment status of multiple smart contracts. + tags: + - Smart Contracts + operationId: get_smart_contracts_status + parameters: + - name: contract_id + in: query + description: contract ids to fetch status for + required: true + style: form + explode: true + schema: + type: array + example: "SPQZF23W7SEYBFG5JQ496NMY0G7379SRYEDREMSV.Candy" + items: + type: string + responses: + 200: + description: List of smart contract status + content: + application/json: + schema: + $ref: ./api/blocks/get-blocks.schema.json + example: + $ref: ./api/blocks/get-blocks.example.json + /extended/v1/block: get: summary: Get recent blocks From 06365e1ae55247292bbd4d91e4aff8618755a220 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Tue, 16 Jan 2024 17:54:49 -0600 Subject: [PATCH 3/8] feat: response type --- src/api/routes/v2/smart-contracts.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/api/routes/v2/smart-contracts.ts b/src/api/routes/v2/smart-contracts.ts index 13c3d6dbb0..9d85abd878 100644 --- a/src/api/routes/v2/smart-contracts.ts +++ b/src/api/routes/v2/smart-contracts.ts @@ -2,14 +2,12 @@ import * as express from 'express'; import { PgStore } from '../../../datastore/pg-store'; import { getETagCacheHandler, setETagCacheHeaders } from '../../controllers/cache-controller'; import { asyncHandler } from '../../async-handler'; -import { NakamotoBlockListResponse } from 'docs/generated'; +import { SmartContractsStatusResponse } from 'docs/generated'; import { validRequestQuery, - BlockPaginationQueryParams, CompiledSmartContractStatusParams, SmartContractStatusParams, } from './schemas'; -import { parseDbNakamotoBlock } from './helpers'; export function createV2SmartContractsRouter(db: PgStore): express.Router { const router = express.Router(); @@ -22,13 +20,7 @@ export function createV2SmartContractsRouter(db: PgStore): express.Router { if (!validRequestQuery(req, res, CompiledSmartContractStatusParams)) return; const query = req.query as SmartContractStatusParams; - const results = await db.v2.getSmartContractStatus(query); - // const response: NakamotoBlockListResponse = { - // limit, - // offset, - // total, - // results: results.map(r => parseDbNakamotoBlock(r)), - // }; + const response = (await db.v2.getSmartContractStatus(query)) as SmartContractsStatusResponse; setETagCacheHeaders(res); res.json(response); }) From 076171489045aa27c95f59f95c982f8e3527e9a9 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Wed, 17 Jan 2024 11:06:17 -0600 Subject: [PATCH 4/8] fix: add route --- src/api/init.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/init.ts b/src/api/init.ts index 2376235500..c4515c0ae0 100644 --- a/src/api/init.ts +++ b/src/api/init.ts @@ -53,6 +53,7 @@ import { createV2BlocksRouter } from './routes/v2/blocks'; import { getReqQuery } from './query-helpers'; import { createV2BurnBlocksRouter } from './routes/v2/burn-blocks'; import { createMempoolRouter } from './routes/v2/mempool'; +import { createV2SmartContractsRouter } from './routes/v2/smart-contracts'; export interface ApiServer { expressApp: express.Express; @@ -234,6 +235,7 @@ export async function startApiServer(opts: { const v2 = express.Router(); v2.use('/blocks', createV2BlocksRouter(datastore)); v2.use('/burn-blocks', createV2BurnBlocksRouter(datastore)); + v2.use('/smart-contracts', createV2SmartContractsRouter(datastore)); v2.use('/mempool', createMempoolRouter(datastore)); return v2; })() From 97d556fdd8755304778c0cabf7644151348fa3e8 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Wed, 17 Jan 2024 14:57:52 -0600 Subject: [PATCH 5/8] fix: new schema --- .../get-smart-contracts-status.example.json | 44 ++++++++++++------- .../get-smart-contracts-status.schema.json | 14 ++++-- .../smart-contract-found.schema.json | 15 +++++++ .../smart-contract-not-found.schema.json | 12 +++++ docs/generated.d.ts | 19 ++++++-- docs/openapi.yaml | 4 +- src/api/routes/v2/helpers.ts | 22 +++++++++- src/api/routes/v2/smart-contracts.ts | 6 +-- src/datastore/pg-store-v2.ts | 4 +- 9 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 docs/api/smart-contracts/smart-contract-found.schema.json create mode 100644 docs/api/smart-contracts/smart-contract-not-found.schema.json diff --git a/docs/api/smart-contracts/get-smart-contracts-status.example.json b/docs/api/smart-contracts/get-smart-contracts-status.example.json index 203809be0c..024abd17f0 100644 --- a/docs/api/smart-contracts/get-smart-contracts-status.example.json +++ b/docs/api/smart-contracts/get-smart-contracts-status.example.json @@ -1,19 +1,31 @@ -[ - { - "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1", - "status": "success", - "tx_id": "0x8542d28e427256ea3c29dcd8793222891999ceff4ef1bb062e2f21cb6def6884", - "block_height": 111021 +{ + "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1": { + "found": true, + "status": { + "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1", + "status": "success", + "tx_id": "0x8542d28e427256ea3c29dcd8793222891999ceff4ef1bb062e2f21cb6def6884", + "block_height": 111021 + } }, - { - "contract_id": "SP1JTCR202ECC6333N7ZXD7MK7E3ZTEEE1MJ73C60.name-registrar", - "status": "success", - "tx_id": "0x6e1114cce8c6f2e9c8130f9acd75d67bb667ae584f882acdd2db6dd74e6cbe5e", - "block_height": 113010 + "SP1JTCR202ECC6333N7ZXD7MK7E3ZTEEE1MJ73C60.name-registrar": { + "found": true, + "status": { + "contract_id": "SP1JTCR202ECC6333N7ZXD7MK7E3ZTEEE1MJ73C60.name-registrar", + "status": "success", + "tx_id": "0x6e1114cce8c6f2e9c8130f9acd75d67bb667ae584f882acdd2db6dd74e6cbe5e", + "block_height": 113010 + } }, - { - "contract_id": "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1", - "status": "pending", - "tx_id": "0x10bdcf10ffee72994f493ac36760f4e95a76c8471370182fd4705c2153dc173d" + "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1": { + "found": true, + "status": { + "contract_id": "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1", + "status": "pending", + "tx_id": "0x10bdcf10ffee72994f493ac36760f4e95a76c8471370182fd4705c2153dc173d" + } + }, + "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core": { + "found": false } -] +} diff --git a/docs/api/smart-contracts/get-smart-contracts-status.schema.json b/docs/api/smart-contracts/get-smart-contracts-status.schema.json index 51697aab13..2385c6afa0 100644 --- a/docs/api/smart-contracts/get-smart-contracts-status.schema.json +++ b/docs/api/smart-contracts/get-smart-contracts-status.schema.json @@ -1,9 +1,15 @@ { "description": "GET request that returns the deployment status of multiple smart contracts", - "additionalProperties": false, "title": "SmartContractsStatusResponse", - "type": "array", - "items": { - "$ref": "../../entities/smart-contracts/smart-contract-status.schema.json" + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "./smart-contract-found.schema.json" + }, + { + "$ref": "./smart-contract-not-found.schema.json" + } + ] } } diff --git a/docs/api/smart-contracts/smart-contract-found.schema.json b/docs/api/smart-contracts/smart-contract-found.schema.json new file mode 100644 index 0000000000..38a77d222a --- /dev/null +++ b/docs/api/smart-contracts/smart-contract-found.schema.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "title": "SmartContractFound", + "additionalProperties": false, + "required": ["found", "result"], + "properties": { + "found": { + "type": "boolean", + "enum": [true] + }, + "status": { + "$ref": "../../entities/smart-contracts/smart-contract-status.schema.json" + } + } +} diff --git a/docs/api/smart-contracts/smart-contract-not-found.schema.json b/docs/api/smart-contracts/smart-contract-not-found.schema.json new file mode 100644 index 0000000000..4a4d63514c --- /dev/null +++ b/docs/api/smart-contracts/smart-contract-not-found.schema.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "title": "SmartContractNotFound", + "additionalProperties": false, + "properties": { + "found": { + "type": "boolean", + "enum": [false] + } + }, + "required": ["found"] +} diff --git a/docs/generated.d.ts b/docs/generated.d.ts index b0317899ac..79455feca8 100644 --- a/docs/generated.d.ts +++ b/docs/generated.d.ts @@ -94,6 +94,8 @@ export type SchemaMergeRootStub = | TxSearchResult | SearchResult | SmartContractsStatusResponse + | SmartContractFound + | SmartContractNotFound | PoolDelegationsResponse | { [k: string]: unknown | undefined; @@ -691,10 +693,6 @@ export type SearchSuccessResult = * complete search result for terms */ export type SearchResult = SearchErrorResult | SearchSuccessResult; -/** - * GET request that returns the deployment status of multiple smart contracts - */ -export type SmartContractsStatusResponse = SmartContractStatus[]; /** * Describes an event from the history of a Non-Fungible Token */ @@ -3065,6 +3063,16 @@ export interface TxSearchResult { metadata?: Transaction; }; } +/** + * GET request that returns the deployment status of multiple smart contracts + */ +export interface SmartContractsStatusResponse { + [k: string]: (SmartContractFound | SmartContractNotFound) | undefined; +} +export interface SmartContractFound { + found: true; + status?: SmartContractStatus; +} /** * Deployment status of a smart contract */ @@ -3086,6 +3094,9 @@ export interface SmartContractStatus { */ block_height?: number; } +export interface SmartContractNotFound { + found: false; +} /** * GET request that returns stacking pool member details for a given pool (delegator) principal */ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index b5fd162265..9bb97492c2 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -860,9 +860,9 @@ paths: content: application/json: schema: - $ref: ./api/blocks/get-blocks.schema.json + $ref: ./api/smart-contracts/get-smart-contracts-status.schema.json example: - $ref: ./api/blocks/get-blocks.example.json + $ref: ./api/smart-contracts/get-smart-contracts-status.example.json /extended/v1/block: get: diff --git a/src/api/routes/v2/helpers.ts b/src/api/routes/v2/helpers.ts index 41d0ce2a2e..8015e9e9c5 100644 --- a/src/api/routes/v2/helpers.ts +++ b/src/api/routes/v2/helpers.ts @@ -1,6 +1,7 @@ -import { BurnBlock, NakamotoBlock } from 'docs/generated'; -import { DbBlock, DbBurnBlock } from '../../../datastore/common'; +import { BurnBlock, NakamotoBlock, SmartContractsStatusResponse } from 'docs/generated'; +import { DbBlock, DbBurnBlock, DbSmartContractStatus } from '../../../datastore/common'; import { unixEpochToIso } from '../../../helpers'; +import { SmartContractStatusParams } from './schemas'; export function parseDbNakamotoBlock(block: DbBlock): NakamotoBlock { const apiBlock: NakamotoBlock = { @@ -35,3 +36,20 @@ export function parseDbBurnBlock(block: DbBurnBlock): BurnBlock { }; return burnBlock; } + +export function parseDbSmartContractStatusArray( + params: SmartContractStatusParams, + status: DbSmartContractStatus[] +): SmartContractsStatusResponse { + const ids = new Set(params.contract_id); + const response: SmartContractsStatusResponse = {}; + for (const s of status) { + response[s.contract_id] = { + found: true, + status: s, + }; + ids.delete(s.contract_id); + } + for (const missingId of ids) response[missingId] = { found: false }; + return response; +} diff --git a/src/api/routes/v2/smart-contracts.ts b/src/api/routes/v2/smart-contracts.ts index 9d85abd878..81a57e5285 100644 --- a/src/api/routes/v2/smart-contracts.ts +++ b/src/api/routes/v2/smart-contracts.ts @@ -2,12 +2,12 @@ import * as express from 'express'; import { PgStore } from '../../../datastore/pg-store'; import { getETagCacheHandler, setETagCacheHeaders } from '../../controllers/cache-controller'; import { asyncHandler } from '../../async-handler'; -import { SmartContractsStatusResponse } from 'docs/generated'; import { validRequestQuery, CompiledSmartContractStatusParams, SmartContractStatusParams, } from './schemas'; +import { parseDbSmartContractStatusArray } from './helpers'; export function createV2SmartContractsRouter(db: PgStore): express.Router { const router = express.Router(); @@ -20,9 +20,9 @@ export function createV2SmartContractsRouter(db: PgStore): express.Router { if (!validRequestQuery(req, res, CompiledSmartContractStatusParams)) return; const query = req.query as SmartContractStatusParams; - const response = (await db.v2.getSmartContractStatus(query)) as SmartContractsStatusResponse; + const result = await db.v2.getSmartContractStatus(query); setETagCacheHeaders(res); - res.json(response); + res.json(parseDbSmartContractStatusArray(query, result)); }) ); diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index 60d6d81c61..1a6c5066b2 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -19,6 +19,7 @@ import { DbBurnBlock, DbTxTypeId, DbSmartContractStatus, + DbTxStatus, } from './common'; import { BLOCK_COLUMNS, parseBlockQueryResult, TX_COLUMNS, parseTxQueryResult } from './helpers'; @@ -246,6 +247,7 @@ export class PgStoreV2 extends BasePgStoreModule { AND smart_contract_contract_id IN ${sql(args.contract_id)} AND canonical = TRUE AND microblock_canonical = TRUE + AND status = ${DbTxStatus.Success} ORDER BY smart_contract_contract_id, block_height DESC, microblock_sequence DESC, tx_index DESC `; statusArray.push(...confirmed); @@ -259,7 +261,7 @@ export class PgStoreV2 extends BasePgStoreModule { WHERE pruned = FALSE AND type_id IN ${sql([DbTxTypeId.SmartContract, DbTxTypeId.VersionedSmartContract])} AND smart_contract_contract_id IN ${sql(remainingIds)} - ORDER BY smart_contract_contract_id + ORDER BY smart_contract_contract_id, nonce `; statusArray.push(...mempool); } From d68981ed9020a5000d90d2a031d84de017c6aefb Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Wed, 17 Jan 2024 17:01:57 -0600 Subject: [PATCH 6/8] test: add --- src/api/controllers/db-controller.ts | 4 +- src/api/routes/v2/helpers.ts | 16 +++- src/api/routes/v2/schemas.ts | 7 +- src/datastore/common.ts | 4 +- src/datastore/pg-store-v2.ts | 9 ++- src/tests/smart-contract-tests.ts | 109 +++++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 14 deletions(-) diff --git a/src/api/controllers/db-controller.ts b/src/api/controllers/db-controller.ts index fe9399b09d..b2132286f7 100644 --- a/src/api/controllers/db-controller.ts +++ b/src/api/controllers/db-controller.ts @@ -149,7 +149,9 @@ export function getTxTypeId(typeString: Transaction['tx_type']): DbTxTypeId[] { } } -function getTxStatusString(txStatus: DbTxStatus): TransactionStatus | MempoolTransactionStatus { +export function getTxStatusString( + txStatus: DbTxStatus +): TransactionStatus | MempoolTransactionStatus { switch (txStatus) { case DbTxStatus.Pending: return 'pending'; diff --git a/src/api/routes/v2/helpers.ts b/src/api/routes/v2/helpers.ts index 8015e9e9c5..13b010607d 100644 --- a/src/api/routes/v2/helpers.ts +++ b/src/api/routes/v2/helpers.ts @@ -2,6 +2,7 @@ import { BurnBlock, NakamotoBlock, SmartContractsStatusResponse } from 'docs/gen import { DbBlock, DbBurnBlock, DbSmartContractStatus } from '../../../datastore/common'; import { unixEpochToIso } from '../../../helpers'; import { SmartContractStatusParams } from './schemas'; +import { getTxStatusString } from '../../../api/controllers/db-controller'; export function parseDbNakamotoBlock(block: DbBlock): NakamotoBlock { const apiBlock: NakamotoBlock = { @@ -41,14 +42,21 @@ export function parseDbSmartContractStatusArray( params: SmartContractStatusParams, status: DbSmartContractStatus[] ): SmartContractsStatusResponse { - const ids = new Set(params.contract_id); + const ids = new Set( + Array.isArray(params.contract_id) ? params.contract_id : [params.contract_id] + ); const response: SmartContractsStatusResponse = {}; for (const s of status) { - response[s.contract_id] = { + ids.delete(s.smart_contract_contract_id); + response[s.smart_contract_contract_id] = { found: true, - status: s, + status: { + contract_id: s.smart_contract_contract_id, + block_height: s.block_height, + status: getTxStatusString(s.status), + tx_id: s.tx_id, + }, }; - ids.delete(s.contract_id); } for (const missingId of ids) response[missingId] = { found: false }; return response; diff --git a/src/api/routes/v2/schemas.ts b/src/api/routes/v2/schemas.ts index cd880a3c4a..689c9bf7e7 100644 --- a/src/api/routes/v2/schemas.ts +++ b/src/api/routes/v2/schemas.ts @@ -126,11 +126,12 @@ const BlockParamsSchema = Type.Object( export type BlockParams = Static; export const CompiledBlockParams = ajv.compile(BlockParamsSchema); +const SmartContractPrincipal = Type.RegExp( + /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$/ +); const SmartContractStatusParamsSchema = Type.Object( { - contract_id: Type.Array( - Type.RegExp(/^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{28,41}\.[a-zA-Z]([a-zA-Z0-9]|[-_]){0,39}$/) - ), + contract_id: Type.Union([Type.Array(SmartContractPrincipal), SmartContractPrincipal]), }, { additionalProperties: false } ); diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 98f1222d11..2f3459b90b 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -1556,8 +1556,8 @@ export enum IndexesState { } export interface DbSmartContractStatus { - contract_id: string; + smart_contract_contract_id: string; tx_id: string; - status: string; + status: DbTxStatus; block_height?: number; } diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index 1a6c5066b2..5b730b4fc8 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -238,23 +238,24 @@ export class PgStoreV2 extends BasePgStoreModule { async getSmartContractStatus(args: SmartContractStatusParams): Promise { return await this.sqlTransaction(async sql => { const statusArray: DbSmartContractStatus[] = []; + const contractArray = Array.isArray(args.contract_id) ? args.contract_id : [args.contract_id]; // Search confirmed txs. const confirmed = await sql` SELECT DISTINCT ON (smart_contract_contract_id) smart_contract_contract_id, tx_id, block_height, status FROM txs WHERE type_id IN ${sql([DbTxTypeId.SmartContract, DbTxTypeId.VersionedSmartContract])} - AND smart_contract_contract_id IN ${sql(args.contract_id)} + AND smart_contract_contract_id IN ${sql(contractArray)} AND canonical = TRUE AND microblock_canonical = TRUE AND status = ${DbTxStatus.Success} ORDER BY smart_contract_contract_id, block_height DESC, microblock_sequence DESC, tx_index DESC `; statusArray.push(...confirmed); - if (confirmed.count < args.contract_id.length) { + if (confirmed.count < contractArray.length) { // Search mempool txs. - const confirmedIds = confirmed.map(c => c.contract_id); - const remainingIds = args.contract_id.filter(c => !confirmedIds.includes(c)); + const confirmedIds = confirmed.map(c => c.smart_contract_contract_id); + const remainingIds = contractArray.filter(c => !confirmedIds.includes(c)); const mempool = await sql` SELECT DISTINCT ON (smart_contract_contract_id) smart_contract_contract_id, tx_id, status FROM mempool_txs diff --git a/src/tests/smart-contract-tests.ts b/src/tests/smart-contract-tests.ts index d3b32161af..d32b41f3ed 100644 --- a/src/tests/smart-contract-tests.ts +++ b/src/tests/smart-contract-tests.ts @@ -13,6 +13,7 @@ import { I32_MAX } from '../helpers'; import { PgWriteStore } from '../datastore/pg-write-store'; import { bufferToHex, PgSqlClient, waiter } from '@hirosystems/api-toolkit'; import { migrate } from '../test-utils/test-helpers'; +import { TestBlockBuilder, testMempoolTx } from '../test-utils/test-builders'; describe('smart contract tests', () => { let db: PgWriteStore; @@ -1715,4 +1716,112 @@ describe('smart contract tests', () => { ); expect(query.status).toBe(431); }); + + test('status for multiple contracts', async () => { + const block1 = new TestBlockBuilder({ block_height: 1, index_block_hash: '0x01' }) + .addTx({ + tx_id: '0x1234', + type_id: DbTxTypeId.SmartContract, + smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1', + smart_contract_source_code: '(some-contract-src)', + }) + .addTxSmartContract({ + tx_id: '0x1234', + block_height: 1, + contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1', + contract_source: '(some-contract-src)', + }) + .build(); + await db.update(block1); + const block2 = new TestBlockBuilder({ + block_height: 2, + index_block_hash: '0x02', + parent_index_block_hash: '0x01', + }) + .addTx({ + tx_id: '0x1222', + type_id: DbTxTypeId.SmartContract, + smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2', + smart_contract_source_code: '(some-contract-src)', + }) + .addTxSmartContract({ + tx_id: '0x1222', + block_height: 2, + contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2', + contract_source: '(some-contract-src)', + }) + .build(); + await db.update(block2); + + // Contracts are found + let query = await supertest(api.server).get( + `/extended/v2/smart-contracts/status?contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1&contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2` + ); + expect(query.status).toBe(200); + let json = JSON.parse(query.text); + expect(json).toStrictEqual({ + 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1': { + found: true, + status: { + block_height: 1, + contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1', + status: 'success', + tx_id: '0x1234', + }, + }, + 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2': { + found: true, + status: { + block_height: 2, + contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2', + status: 'success', + tx_id: '0x1222', + }, + }, + }); + + // Assume two contract attempts on the mempool + const mempoolTx1 = testMempoolTx({ + tx_id: '0x111111', + type_id: DbTxTypeId.SmartContract, + nonce: 5, + smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3', + }); + const mempoolTx2 = testMempoolTx({ + tx_id: '0x111122', + type_id: DbTxTypeId.SmartContract, + nonce: 6, + smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3', + }); + await db.updateMempoolTxs({ mempoolTxs: [mempoolTx1, mempoolTx2] }); + query = await supertest(api.server).get( + `/extended/v2/smart-contracts/status?contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3` + ); + expect(query.status).toBe(200); + json = JSON.parse(query.text); + // Only the first one is reported. + expect(json).toStrictEqual({ + 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3': { + found: true, + status: { + contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3', + status: 'pending', + tx_id: '0x111111', + }, + }, + }); + + // Check found = false + query = await supertest(api.server).get( + `/extended/v2/smart-contracts/status?contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.abcde` + ); + expect(query.status).toBe(200); + json = JSON.parse(query.text); + // Only the first one is reported. + expect(json).toStrictEqual({ + 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.abcde': { + found: false, + }, + }); + }); }); From a479cdfe25812dc1e3d7edba6a7282a461614f81 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Thu, 18 Jan 2024 10:43:19 -0600 Subject: [PATCH 7/8] fix: rename result --- docs/api/smart-contracts/smart-contract-found.schema.json | 2 +- docs/generated.d.ts | 2 +- src/api/routes/v2/helpers.ts | 2 +- src/datastore/pg-store-v2.ts | 3 +-- src/tests/smart-contract-tests.ts | 6 +++--- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/api/smart-contracts/smart-contract-found.schema.json b/docs/api/smart-contracts/smart-contract-found.schema.json index 38a77d222a..6d781711df 100644 --- a/docs/api/smart-contracts/smart-contract-found.schema.json +++ b/docs/api/smart-contracts/smart-contract-found.schema.json @@ -8,7 +8,7 @@ "type": "boolean", "enum": [true] }, - "status": { + "result": { "$ref": "../../entities/smart-contracts/smart-contract-status.schema.json" } } diff --git a/docs/generated.d.ts b/docs/generated.d.ts index 79455feca8..095800439f 100644 --- a/docs/generated.d.ts +++ b/docs/generated.d.ts @@ -3071,7 +3071,7 @@ export interface SmartContractsStatusResponse { } export interface SmartContractFound { found: true; - status?: SmartContractStatus; + result: SmartContractStatus; } /** * Deployment status of a smart contract diff --git a/src/api/routes/v2/helpers.ts b/src/api/routes/v2/helpers.ts index 13b010607d..23ff0d18d3 100644 --- a/src/api/routes/v2/helpers.ts +++ b/src/api/routes/v2/helpers.ts @@ -50,7 +50,7 @@ export function parseDbSmartContractStatusArray( ids.delete(s.smart_contract_contract_id); response[s.smart_contract_contract_id] = { found: true, - status: { + result: { contract_id: s.smart_contract_contract_id, block_height: s.block_height, status: getTxStatusString(s.status), diff --git a/src/datastore/pg-store-v2.ts b/src/datastore/pg-store-v2.ts index 5b730b4fc8..1e7554594b 100644 --- a/src/datastore/pg-store-v2.ts +++ b/src/datastore/pg-store-v2.ts @@ -248,8 +248,7 @@ export class PgStoreV2 extends BasePgStoreModule { AND smart_contract_contract_id IN ${sql(contractArray)} AND canonical = TRUE AND microblock_canonical = TRUE - AND status = ${DbTxStatus.Success} - ORDER BY smart_contract_contract_id, block_height DESC, microblock_sequence DESC, tx_index DESC + ORDER BY smart_contract_contract_id, block_height DESC, microblock_sequence DESC, tx_index DESC, status `; statusArray.push(...confirmed); if (confirmed.count < contractArray.length) { diff --git a/src/tests/smart-contract-tests.ts b/src/tests/smart-contract-tests.ts index d32b41f3ed..2cab93e7ec 100644 --- a/src/tests/smart-contract-tests.ts +++ b/src/tests/smart-contract-tests.ts @@ -1762,7 +1762,7 @@ describe('smart contract tests', () => { expect(json).toStrictEqual({ 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1': { found: true, - status: { + result: { block_height: 1, contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1', status: 'success', @@ -1771,7 +1771,7 @@ describe('smart contract tests', () => { }, 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2': { found: true, - status: { + result: { block_height: 2, contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2', status: 'success', @@ -1803,7 +1803,7 @@ describe('smart contract tests', () => { expect(json).toStrictEqual({ 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3': { found: true, - status: { + result: { contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-3', status: 'pending', tx_id: '0x111111', From 12275c07c88522da751c2f2b86ea697d7296293e Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Thu, 18 Jan 2024 10:44:18 -0600 Subject: [PATCH 8/8] docs: example result --- .../smart-contracts/get-smart-contracts-status.example.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/smart-contracts/get-smart-contracts-status.example.json b/docs/api/smart-contracts/get-smart-contracts-status.example.json index 024abd17f0..15f66ee27c 100644 --- a/docs/api/smart-contracts/get-smart-contracts-status.example.json +++ b/docs/api/smart-contracts/get-smart-contracts-status.example.json @@ -1,7 +1,7 @@ { "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1": { "found": true, - "status": { + "result": { "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.swap-helper-bridged-v1-1", "status": "success", "tx_id": "0x8542d28e427256ea3c29dcd8793222891999ceff4ef1bb062e2f21cb6def6884", @@ -10,7 +10,7 @@ }, "SP1JTCR202ECC6333N7ZXD7MK7E3ZTEEE1MJ73C60.name-registrar": { "found": true, - "status": { + "result": { "contract_id": "SP1JTCR202ECC6333N7ZXD7MK7E3ZTEEE1MJ73C60.name-registrar", "status": "success", "tx_id": "0x6e1114cce8c6f2e9c8130f9acd75d67bb667ae584f882acdd2db6dd74e6cbe5e", @@ -19,7 +19,7 @@ }, "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1": { "found": true, - "status": { + "result": { "contract_id": "SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1", "status": "pending", "tx_id": "0x10bdcf10ffee72994f493ac36760f4e95a76c8471370182fd4705c2153dc173d"