From 3456c526e7c2ac0fcc22ea5e73ffa04a9f883b54 Mon Sep 17 00:00:00 2001 From: Etienne Donneger Date: Tue, 9 Dec 2025 10:04:57 -0500 Subject: [PATCH 1/4] Add token metadata to SVM Swaps - Same format as EVM with tuple of `(mint, symbol, decimals)` - Add `price` and `price_inv`, `summary` of swap - Update OpenAPI schemas --- src/routes/v1/svm/swaps/svm.ts | 48 ++++++++++++++---- src/sql/swaps/svm.sql | 93 +++++++++++++++++++++++++++------- src/types/zod.ts | 5 +- 3 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/routes/v1/svm/swaps/svm.ts b/src/routes/v1/svm/swaps/svm.ts index 21e28a26..c9788e8a 100644 --- a/src/routes/v1/svm/swaps/svm.ts +++ b/src/routes/v1/svm/swaps/svm.ts @@ -13,6 +13,7 @@ import { svmAddressSchema, svmAmmPoolSchema, svmAmmSchema, + svmMintResponseSchema, svmMintSchema, svmNetworkIdSchema, svmProgramIdSchema, @@ -65,10 +66,16 @@ const responseSchema = apiUsageResponseSchema.extend({ amm_pool: svmAmmPoolSchema, user: svmAddressSchema, - input_mint: svmMintSchema, - input_amount: z.number(), - output_mint: svmMintSchema, - output_amount: z.number(), + input_mint: svmMintResponseSchema, + input_amount: z.string(), + input_value: z.number(), + output_mint: svmMintResponseSchema, + output_amount: z.string(), + output_value: z.number(), + + price: z.number(), + price_inv: z.number(), + summary: z.string(), // -- chain -- network: svmNetworkIdSchema, @@ -102,14 +109,27 @@ const openapi = describeRoute( transaction_index: 8, instruction_index: 1, program_id: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4', - program_name: 'Jupiter Aggregator v6', + program_name: 'Unknown', amm: '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8', amm_pool: '', user: '5MGfsuYNRhbuN6x1M6WaR3721dSDGtXpcsHxNsgkjsXC', - input_mint: 'HmrzeZapM1EygFc4tBJUXwWTzv5Ahy8axLSAadBx51sw', - input_amount: 49572355581648, - output_mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - output_amount: 936671, + input_mint: { + mint: 'HmrzeZapM1EygFc4tBJUXwWTzv5Ahy8axLSAadBx51sw', + symbol: 'Aeth', + decimals: 9, + }, + input_amount: '49572355581648', + input_value: 49572.355581648, + output_mint: { + mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + symbol: 'USDC', + decimals: 6, + }, + output_amount: '936671', + output_value: 0.936671, + price: 0.000018895027057111676, + price_inv: 52923.97819687809, + summary: 'Swap 49.57 thousand Aeth for 0.936671 USDC on Unknown', network: 'solana', }, ], @@ -129,13 +149,19 @@ route.get('/', openapi, validator('query', querySchema, validatorHook), async (c const params = c.req.valid('query'); const dbConfig = config.uniswapDatabases[params.network]; - if (!dbConfig) { + const db_svm_metadata = config.tokenDatabases[params.network]; + if (!dbConfig || !db_svm_metadata) { return c.json({ error: `Network not found: ${params.network}` }, 400); } const query = sqlQueries.swaps?.[dbConfig.type]; if (!query) return c.json({ error: 'Query for swaps could not be loaded' }, 500); - const response = await makeUsageQueryJson(c, [query], params, { database: dbConfig.database }); + const response = await makeUsageQueryJson( + c, + [query], + { ...params, db_svm_metadata: db_svm_metadata.database }, + { database: dbConfig.database } + ); return handleUsageQueryError(c, response); }); diff --git a/src/sql/swaps/svm.sql b/src/sql/swaps/svm.sql index 3c2da8dd..7c19bba2 100644 --- a/src/sql/swaps/svm.sql +++ b/src/sql/swaps/svm.sql @@ -73,8 +73,8 @@ filtered_minutes AS LIMIT 1 BY minute LIMIT if( (SELECT n FROM active_filters) <= 1, - toUInt64({limit:UInt64}) + toUInt64({offset:UInt64}), /* safe to limit if there is 1 active filter */ - (toUInt64({limit:UInt64}) + toUInt64({offset:UInt64})) * 10 /* unsafe limit with a multiplier - usually safe but find a way to early return */ + toUInt64({limit:UInt64}) + toUInt64({offset:UInt64}), + (toUInt64({limit:UInt64}) + toUInt64({offset:UInt64})) * 10 ) ), /* Latest ingested timestamp in source table */ @@ -105,17 +105,14 @@ filtered_swaps AS AND block_num BETWEEN {start_block: UInt64} AND {end_block: UInt64} AND ( ( - /* if no filters are active, search through the last 10 minutes only */ (SELECT n FROM active_filters) = 0 AND timestamp BETWEEN greatest( toDateTime({start_time:UInt64}), least(toDateTime({end_time:UInt64}), (SELECT ts FROM latest_ts)) - (INTERVAL 10 MINUTE + INTERVAL 1 * {offset:UInt64} SECOND)) AND least(toDateTime({end_time:UInt64}), (SELECT ts FROM latest_ts)) ) - /* if filters are active, search through the intersecting minute ranges */ OR toRelativeMinuteNum(timestamp) IN (SELECT minute FROM filtered_minutes) ) WHERE - /* filter the trimmed down minute ranges by the active filters */ ({signature:Array(String)} = [''] OR signature IN {signature:Array(String)}) AND ({amm:Array(String)} = [''] OR amm IN {amm:Array(String)}) AND ({amm_pool:Array(String)} = [''] OR amm_pool IN {amm_pool:Array(String)}) @@ -126,23 +123,81 @@ filtered_swaps AS ORDER BY timestamp DESC, transaction_index DESC, instruction_index DESC LIMIT {limit:UInt64} OFFSET {offset:UInt64} +), +/* Pre-fetch only the mints we need */ +unique_mints AS ( + SELECT DISTINCT input_mint AS mint FROM filtered_swaps + UNION DISTINCT + SELECT DISTINCT output_mint AS mint FROM filtered_swaps +), +/* Batch lookup decimals */ +decimals_lookup AS ( + SELECT mint, decimals + FROM {db_svm_metadata:Identifier}.decimals_state + WHERE mint IN (SELECT mint FROM unique_mints) +), +/* Batch lookup symbols (mint -> metadata -> symbol) */ +symbols_lookup AS ( + SELECT m.mint, s.symbol + FROM {db_svm_metadata:Identifier}.metadata_mint_state AS m + INNER JOIN {db_svm_metadata:Identifier}.metadata_symbol_state AS s ON m.metadata = s.metadata + WHERE m.mint IN (SELECT mint FROM unique_mints) +), +/* Build token tuples */ +tokens_lookup AS ( + SELECT + d.mint, + CAST( + ( + d.mint, + coalesce(s.symbol, ''), + coalesce(d.decimals, 0) + ) AS Tuple(mint String, symbol String, decimals UInt8) + ) AS token + FROM decimals_lookup d + LEFT JOIN symbols_lookup s ON d.mint = s.mint ) SELECT - block_num, + s.block_num, s.timestamp AS datetime, toUnixTimestamp(s.timestamp) AS timestamp, - toString(signature) AS signature, - transaction_index, - instruction_index, - toString(program_id) AS program_id, - program_name, - toString(amm) AS amm, - toString(amm_pool) AS amm_pool, - toString(user) AS user, - toString(input_mint) AS input_mint, - input_amount, - toString(output_mint) AS output_mint, - output_amount, + toString(s.signature) AS signature, + s.transaction_index, + s.instruction_index, + toString(s.program_id) AS program_id, + s.program_name, + toString(s.amm) AS amm, + toString(s.amm_pool) AS amm_pool, + toString(s.user) AS user, + coalesce(it.token, CAST((toString(s.input_mint), '', 0) AS Tuple(mint String, symbol String, decimals UInt8))) AS input_mint, + toString(s.input_amount) AS input_amount, + s.input_amount / pow(10, coalesce(it.token.decimals, 0)) AS input_value, + coalesce(ot.token, CAST((toString(s.output_mint), '', 0) AS Tuple(mint String, symbol String, decimals UInt8))) AS output_mint, + toString(s.output_amount) AS output_amount, + s.output_amount / pow(10, coalesce(ot.token.decimals, 0)) AS output_value, + if(s.input_amount > 0, + (s.output_amount / pow(10, coalesce(ot.token.decimals, 0))) / (s.input_amount / pow(10, coalesce(it.token.decimals, 0))), + 0 + ) AS price, + if(s.output_amount > 0, + (s.input_amount / pow(10, coalesce(it.token.decimals, 0))) / (s.output_amount / pow(10, coalesce(ot.token.decimals, 0))), + 0 + ) AS price_inv, + format('Swap {} {} for {} {} on {}', + if(s.input_amount / pow(10, coalesce(it.token.decimals, 0)) > 1000, + formatReadableQuantity(s.input_amount / pow(10, coalesce(it.token.decimals, 0))), + toString(round(s.input_amount / pow(10, coalesce(it.token.decimals, 0)), coalesce(it.token.decimals, 0))) + ), + coalesce(it.token.symbol, 'Unknown'), + if(s.output_amount / pow(10, coalesce(ot.token.decimals, 0)) > 1000, + formatReadableQuantity(s.output_amount / pow(10, coalesce(ot.token.decimals, 0))), + toString(round(s.output_amount / pow(10, coalesce(ot.token.decimals, 0)), coalesce(ot.token.decimals, 0))) + ), + coalesce(ot.token.symbol, 'Unknown'), + s.program_name + ) AS summary, {network:String} AS network FROM filtered_swaps AS s -ORDER BY timestamp DESC, transaction_index DESC, instruction_index DESC +LEFT JOIN tokens_lookup AS it ON s.input_mint = it.mint +LEFT JOIN tokens_lookup AS ot ON s.output_mint = ot.mint +ORDER BY s.timestamp DESC, s.transaction_index DESC, s.instruction_index DESC \ No newline at end of file diff --git a/src/types/zod.ts b/src/types/zod.ts index b9464574..8cfb08cd 100644 --- a/src/types/zod.ts +++ b/src/types/zod.ts @@ -409,8 +409,9 @@ export const evmTokenResponseSchema = z.object({ }); export const svmMintResponseSchema = z.object({ - address: svmAddressSchema, - decimals: z.number(), + mint: svmAddressSchema, + symbol: z.string().nullable(), + decimals: z.number().nullable(), }); export const tvmTokenResponseSchema = z.object({ From d8beac363393675c95f7c925a884f6b368672202 Mon Sep 17 00:00:00 2001 From: Etienne Donneger Date: Tue, 9 Dec 2025 10:06:49 -0500 Subject: [PATCH 2/4] Add changeset --- .changeset/fresh-lands-bet.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-lands-bet.md diff --git a/.changeset/fresh-lands-bet.md b/.changeset/fresh-lands-bet.md new file mode 100644 index 00000000..47725241 --- /dev/null +++ b/.changeset/fresh-lands-bet.md @@ -0,0 +1,5 @@ +--- +"token-api": minor +--- + +Update SVM Swaps response to include token metadata, price and summary From 110fb5ade9c11faab5a3f0ff08c6df79dac61dc1 Mon Sep 17 00:00:00 2001 From: Etienne Donneger Date: Tue, 9 Dec 2025 10:09:24 -0500 Subject: [PATCH 3/4] Update Zod schema tests --- biome.jsonc | 2 +- src/types/zod.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index 7566d6e5..c3154269 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.5/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, diff --git a/src/types/zod.spec.ts b/src/types/zod.spec.ts index ce8d58da..5a6e6f86 100644 --- a/src/types/zod.spec.ts +++ b/src/types/zod.spec.ts @@ -541,7 +541,8 @@ describe('Response Schemas', () => { describe('svmMintResponseSchema', () => { it('should validate mint response objects', () => { const mint = { - address: 'So11111111111111111111111111111111111111112', + mint: 'So11111111111111111111111111111111111111112', + symbol: 'SOL', decimals: 9, }; const result = svmMintResponseSchema.parse(mint); From 6d26a2ab78a7f7bc4fbc26ae564328776978ce4f Mon Sep 17 00:00:00 2001 From: Etienne Donneger Date: Tue, 9 Dec 2025 10:47:58 -0500 Subject: [PATCH 4/4] Fix `program_names` returning `Unknown` Convert `program_id` to string before using the SQL function --- src/routes/v1/svm/swaps/svm.ts | 5 +++-- src/sql/swaps/svm.sql | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/v1/svm/swaps/svm.ts b/src/routes/v1/svm/swaps/svm.ts index c9788e8a..945fa19e 100644 --- a/src/routes/v1/svm/swaps/svm.ts +++ b/src/routes/v1/svm/swaps/svm.ts @@ -109,7 +109,7 @@ const openapi = describeRoute( transaction_index: 8, instruction_index: 1, program_id: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4', - program_name: 'Unknown', + program_name: 'Jupiter Aggregator v6', amm: '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8', amm_pool: '', user: '5MGfsuYNRhbuN6x1M6WaR3721dSDGtXpcsHxNsgkjsXC', @@ -129,7 +129,8 @@ const openapi = describeRoute( output_value: 0.936671, price: 0.000018895027057111676, price_inv: 52923.97819687809, - summary: 'Swap 49.57 thousand Aeth for 0.936671 USDC on Unknown', + summary: + 'Swap 49.57 thousand Aeth for 0.936671 USDC on Jupiter Aggregator v6', network: 'solana', }, ], diff --git a/src/sql/swaps/svm.sql b/src/sql/swaps/svm.sql index 7c19bba2..d650c4c4 100644 --- a/src/sql/swaps/svm.sql +++ b/src/sql/swaps/svm.sql @@ -90,7 +90,7 @@ filtered_swaps AS transaction_index, instruction_index, signature, - program_id, + toString(program_id) as program_id, program_names(program_id) AS program_name, amm, amm_pool,