diff --git a/scripts/patch-missing-contract-abis.ts b/scripts/patch-missing-contract-abis.ts new file mode 100644 index 000000000..258c71b9c --- /dev/null +++ b/scripts/patch-missing-contract-abis.ts @@ -0,0 +1,112 @@ +import { connectPostgres, PgSqlClient } from '@hirosystems/api-toolkit'; +import { StacksCoreRpcClient } from '../src/core-rpc/client'; +import { getConnectionArgs, getConnectionConfig } from '../src/datastore/connection'; +import { ClarityAbi } from '../src/event-stream/contract-abi'; +import { loadDotEnv } from '../src/helpers'; +import { logger } from '../src/logger'; + +const BATCH_SIZE = 64; +const LAST_BLOCK_HEIGHT = parseInt(process.env.LAST_BLOCK_HEIGHT ?? '-1'); + +// 1) Environment + DB Setup +loadDotEnv(); +const sql: PgSqlClient = await connectPostgres({ + usageName: 'patch-missing-contract-abis', + connectionArgs: getConnectionArgs(), + connectionConfig: getConnectionConfig(), +}); + +try { + logger.info('Starting script to patch missing contract ABIs...'); + + // 2) Initialize script variables and RPC client + let lastBlockHeight = LAST_BLOCK_HEIGHT; // Initial value for the first query + + let totalConsideredCount = 0; + let totalPatchedCount = 0; + + const rpc = new StacksCoreRpcClient(); // Default to RPC host from ENV + + // 3) Main processing loop: Fetch and patch contracts in batches + while (true) { + // 3.1) Find contracts whose ABI is still missing (paginated) + const missing = await sql<{ contract_id: string; block_height: number }[]>` + SELECT contract_id, block_height + FROM smart_contracts + WHERE (abi::text = '"null"') + AND canonical = TRUE + AND block_height > ${lastBlockHeight} + ORDER BY block_height ASC + LIMIT ${BATCH_SIZE} + `; + + if (missing.length === 0) { + if (totalConsideredCount === 0) { + logger.info(' - No contracts with missing ABI found.'); + } else { + logger.info(` - Patched ${totalPatchedCount}/${totalConsideredCount} contracts.`); + } + break; // Exit the while loop + } + + logger.info(`- Found batch of ${missing.length} contracts with missing ABIs.`); + + // 3.2) Process each contract in the current batch + for (const contract of missing) { + totalConsideredCount++; + const { contract_id, block_height } = contract; + const [address, name] = contract_id.split('.'); + if (!address || !name) { + logger.warn(` - Skipping invalid contract id: ${contract_id}`); + continue; + } + + try { + // 3.3) Fetch ABI from the connected Stacks node + const abi = await rpc.fetchJson(`v2/contracts/interface/${address}/${name}`); + + if (!abi || typeof abi !== 'object' || Object.keys(abi).length === 0) { + logger.warn(` - Skipping ${contract_id}. Fetched empty or invalid ABI.`); + continue; + } + + // 3.4) Update row for this contract still missing an ABI + const rows = await sql` + UPDATE smart_contracts + SET abi = ${abi} + WHERE contract_id = ${contract_id} + AND (abi::text = '"null"') + AND canonical = TRUE + `; + if (rows.count === 0) { + logger.warn(` - Failed to patch ${contract_id}. No rows updated.`); + continue; + } + logger.info(` - Patched ABI for ${contract_id}`); + totalPatchedCount++; + } catch (err: any) { + logger.error(err, ` - Failed to patch ${contract_id}`); + } + + // Keep track of the latest block_height we've processed + if (block_height > lastBlockHeight) { + lastBlockHeight = block_height; + logger.info(` - Processed up to block ${lastBlockHeight}`); + } + } + + // 3.5) Check if it was the last batch + if (missing.length < BATCH_SIZE) { + logger.info(` - Patched ${totalPatchedCount}/${totalConsideredCount} contracts.`); + break; // Last batch was smaller than batchSize, so no more items. + } + } +} catch (err: any) { + logger.error(err, 'An unexpected error occurred'); + throw err; +} finally { + // 4) Close DB connection + logger.info('Closing database connection...'); + await sql.end({ timeout: 5 }); + logger.info('Done.'); +} diff --git a/src/event-stream/core-node-message.ts b/src/event-stream/core-node-message.ts index 9c8b276de..065d1dd87 100644 --- a/src/event-stream/core-node-message.ts +++ b/src/event-stream/core-node-message.ts @@ -240,7 +240,9 @@ export interface CoreNodeTxMessage { raw_result: string; txid: string; tx_index: number; - contract_abi: ClarityAbi | null; + contract_interface: ClarityAbi | null; + /** @deprecated Use `contract_interface` instead. The node renamed `contract_abi` to `contract_interface`. */ + contract_abi?: ClarityAbi | null; execution_cost: CoreNodeExecutionCostMessage; microblock_sequence: number | null; microblock_hash: string | null; diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index b3ab6dbff..eb0073451 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -287,7 +287,7 @@ function parseDataStoreTxEventData( block_height: blockData.block_height, clarity_version: clarityVersion, source_code: tx.parsed_tx.payload.code_body, - abi: JSON.stringify(tx.core_tx.contract_abi), + abi: JSON.stringify(tx.core_tx.contract_interface ?? tx.core_tx.contract_abi), canonical: true, }); break; diff --git a/tests/api/address.test.ts b/tests/api/address.test.ts index 216e50595..6a93e827a 100644 --- a/tests/api/address.test.ts +++ b/tests/api/address.test.ts @@ -2514,7 +2514,7 @@ describe('address tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: '0x' + txBuilder.txid(), tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, diff --git a/tests/api/tx.test.ts b/tests/api/tx.test.ts index 9913bc49d..e8f2372a4 100644 --- a/tests/api/tx.test.ts +++ b/tests/api/tx.test.ts @@ -384,7 +384,7 @@ describe('tx tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: tx.tx_id, tx_index: 2, - contract_abi: abiSample, + contract_interface: abiSample, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -543,7 +543,7 @@ describe('tx tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: tx.tx_id, tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -692,7 +692,7 @@ describe('tx tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: tx.tx_id, tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -852,7 +852,7 @@ describe('tx tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: '0x' + txBuilder.txid(), tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -1076,7 +1076,7 @@ describe('tx tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: '0x' + txBuilder.txid(), tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -1480,7 +1480,7 @@ describe('tx tests', () => { raw_result: '0x0100000000000000000000000000000001', // u1 txid: '0x' + txBuilder.txid(), tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -1713,7 +1713,7 @@ describe('tx tests', () => { status: 'abort_by_response', txid: '0x' + txBuilder.txid(), tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null, @@ -1869,7 +1869,7 @@ describe('tx tests', () => { status: 'abort_by_post_condition', txid: '0x' + txBuilder.txid(), tx_index: 2, - contract_abi: null, + contract_interface: null, microblock_hash: null, microblock_parent_hash: null, microblock_sequence: null,