Skip to content

Commit 2965061

Browse files
authored
Merge pull request #110 from hirosystems/beta-next
release beta.8
2 parents c4a6074 + 156e9e2 commit 2965061

File tree

14 files changed

+128
-39
lines changed

14 files changed

+128
-39
lines changed

package-lock.json

Lines changed: 7 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"evt": "^1.11.2",
5757
"fastify": "^4.9.2",
5858
"fastify-metrics": "^9.2.4",
59+
"json5": "^2.2.3",
5960
"node-pg-migrate": "^6.2.2",
6061
"p-queue": "^6.6.2",
6162
"pino": "^8.8.0",

src/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const Metadata = Type.Object({
3939
name: Type.Optional(Type.String()),
4040
description: Type.Optional(Type.String()),
4141
image: Type.Optional(Type.String({ format: 'uri' })),
42+
cached_image: Type.Optional(Type.String({ format: 'uri' })),
4243
attributes: Type.Optional(Type.Array(MetadataAttribute)),
4344
properties: Type.Optional(MetadataProperties),
4445
localization: Type.Optional(MetadataLocalization),

src/api/util/helpers.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ export function parseMetadataLocaleBundle(
88
if (locale && locale.metadata) {
99
response = {
1010
sip: locale.metadata.sip,
11-
name: locale.metadata.name ?? undefined,
12-
description: locale.metadata.description ?? undefined,
13-
image: locale.metadata.image ?? undefined,
11+
name: locale.metadata.name,
12+
description: locale.metadata.description,
13+
image: locale.metadata.image,
14+
cached_image: locale.metadata.cached_image,
1415
};
1516
if (locale.attributes.length > 0) {
1617
response.attributes = locale.attributes.map(item => ({
1718
trait_type: item.trait_type,
1819
value: item.value as MetadataValueType,
19-
display_type: item.display_type ?? undefined,
20+
display_type: item.display_type,
2021
}));
2122
}
2223
if (locale.properties.length > 0) {

src/env.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ interface Env {
7373
* header returned by the domain, if any.
7474
*/
7575
METADATA_RATE_LIMITED_HOST_RETRY_AFTER: number;
76+
/**
77+
* Maximum number of HTTP redirections to follow when fetching metadata. Defaults to 5.
78+
*/
79+
METADATA_FETCH_MAX_REDIRECTIONS: number;
7680

7781
/** Whether or not the `JobQueue` will continue to try retryable failed jobs indefinitely. */
7882
JOB_QUEUE_STRICT_MODE: boolean;
@@ -235,6 +239,10 @@ export function getEnvVars(): Env {
235239
type: 'number',
236240
default: 3600, // 1 hour
237241
},
242+
METADATA_FETCH_MAX_REDIRECTIONS: {
243+
type: 'number',
244+
default: 5,
245+
},
238246
JOB_QUEUE_CONCURRENCY_LIMIT: {
239247
type: 'number',
240248
default: 5,

src/token-processor/blockchain-api/blockchain-importer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,21 @@ export class SmartContractImportInterruptedError extends Error {
1717
}
1818
}
1919

20+
class ApiBlockHeightNotReadyError extends Error {
21+
constructor() {
22+
super();
23+
this.name = this.constructor.name;
24+
}
25+
}
26+
2027
/**
2128
* Imports token contracts and SIP-019 token metadata update notifications from the Stacks
2229
* Blockchain API database.
2330
*/
2431
export class BlockchainImporter {
2532
private readonly db: PgStore;
2633
private readonly apiDb: PgBlockchainApiStore;
34+
private apiBlockHeightRetryIntervalMs = 5000;
2735
private startingBlockHeight: number;
2836
private importInterruptWaiter: Waiter<void>;
2937
private importInterrupted = false;
@@ -46,6 +54,7 @@ export class BlockchainImporter {
4654
}
4755

4856
async import() {
57+
logger.info(`BlockchainImporter last imported block height: ${this.startingBlockHeight}`);
4958
while (!this.importFinished) {
5059
try {
5160
const apiBlockHeight = await this.getApiBlockHeight();
@@ -70,7 +79,10 @@ export class BlockchainImporter {
7079
error,
7180
'BlockchainImporter encountered a PG connection error during import, retrying...'
7281
);
73-
await timeout(100);
82+
await timeout(1000);
83+
} else if (error instanceof ApiBlockHeightNotReadyError) {
84+
logger.warn(`BlockchainImporter API block height too low, retrying...`);
85+
await timeout(this.apiBlockHeightRetryIntervalMs);
7486
} else if (error instanceof SmartContractImportInterruptedError) {
7587
this.importInterruptWaiter.finish();
7688
throw error;
@@ -83,10 +95,9 @@ export class BlockchainImporter {
8395

8496
private async getApiBlockHeight(): Promise<number> {
8597
const blockHeight = (await this.apiDb.getCurrentBlockHeight()) ?? 1;
98+
logger.info(`BlockchainImporter API block height: ${blockHeight}`);
8699
if (this.startingBlockHeight > blockHeight) {
87-
throw new Error(
88-
`BlockchainImporter starting block height ${this.startingBlockHeight} is greater than the API's block height ${blockHeight}`
89-
);
100+
throw new ApiBlockHeightNotReadyError();
90101
}
91102
return blockHeight;
92103
}

src/token-processor/util/metadata-helpers.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as querystring from 'querystring';
2-
import { IncomingHttpHeaders } from 'http';
2+
import * as JSON5 from 'json5';
33
import { Agent, errors, request } from 'undici';
44
import {
55
DbMetadataAttributeInsert,
@@ -31,6 +31,10 @@ const METADATA_FETCH_HTTP_AGENT = new Agent({
3131
headersTimeout: ENV.METADATA_FETCH_TIMEOUT_MS,
3232
bodyTimeout: ENV.METADATA_FETCH_TIMEOUT_MS,
3333
maxResponseSize: ENV.METADATA_MAX_PAYLOAD_BYTE_SIZE,
34+
maxRedirections: ENV.METADATA_FETCH_MAX_REDIRECTIONS,
35+
connect: {
36+
rejectUnauthorized: false, // Ignore SSL cert errors.
37+
},
3438
});
3539

3640
/**
@@ -183,14 +187,12 @@ async function parseMetadataForInsertion(
183187
}
184188

185189
/**
186-
* Fetches metadata while monitoring timeout and size limits. Throws if any is reached.
187-
* Taken from https://github.com/node-fetch/node-fetch/issues/1149#issuecomment-840416752
190+
* Fetches metadata while monitoring timeout and size limits, hostname rate limits, etc. Throws if
191+
* any is reached.
188192
* @param httpUrl - URL to fetch
189193
* @returns Response text
190194
*/
191-
export async function performSizeAndTimeLimitedMetadataFetch(
192-
httpUrl: URL
193-
): Promise<string | undefined> {
195+
export async function fetchMetadata(httpUrl: URL): Promise<string | undefined> {
194196
const url = httpUrl.toString();
195197
try {
196198
const result = await request(url, {
@@ -238,7 +240,7 @@ export async function getMetadataFromUri(token_uri: string): Promise<RawMetadata
238240
content = dataUrl.data;
239241
}
240242
try {
241-
const result = JSON.parse(content);
243+
const result = JSON5.parse(content);
242244
if (RawMetadataCType.Check(result)) {
243245
return result;
244246
}
@@ -258,8 +260,8 @@ export async function getMetadataFromUri(token_uri: string): Promise<RawMetadata
258260
// metadata as dead.
259261
do {
260262
try {
261-
const text = await performSizeAndTimeLimitedMetadataFetch(httpUrl);
262-
result = text ? JSON.parse(text) : undefined;
263+
const text = await fetchMetadata(httpUrl);
264+
result = text ? JSON5.parse(text) : undefined;
263265
break;
264266
} catch (error) {
265267
fetchImmediateRetryCount++;

tests/blockchain-importer.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
BlockchainDbSmartContract,
77
} from '../src/pg/blockchain-api/pg-blockchain-api-store';
88
import { DbSipNumber, DbSmartContractInsert, DbTokenType } from '../src/pg/types';
9-
import { MockPgBlockchainApiStore, SIP_009_ABI, SIP_010_ABI, SIP_013_ABI } from './helpers';
9+
import { MockPgBlockchainApiStore, SIP_009_ABI, SIP_010_ABI, SIP_013_ABI, sleep } from './helpers';
1010
import { BlockchainImporter } from '../src/token-processor/blockchain-api/blockchain-importer';
1111
import { cvToHex, tupleCV, bufferCV, listCV, uintCV } from '@stacks/transactions';
1212

@@ -125,4 +125,24 @@ describe('BlockchainImporter', () => {
125125
expect(jobs2[0].token_id).toBe(1);
126126
expect(jobs2[1].token_id).toBe(2);
127127
});
128+
129+
test('waits for the API to catch up to the chain tip', async () => {
130+
// Set the API behind on purpose.
131+
apiDb.currentBlockHeight = 3;
132+
await db.sql`UPDATE chain_tip SET block_height = 5`;
133+
importer = new BlockchainImporter({ db, apiDb, startingBlockHeight: 5 });
134+
importer['apiBlockHeightRetryIntervalMs'] = 1000;
135+
136+
// Start import, this will trigger a 1s wait loop for the API block height to catch up.
137+
const importPromise = new Promise<void>(resolve => {
138+
void importer.import().then(() => resolve());
139+
});
140+
141+
// Update the API block height after 500ms.
142+
await sleep(500);
143+
apiDb.currentBlockHeight = 5;
144+
145+
// The import finishes then.
146+
await importPromise;
147+
});
128148
});

tests/ft.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ describe('FT routes', () => {
142142
l10n_uri: null,
143143
l10n_default: true,
144144
description: 'test',
145-
image: null,
146-
cached_image: null,
145+
image: 'http://test.com/image.png',
146+
cached_image: 'http://test.com/image.png?processed=true',
147147
},
148148
attributes: [
149149
{
@@ -186,6 +186,8 @@ describe('FT routes', () => {
186186
sip: 16,
187187
description: 'test',
188188
name: 'hello-world',
189+
image: 'http://test.com/image.png',
190+
cached_image: 'http://test.com/image.png?processed=true',
189191
attributes: [
190192
{
191193
display_type: 'number',

tests/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export async function startTestApiServer(db: PgStore): Promise<TestFastifyServer
2323
return await buildApiServer({ db });
2424
}
2525

26+
export const sleep = (time: number) => {
27+
return new Promise(resolve => setTimeout(resolve, time));
28+
};
29+
2630
export class MockPgBlockchainApiStore extends PgBlockchainApiStore {
2731
constructor() {
2832
super(postgres());

0 commit comments

Comments
 (0)