Skip to content

Commit

Permalink
add timeout on getOffChainMetadata (orca-so#69)
Browse files Browse the repository at this point in the history
* add timeout on getOffChainMetadata

* update default timeout to 2000ms
  • Loading branch information
yugure-orca authored Jan 22, 2024
1 parent 6126b9f commit c1ea534
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 7 deletions.
4 changes: 2 additions & 2 deletions packages/token-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@orca-so/token-sdk",
"version": "0.3.1",
"version": "0.3.2",
"description": "SPL Token Utilities",
"repository": "https://github.com/orca-so/orca-sdks",
"author": "Orca Foundation",
Expand All @@ -24,7 +24,7 @@
"watch": "tsc -w -p src",
"prepublishOnly": "yarn build",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "jest --detectOpenHandles"
"test": "jest --verbose --detectOpenHandles"
},
"lint-staged": {
"*.{ts,md}": "yarn run prettier-format"
Expand Down
9 changes: 6 additions & 3 deletions packages/token-sdk/src/metadata/client/metaplex-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import invariant from "tiny-invariant";

const METADATA_PROGRAM_ID = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");

// Metadata should be a just tiny JSON file, 2000ms should be sufficient for most cases
const DEFAULT_GET_OFF_CHAIN_METADATA_TIMEOUT_MS = 2000;

interface Creator {
address: PublicKey;
verified: boolean;
Expand Down Expand Up @@ -56,7 +59,7 @@ export interface OffChainMetadata {
export interface MetaplexClient {
getMetadataAddress(mint: PublicKey): PublicKey;
parseOnChainMetadata(mint: PublicKey, buffer: Buffer | Uint8Array): OnChainMetadata | null;
getOffChainMetadata(metadata: OnChainMetadata): Promise<OffChainMetadata | null>;
getOffChainMetadata(metadata: OnChainMetadata, timeoutMs?: number): Promise<OffChainMetadata | null>;
}

export class MetaplexHttpClient implements MetaplexClient {
Expand All @@ -79,12 +82,12 @@ export class MetaplexHttpClient implements MetaplexClient {
}
}

async getOffChainMetadata(metadata: OnChainMetadata): Promise<OffChainMetadata | null> {
async getOffChainMetadata(metadata: OnChainMetadata, timeoutMs: number = DEFAULT_GET_OFF_CHAIN_METADATA_TIMEOUT_MS): Promise<OffChainMetadata | null> {
try {
if (metadata.uri === "") {
return null;
}
const response = await fetch(metadata.uri);
const response = await fetch(metadata.uri, { signal: AbortSignal.timeout(timeoutMs) });
if (response.status === 404) {
return null;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/token-sdk/src/metadata/metaplex-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface Opts {
* https://github.com/metaplex-foundation/js#load
*/
loadImage?: boolean;
getOffChainMetadataTimeoutMs?: number;
}

export class MetaplexProvider implements MetadataProvider {
Expand Down Expand Up @@ -45,7 +46,7 @@ export class MetaplexProvider implements MetadataProvider {
}
let image: string | undefined;
if (this.opts.loadImage ?? true) {
const json = await this.client.getOffChainMetadata(meta);
const json = await this.client.getOffChainMetadata(meta, this.opts.getOffChainMetadataTimeoutMs);
if (json) {
image = json.image;
}
Expand Down Expand Up @@ -84,7 +85,7 @@ export class MetaplexProvider implements MetadataProvider {
continue;
}
jsonHandlers.push(async () => {
const json = await this.client.getOffChainMetadata(meta);
const json = await this.client.getOffChainMetadata(meta, this.opts.getOffChainMetadataTimeoutMs);
jsons[i] = json;
});
}
Expand Down
93 changes: 93 additions & 0 deletions packages/token-sdk/tests/metaplex-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { MetaplexProvider } from "../src/metadata";
import { TokenFetcher } from "../src/fetcher";
import { createTestContext } from "./test-context";

jest.setTimeout(100 * 1000 /* ms */);

describe("metaplex-provider", () => {
// mainnet
const ctx = createTestContext("https://api.mainnet-beta.solana.com");

// ORCA has metadata, but not has image info, so use BONK and SHDW
const MINT_BONK = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263";
const MINT_SHDW = "SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y";

it("find ok", async () => {
const timeoutMs = 2000;
const metaplex_provider = new MetaplexProvider(ctx.connection, {loadImage: true, concurrency: 1, intervalMs: 1000, getOffChainMetadataTimeoutMs: timeoutMs});
const fetcher = new TokenFetcher(ctx.connection).addProvider(metaplex_provider);

const result = await fetcher.find(MINT_BONK, true);
expect(result).toBeDefined();
expect(result.mint).toEqual(MINT_BONK);
expect(result.decimals).toEqual(5);
expect(result.name).toEqual("Bonk");
expect(result.symbol).toEqual("Bonk"); // Their symbol is Bonk (not BONK)...
expect(result.image).toEqual("https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I");
});

it("findMany ok", async () => {
const timeoutMs = 2000;
const metaplex_provider = new MetaplexProvider(ctx.connection, {loadImage: true, concurrency: 2, intervalMs: 1000, getOffChainMetadataTimeoutMs: timeoutMs});
const fetcher = new TokenFetcher(ctx.connection).addProvider(metaplex_provider);

const result = await fetcher.findMany([MINT_BONK, MINT_SHDW], true);
const resultBonk = result.get(MINT_BONK);
expect(resultBonk).toBeDefined();
expect(resultBonk?.mint).toEqual(MINT_BONK);
expect(resultBonk?.decimals).toEqual(5);
expect(resultBonk?.name).toEqual("Bonk");
expect(resultBonk?.symbol).toEqual("Bonk"); // Their symbol is Bonk (not BONK)...
expect(resultBonk?.image).toEqual("https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I");

const resultShdw = result.get(MINT_SHDW);
expect(resultShdw).toBeDefined();
expect(resultShdw?.mint).toEqual(MINT_SHDW);
expect(resultShdw?.decimals).toEqual(9);
expect(resultShdw?.name).toEqual("Shadow Token");
expect(resultShdw?.symbol).toEqual("SHDW");
expect(resultShdw?.image).toEqual("https://shdw-drive.genesysgo.net/FDcC9gn12fFkSU2KuQYH4TUjihrZxiTodFRWNF4ns9Kt/250x250_with_padding.png");
});

it("find timeout", async () => {
const timeoutMs = 1;
const metaplex_provider = new MetaplexProvider(ctx.connection, {loadImage: true, concurrency: 1, intervalMs: 1000, getOffChainMetadataTimeoutMs: timeoutMs});
const fetcher = new TokenFetcher(ctx.connection).addProvider(metaplex_provider);

const result = await fetcher.find(MINT_BONK, true);
expect(result).toBeDefined();
expect(result.mint).toEqual(MINT_BONK);
expect(result.decimals).toEqual(5);
expect(result.name).toEqual("Bonk");
expect(result.symbol).toEqual("Bonk");

// 1ms is too short to fetch offchain metadata, so we expect image to be undefined
expect(result.image).toBeUndefined();
});

it("findMany timeout", async () => {
const timeoutMs = 1;
const metaplex_provider = new MetaplexProvider(ctx.connection, {loadImage: true, concurrency: 2, intervalMs: 1000, getOffChainMetadataTimeoutMs: timeoutMs});
const fetcher = new TokenFetcher(ctx.connection).addProvider(metaplex_provider);

const result = await fetcher.findMany([MINT_BONK, MINT_SHDW], true);
const resultBonk = result.get(MINT_BONK);
expect(resultBonk).toBeDefined();
expect(resultBonk?.mint).toEqual(MINT_BONK);
expect(resultBonk?.decimals).toEqual(5);
expect(resultBonk?.name).toEqual("Bonk");
expect(resultBonk?.symbol).toEqual("Bonk");

const resultShdw = result.get(MINT_SHDW);
expect(resultShdw).toBeDefined();
expect(resultShdw?.mint).toEqual(MINT_SHDW);
expect(resultShdw?.decimals).toEqual(9);
expect(resultShdw?.name).toEqual("Shadow Token");
expect(resultShdw?.symbol).toEqual("SHDW");

// 1ms is too short to fetch offchain metadata, so we expect image to be undefined
expect(resultBonk?.image).toBeUndefined();
expect(resultShdw?.image).toBeUndefined();
});

});

0 comments on commit c1ea534

Please sign in to comment.