Skip to content

Commit b9ca4bb

Browse files
authored
feat: add strict ft/nft metadata processing mode for better error handling (#1165)
* refactor: create root dir for token-metadata, separate into files * feat: start creating retryable errors * feat: catch all errors in one place * fix: current tests * feat: add strict flag and max retry env var * style: comment nits * style: remove unused exports * fix: existing unit tests * test: strict mode * test: block metadata retry * test: metadata uri timeout and max size * chore: move retry_count column to separate migration * fix: make fetch timeout dynamic * feat: retry metadata uri fetch 5 times before failing
1 parent 968a671 commit b9ca4bb

19 files changed

+1423
-941
lines changed

.env

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,21 @@ MAINNET_SEND_MANY_CONTRACT_ID=SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-man
105105
# STACKS_API_ENABLE_FT_METADATA=1
106106
# STACKS_API_ENABLE_NFT_METADATA=1
107107

108+
# If token metadata processing is enabled, this variable determines how the API reacts to metadata processing failures.
109+
# When strict mode is enabled, any failures caused by recoverable errors will be retried indefinitely. Otherwise,
110+
# the API will give up after `STACKS_API_TOKEN_METADATA_MAX_RETRIES` is reached for that smart contract.
111+
# STACKS_API_TOKEN_METADATA_STRICT_MODE=1
112+
113+
# Maximum number of times we'll try processing FT/NFT metadata for a specific smart contract if we've failed
114+
# because of a recoverable error.
115+
# Only used if `STACKS_API_TOKEN_METADATA_STRICT_MODE` is disabled.
116+
# STACKS_API_TOKEN_METADATA_MAX_RETRIES=5
117+
108118
# Controls the token metadata error handling mode. The possible values are:
109119
# * `warning`: If required metadata is not found, the API will issue a warning and not display data for that token.
110120
# * `error`: If required metadata is not found, the API will throw an error.
111121
# If not specified or any other value is provided, the mode will be set to `warning`.
112-
# STACKS_API_TOKEN_METADATA_ERROR_MODE=
122+
# STACKS_API_TOKEN_METADATA_ERROR_MODE=warning
113123

114124
# Configure a script to handle image URLs during token metadata processing.
115125
# This example script uses the `imgix.net` service to create CDN URLs.

src/api/routes/rosetta/account.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { RosettaErrors, RosettaConstants, RosettaErrorsTypes } from '../../roset
1616
import { rosettaValidateRequest, ValidSchema, makeRosettaError } from '../../rosetta-validate';
1717
import { ChainID } from '@stacks/transactions';
1818
import { getValidatedFtMetadata } from '../../../rosetta-helpers';
19-
import { isFtMetadataEnabled } from '../../../event-stream/tokens-contract-handler';
19+
import { isFtMetadataEnabled } from '../../../token-metadata/helpers';
2020

2121
export function createRosettaAccountRouter(db: DataStore, chainId: ChainID): express.Router {
2222
const router = express.Router();

src/api/routes/tokens/tokens.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ import {
1414
NonFungibleTokensMetadataList,
1515
} from '@stacks/stacks-blockchain-api-types';
1616
import { parseLimitQuery, parsePagingQueryInput } from './../../pagination';
17-
import {
18-
isFtMetadataEnabled,
19-
isNftMetadataEnabled,
20-
} from '../../../event-stream/tokens-contract-handler';
17+
import { isFtMetadataEnabled, isNftMetadataEnabled } from '../../../token-metadata/helpers';
2118
import { bufferToHexPrefixString, has0xPrefix, isValidPrincipal } from '../../../helpers';
2219
import { booleanValueForParam, isUnanchoredRequest } from '../../../api/query-helpers';
2320
import { decodeClarityValueToRepr } from 'stacks-encoding-native-js';

src/core-rpc/client.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import fetch, { RequestInit } from 'node-fetch';
33
import { parsePort, stopwatch, logError, timeout } from '../helpers';
44
import { CoreNodeFeeResponse } from '@stacks/stacks-blockchain-api-types';
5+
import { ClarityValue, cvToHex } from '@stacks/transactions';
56

67
interface CoreRpcAccountInfo {
78
/** Hex-prefixed uint128. */
@@ -49,6 +50,20 @@ export interface Neighbor {
4950
authenticated: boolean;
5051
}
5152

53+
interface ReadOnlyContractCallSuccessResponse {
54+
okay: true;
55+
result: string;
56+
}
57+
58+
interface ReadOnlyContractCallFailResponse {
59+
okay: false;
60+
cause: string;
61+
}
62+
63+
export type ReadOnlyContractCallResponse =
64+
| ReadOnlyContractCallSuccessResponse
65+
| ReadOnlyContractCallFailResponse;
66+
5267
interface CoreRpcNeighbors {
5368
sample: Neighbor[];
5469
inbound: Neighbor[];
@@ -208,6 +223,27 @@ export class StacksCoreRpcClient {
208223
};
209224
}
210225

226+
async sendReadOnlyContractCall(
227+
contractAddress: string,
228+
contractName: string,
229+
functionName: string,
230+
senderAddress: string,
231+
functionArgs: ClarityValue[]
232+
): Promise<ReadOnlyContractCallResponse> {
233+
const body = {
234+
sender: senderAddress,
235+
arguments: functionArgs.map(arg => cvToHex(arg)),
236+
};
237+
return await this.fetchJson<ReadOnlyContractCallResponse>(
238+
`v2/contracts/call-read/${contractAddress}/${contractName}/${functionName}`,
239+
{
240+
method: 'POST',
241+
headers: { 'Content-Type': 'application/json' },
242+
body: JSON.stringify(body),
243+
}
244+
);
245+
}
246+
211247
async getNeighbors(): Promise<CoreRpcNeighbors> {
212248
const result = await this.fetchJson<CoreRpcNeighbors>(`v2/neighbors`, {
213249
method: 'GET',

src/datastore/common.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ export interface DbTokenMetadataQueueEntry {
604604
contractAbi: ClarityAbi;
605605
blockHeight: number;
606606
processed: boolean;
607+
retry_count: number;
607608
}
608609

609610
export interface DbChainTip {
@@ -983,8 +984,8 @@ export interface DataStore extends DataStoreEventEmitter {
983984
getFtMetadata(contractId: string): Promise<FoundOrNot<DbFungibleTokenMetadata>>;
984985
getNftMetadata(contractId: string): Promise<FoundOrNot<DbNonFungibleTokenMetadata>>;
985986

986-
updateNFtMetadata(nftMetadata: DbNonFungibleTokenMetadata, dbQueueId: number): Promise<number>;
987-
updateFtMetadata(ftMetadata: DbFungibleTokenMetadata, dbQueueId: number): Promise<number>;
987+
updateNFtMetadata(nftMetadata: DbNonFungibleTokenMetadata): Promise<number>;
988+
updateFtMetadata(ftMetadata: DbFungibleTokenMetadata): Promise<number>;
988989

989990
getFtMetadataList(args: {
990991
limit: number;
@@ -1001,6 +1002,19 @@ export interface DataStore extends DataStoreEventEmitter {
10011002
*/
10021003
getTokenMetadataQueueEntry(queueId: number): Promise<FoundOrNot<DbTokenMetadataQueueEntry>>;
10031004

1005+
/**
1006+
* Marks a token metadata queue entry as processed.
1007+
* @param queueId - queue entry id
1008+
*/
1009+
updateProcessedTokenMetadataQueueEntry(queueId: number): Promise<void>;
1010+
1011+
/**
1012+
* Increases the retry count for a specific token metadata queue entry.
1013+
* @param queueId - queue entry id
1014+
* @returns new retry count
1015+
*/
1016+
increaseTokenMetadataQueueEntryRetryCount(queueId: number): Promise<number>;
1017+
10041018
getTokenMetadataQueue(
10051019
limit: number,
10061020
excludingEntries: number[]

src/datastore/postgres-store.ts

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ import {
105105
Block,
106106
} from '@stacks/stacks-blockchain-api-types';
107107
import { getTxTypeId } from '../api/controllers/db-controller';
108-
import { isProcessableTokenMetadata } from '../event-stream/tokens-contract-handler';
108+
import { isProcessableTokenMetadata } from '../token-metadata/helpers';
109109
import { ChainID, ClarityAbi, hexToCV, TupleCV } from '@stacks/transactions';
110110
import {
111111
PgAddressNotificationPayload,
@@ -714,6 +714,7 @@ interface DbTokenMetadataQueueEntryQuery {
714714
contract_abi: string;
715715
block_height: number;
716716
processed: boolean;
717+
retry_count: number;
717718
}
718719

719720
interface StxEventQueryResult {
@@ -1503,6 +1504,7 @@ export class PgDataStore
15031504
contractAbi: contractAbi,
15041505
blockHeight: entry.tx.block_height,
15051506
processed: false,
1507+
retry_count: 0,
15061508
};
15071509
return queueEntry;
15081510
})
@@ -5128,6 +5130,7 @@ export class PgDataStore
51285130
contractAbi: JSON.parse(row.contract_abi),
51295131
blockHeight: row.block_height,
51305132
processed: row.processed,
5133+
retry_count: row.retry_count,
51315134
};
51325135
return { found: true, result: entry };
51335136
}
@@ -5158,6 +5161,7 @@ export class PgDataStore
51585161
contractAbi: JSON.parse(row.contract_abi),
51595162
blockHeight: row.block_height,
51605163
processed: row.processed,
5164+
retry_count: row.retry_count,
51615165
};
51625166
return entry;
51635167
});
@@ -5239,7 +5243,7 @@ export class PgDataStore
52395243
});
52405244
}
52415245

5242-
async getSmartContract(contractId: string) {
5246+
async getSmartContract(contractId: string): Promise<FoundOrNot<DbSmartContract>> {
52435247
return this.query(async client => {
52445248
const result = await client.query<{
52455249
tx_id: Buffer;
@@ -7616,7 +7620,7 @@ export class PgDataStore
76167620
});
76177621
}
76187622

7619-
async updateFtMetadata(ftMetadata: DbFungibleTokenMetadata, dbQueueId: number): Promise<number> {
7623+
async updateFtMetadata(ftMetadata: DbFungibleTokenMetadata): Promise<number> {
76207624
const {
76217625
token_uri,
76227626
name,
@@ -7629,8 +7633,7 @@ export class PgDataStore
76297633
tx_id,
76307634
sender_address,
76317635
} = ftMetadata;
7632-
7633-
const rowCount = await this.queryTx(async client => {
7636+
const rowCount = await this.query(async client => {
76347637
const result = await client.query(
76357638
`
76367639
INSERT INTO ft_metadata(
@@ -7650,24 +7653,13 @@ export class PgDataStore
76507653
sender_address,
76517654
]
76527655
);
7653-
await client.query(
7654-
`
7655-
UPDATE token_metadata_queue
7656-
SET processed = true
7657-
WHERE queue_id = $1
7658-
`,
7659-
[dbQueueId]
7660-
);
76617656
return result.rowCount;
76627657
});
76637658
await this.notifier?.sendTokens({ contractID: contract_id });
76647659
return rowCount;
76657660
}
76667661

7667-
async updateNFtMetadata(
7668-
nftMetadata: DbNonFungibleTokenMetadata,
7669-
dbQueueId: number
7670-
): Promise<number> {
7662+
async updateNFtMetadata(nftMetadata: DbNonFungibleTokenMetadata): Promise<number> {
76717663
const {
76727664
token_uri,
76737665
name,
@@ -7678,7 +7670,7 @@ export class PgDataStore
76787670
tx_id,
76797671
sender_address,
76807672
} = nftMetadata;
7681-
const rowCount = await this.queryTx(async client => {
7673+
const rowCount = await this.query(async client => {
76827674
const result = await client.query(
76837675
`
76847676
INSERT INTO nft_metadata(
@@ -7696,18 +7688,38 @@ export class PgDataStore
76967688
sender_address,
76977689
]
76987690
);
7691+
return result.rowCount;
7692+
});
7693+
await this.notifier?.sendTokens({ contractID: contract_id });
7694+
return rowCount;
7695+
}
7696+
7697+
async updateProcessedTokenMetadataQueueEntry(queueId: number): Promise<void> {
7698+
await this.query(async client => {
76997699
await client.query(
77007700
`
77017701
UPDATE token_metadata_queue
77027702
SET processed = true
77037703
WHERE queue_id = $1
77047704
`,
7705-
[dbQueueId]
7705+
[queueId]
77067706
);
7707-
return result.rowCount;
77087707
});
7709-
await this.notifier?.sendTokens({ contractID: contract_id });
7710-
return rowCount;
7708+
}
7709+
7710+
async increaseTokenMetadataQueueEntryRetryCount(queueId: number): Promise<number> {
7711+
return await this.query(async client => {
7712+
const result = await client.query<{ retry_count: number }>(
7713+
`
7714+
UPDATE token_metadata_queue
7715+
SET retry_count = retry_count + 1
7716+
WHERE queue_id = $1
7717+
RETURNING retry_count
7718+
`,
7719+
[queueId]
7720+
);
7721+
return result.rows[0].retry_count;
7722+
});
77117723
}
77127724

77137725
getFtMetadataList({

0 commit comments

Comments
 (0)