Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-lands-bet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"token-api": minor
---

Update SVM Swaps response to include token metadata, price and summary
2 changes: 1 addition & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
47 changes: 37 additions & 10 deletions src/routes/v1/svm/swaps/svm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
svmAddressSchema,
svmAmmPoolSchema,
svmAmmSchema,
svmMintResponseSchema,
svmMintSchema,
svmNetworkIdSchema,
svmProgramIdSchema,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -106,10 +113,24 @@ const openapi = describeRoute(
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 Jupiter Aggregator v6',
network: 'solana',
},
],
Expand All @@ -129,13 +150,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);
});

Expand Down
95 changes: 75 additions & 20 deletions src/sql/swaps/svm.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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,
Expand All @@ -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)})
Expand All @@ -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
3 changes: 2 additions & 1 deletion src/types/zod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions src/types/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down