tapimo 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.d.ts +8641 -8263
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +9 -1
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +4 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +5 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +112 -11
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +2 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/early-access.d.ts +2 -2
- package/dist/admin/apps/early-access.d.ts.map +1 -1
- package/dist/admin/apps/early-access.js +5 -5
- package/dist/admin/apps/early-access.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +124 -1
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +97 -6
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +95 -87
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +20 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +18 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +36 -7
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +5016 -4985
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +2 -15
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +1 -1
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +1 -1
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +27 -27
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +21 -1
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +58 -58
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +113 -113
- package/dist/apps/data/routes/fee-amm.d.ts +105 -91
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +76 -91
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +72 -34
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +34 -26
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts +6 -6
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokens.d.ts +2 -2
- package/dist/apps/data/routes/tokens.js +1 -1
- package/dist/apps/data/routes/transactions.d.ts +4 -4
- package/dist/apps/data/routes/transfers.d.ts +99 -88
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +226 -141
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +34 -34
- package/dist/apps/data/routes/valuation.js +3 -3
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +366 -366
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +460 -523
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +7 -23
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +20 -8
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +106 -151
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +28 -28
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +40 -26
- package/dist/apps/management/routes/faucet.d.ts.map +1 -1
- package/dist/apps/management/routes/faucet.js +33 -18
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +131 -131
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +0 -3
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +6 -8
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +0 -4
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +45 -45
- package/dist/apps/management/routes/orgs.d.ts +94 -94
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +5 -0
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +32 -32
- package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
- package/dist/apps/management/routes/webhooks.d.ts +1 -1
- package/dist/apps/mpp/App.d.ts +363 -0
- package/dist/apps/mpp/App.d.ts.map +1 -0
- package/dist/apps/mpp/App.js +72 -0
- package/dist/apps/mpp/App.js.map +1 -0
- package/dist/apps/mpp/Mpp.d.ts +178 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -0
- package/dist/apps/mpp/Mpp.js +183 -0
- package/dist/apps/mpp/Mpp.js.map +1 -0
- package/dist/apps/mpp/index.d.ts +3 -0
- package/dist/apps/mpp/index.d.ts.map +1 -0
- package/dist/apps/mpp/index.js +3 -0
- package/dist/apps/mpp/index.js.map +1 -0
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +56 -9
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +27 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +11 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/enabledBillingSources.d.ts +73 -0
- package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
- package/dist/db/tables/enabledBillingSources.js +69 -0
- package/dist/db/tables/enabledBillingSources.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -12
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +3 -23
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +5 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +13 -2
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +1 -16
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +17 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +37 -5
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +0 -32
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +3 -51
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +78 -32
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +45 -8
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +18 -4
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +50 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +15 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Response.d.ts +2 -2
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Response.js +12 -4
- package/dist/internal/Response.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/package.json +7 -2
- package/src/App.test.ts +9 -4
- package/src/App.ts +14 -2
- package/src/Client.test-d.ts +109 -28
- package/src/Scope.ts +5 -0
- package/src/admin/App.test.ts +4 -0
- package/src/admin/apps/early-access.ts +6 -5
- package/src/admin/apps/organizations.test.ts +86 -2
- package/src/admin/apps/organizations.ts +155 -31
- package/src/admin/apps/scopes.test.ts +5 -0
- package/src/admin/ui/src/lib/organizations.test.ts +81 -1
- package/src/admin/ui/src/lib/organizations.ts +92 -0
- package/src/admin/ui/src/routes/api-keys.tsx +1 -1
- package/src/admin/ui/src/routes/early-access.tsx +8 -3
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
- package/src/admin/ui.generated.ts +95 -109
- package/src/analytics/Analytics.test.ts +83 -0
- package/src/analytics/Analytics.ts +20 -0
- package/src/analytics/tables/requestEvents.ts +60 -7
- package/src/apps/composition.test.ts +11 -2
- package/src/apps/data/App.test.ts +1 -1
- package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
- package/src/apps/data/FundingQuoteProviders.ts +2 -29
- package/src/apps/data/FxOracle.test.ts +2 -2
- package/src/apps/data/FxOracle.ts +2 -2
- package/src/apps/data/routes/activities.test.ts +6 -2
- package/src/apps/data/routes/activities.ts +39 -1
- package/src/apps/data/routes/balances.test.ts +6 -2
- package/src/apps/data/routes/exchanges.test.ts +7 -3
- package/src/apps/data/routes/fee-amm.test.ts +47 -23
- package/src/apps/data/routes/fee-amm.ts +98 -119
- package/src/apps/data/routes/funding.test.ts +29 -8
- package/src/apps/data/routes/funding.ts +42 -48
- package/src/apps/data/routes/receipts.test.ts +6 -2
- package/src/apps/data/routes/tokens.ts +1 -1
- package/src/apps/data/routes/transfers.test.ts +203 -35
- package/src/apps/data/routes/transfers.ts +294 -172
- package/src/apps/data/routes/valuation.test.ts +18 -3
- package/src/apps/data/routes/valuation.ts +4 -4
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +2 -75
- package/src/apps/management/routes/api-keys.ts +30 -7
- package/src/apps/management/routes/billing.test.ts +106 -85
- package/src/apps/management/routes/billing.ts +32 -21
- package/src/apps/management/routes/faucet.test.ts +11 -3
- package/src/apps/management/routes/faucet.ts +46 -30
- package/src/apps/management/routes/invitations.test.ts +0 -172
- package/src/apps/management/routes/invitations.ts +0 -3
- package/src/apps/management/routes/me.test.ts +3 -145
- package/src/apps/management/routes/me.ts +0 -6
- package/src/apps/management/routes/orgs.test.ts +45 -2
- package/src/apps/management/routes/orgs.ts +6 -0
- package/src/apps/management/routes/scopes.test.ts +1 -0
- package/src/apps/mpp/App.ts +86 -0
- package/src/apps/mpp/Mpp.test.ts +193 -0
- package/src/apps/mpp/Mpp.ts +353 -0
- package/src/apps/mpp/index.ts +2 -0
- package/src/apps/mpp/internal/Adapter.ts +11 -0
- package/src/apps/mpp/internal/Error.ts +9 -0
- package/src/apps/mpp/internal/Idempotency.ts +7 -0
- package/src/apps/mpp/internal/Screening.ts +10 -0
- package/src/apps/relay/App.test.ts +88 -6
- package/src/apps/relay/App.ts +61 -9
- package/src/db/Db.test.ts +2 -2
- package/src/db/Db.ts +30 -0
- package/src/db/Schema.ts +15 -0
- package/src/db/tables/enabledBillingSources.test.ts +303 -0
- package/src/db/tables/enabledBillingSources.ts +118 -0
- package/src/db/tables/invitations.ts +4 -26
- package/src/db/tables/organizations.test.ts +103 -0
- package/src/db/tables/organizations.ts +18 -2
- package/src/handlers/internal/utils.ts +22 -6
- package/src/handlers/relay.errors.test.ts +153 -0
- package/src/handlers/relay.ts +42 -7
- package/src/internal/Auth.test.ts +3 -3
- package/src/internal/Auth.ts +3 -63
- package/src/internal/FundingQuotes.test.ts +18 -5
- package/src/internal/FundingQuotes.ts +46 -11
- package/src/internal/Log.test.ts +91 -0
- package/src/internal/Log.ts +69 -5
- package/src/internal/MetricSink.test.ts +55 -0
- package/src/internal/MetricSink.ts +14 -0
- package/src/internal/Response.test.ts +28 -0
- package/src/internal/Response.ts +12 -4
|
@@ -36,6 +36,9 @@ const row = {
|
|
|
36
36
|
rate_limit_scope: null,
|
|
37
37
|
request_id: 'req-1',
|
|
38
38
|
route: '/v1/transactions',
|
|
39
|
+
rpc_error_code: null,
|
|
40
|
+
rpc_error_count: 0,
|
|
41
|
+
rpc_error_data_code: null,
|
|
39
42
|
service: 'api',
|
|
40
43
|
status: 200,
|
|
41
44
|
timestamp: '2026-01-01 00:00:00.000',
|
|
@@ -211,6 +214,9 @@ describe('createQueueSink', () => {
|
|
|
211
214
|
rate_limit_scope: row.rate_limit_scope,
|
|
212
215
|
request_id: row.request_id,
|
|
213
216
|
route: row.route,
|
|
217
|
+
rpc_error_code: row.rpc_error_code,
|
|
218
|
+
rpc_error_count: row.rpc_error_count,
|
|
219
|
+
rpc_error_data_code: row.rpc_error_data_code,
|
|
214
220
|
service: row.service,
|
|
215
221
|
status: row.status,
|
|
216
222
|
timestamp: '<timestamp>',
|
|
@@ -229,6 +235,9 @@ describe('createQueueSink', () => {
|
|
|
229
235
|
"rate_limit_scope": null,
|
|
230
236
|
"request_id": "req-1",
|
|
231
237
|
"route": "/v1/transactions",
|
|
238
|
+
"rpc_error_code": null,
|
|
239
|
+
"rpc_error_count": 0,
|
|
240
|
+
"rpc_error_data_code": null,
|
|
232
241
|
"service": "api",
|
|
233
242
|
"status": 200,
|
|
234
243
|
"timestamp": "<timestamp>",
|
|
@@ -272,6 +281,24 @@ describe('createQueueSink', () => {
|
|
|
272
281
|
})
|
|
273
282
|
})
|
|
274
283
|
|
|
284
|
+
test('records JSON-RPC error metadata', async () => {
|
|
285
|
+
const send = vi.fn<(message: Analytics.Event) => Promise<void>>().mockResolvedValue(undefined)
|
|
286
|
+
const sink = Analytics.createQueueSink({ send })(context)
|
|
287
|
+
|
|
288
|
+
await sink({
|
|
289
|
+
...entry,
|
|
290
|
+
level: 'warn',
|
|
291
|
+
rpc: { code: -32602, dataCode: 'billing_required', errors: 2 },
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
expect(send.mock.calls[0]![0]).toMatchObject({
|
|
295
|
+
rpc_error_code: -32602,
|
|
296
|
+
rpc_error_count: 2,
|
|
297
|
+
rpc_error_data_code: 'billing_required',
|
|
298
|
+
status: 200,
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
275
302
|
test('rethrows when the retry also fails', async () => {
|
|
276
303
|
const send = vi
|
|
277
304
|
.fn<(message: Analytics.Event) => Promise<void>>()
|
|
@@ -399,6 +426,7 @@ describe('readProjectUsage', () => {
|
|
|
399
426
|
expect(query).toContain(`key_environment = 'sandbox'`)
|
|
400
427
|
expect(query).not.toContain('key_id IN')
|
|
401
428
|
expect(query).toContain('LIMIT 100')
|
|
429
|
+
expect(query).toContain('status >= 400 OR rpc_error_count > 0')
|
|
402
430
|
expect(new URL(String(fetch.mock.calls[0]![0])).searchParams.get('query')).toBeNull()
|
|
403
431
|
expect(usage).toMatchObject({
|
|
404
432
|
from: '2026-01-01T00:00:00.000Z',
|
|
@@ -408,6 +436,29 @@ describe('readProjectUsage', () => {
|
|
|
408
436
|
})
|
|
409
437
|
})
|
|
410
438
|
|
|
439
|
+
test('includes JSON-RPC failures in the error breakdown', async () => {
|
|
440
|
+
const fetch = vi.fn<typeof globalThis.fetch>().mockResolvedValue(
|
|
441
|
+
new Response(JSON.stringify({ data: [{ code: 'billing_required', requests: '2' }] }), {
|
|
442
|
+
status: 200,
|
|
443
|
+
}),
|
|
444
|
+
)
|
|
445
|
+
vi.stubGlobal('fetch', fetch)
|
|
446
|
+
|
|
447
|
+
const breakdown = await RequestEvents.readErrorBreakdown(Analytics.clickhouse(clickHouse), {
|
|
448
|
+
attributions: [{ projectId: 'prj_1' }],
|
|
449
|
+
from: '2026-01-01T00:00:00.000Z',
|
|
450
|
+
interval: 'day',
|
|
451
|
+
orgId: 'org_1',
|
|
452
|
+
to: '2026-01-02T00:00:00.000Z',
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
expect(fetch.mock.calls[0]![1]?.body).toEqual(expect.stringContaining('rpc_error_data_code'))
|
|
456
|
+
expect(fetch.mock.calls[0]![1]?.body).toEqual(
|
|
457
|
+
expect.stringContaining('status >= 400 OR rpc_error_count > 0'),
|
|
458
|
+
)
|
|
459
|
+
expect(breakdown).toEqual([{ code: 'billing_required', requests: 2 }])
|
|
460
|
+
})
|
|
461
|
+
|
|
411
462
|
test('keeps legacy rows scoped to project keys without global org filtering', async () => {
|
|
412
463
|
const fetch = vi
|
|
413
464
|
.fn<typeof globalThis.fetch>()
|
|
@@ -525,3 +576,35 @@ describe('readProjectUsage', () => {
|
|
|
525
576
|
})
|
|
526
577
|
})
|
|
527
578
|
})
|
|
579
|
+
|
|
580
|
+
describe('readLastUsedAt', () => {
|
|
581
|
+
test('returns the latest request timestamp for each requested key', async () => {
|
|
582
|
+
const fetch = vi.fn<typeof globalThis.fetch>().mockImplementation(async (_url, init) => {
|
|
583
|
+
const sql = init?.body
|
|
584
|
+
if (typeof sql !== 'string') throw new Error('Expected a SQL request body.')
|
|
585
|
+
expect(sql).toContain("key_id IN ('key_1', 'key_2')")
|
|
586
|
+
expect(sql).toContain('max(timestamp)')
|
|
587
|
+
return new Response(
|
|
588
|
+
JSON.stringify({
|
|
589
|
+
data: [{ key_id: 'key_1', lastUsedAt: '2026-07-21T12:34:56.000Z' }],
|
|
590
|
+
}),
|
|
591
|
+
{ status: 200 },
|
|
592
|
+
)
|
|
593
|
+
})
|
|
594
|
+
vi.stubGlobal('fetch', fetch)
|
|
595
|
+
|
|
596
|
+
await expect(
|
|
597
|
+
RequestEvents.readLastUsedAt(Analytics.clickhouse(clickHouse), ['key_1', 'key_2']),
|
|
598
|
+
).resolves.toEqual([{ apiKeyId: 'key_1', lastUsedAt: '2026-07-21T12:34:56.000Z' }])
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
test('skips the analytics query when no keys are requested', async () => {
|
|
602
|
+
const fetch = vi.fn<typeof globalThis.fetch>()
|
|
603
|
+
vi.stubGlobal('fetch', fetch)
|
|
604
|
+
|
|
605
|
+
await expect(
|
|
606
|
+
RequestEvents.readLastUsedAt(Analytics.clickhouse(clickHouse), []),
|
|
607
|
+
).resolves.toEqual([])
|
|
608
|
+
expect(fetch).not.toHaveBeenCalled()
|
|
609
|
+
})
|
|
610
|
+
})
|
|
@@ -131,6 +131,9 @@ export const migrations: readonly Migration[] = [
|
|
|
131
131
|
project_id Nullable(String),
|
|
132
132
|
rate_limit_scope Nullable(String),
|
|
133
133
|
error_code Nullable(String),
|
|
134
|
+
rpc_error_code Nullable(Int32),
|
|
135
|
+
rpc_error_data_code Nullable(String),
|
|
136
|
+
rpc_error_count UInt16 DEFAULT 0,
|
|
134
137
|
request_id Nullable(String),
|
|
135
138
|
billing_active Nullable(UInt8)
|
|
136
139
|
)
|
|
@@ -156,6 +159,23 @@ export const migrations: readonly Migration[] = [
|
|
|
156
159
|
ADD COLUMN IF NOT EXISTS billing_active Nullable(UInt8)
|
|
157
160
|
`,
|
|
158
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: '0003_request_events_rpc_errors',
|
|
164
|
+
// Patches tables created before RPC columns entered the baseline. Fresh
|
|
165
|
+
// tables include them, allowing scoped preview users without ALTER to skip.
|
|
166
|
+
guard: `
|
|
167
|
+
SELECT toUInt8(count() = 3) FROM system.columns
|
|
168
|
+
WHERE database = currentDatabase()
|
|
169
|
+
AND table = 'request_events'
|
|
170
|
+
AND name IN ('rpc_error_code', 'rpc_error_data_code', 'rpc_error_count')
|
|
171
|
+
`,
|
|
172
|
+
sql: `
|
|
173
|
+
ALTER TABLE request_events
|
|
174
|
+
ADD COLUMN IF NOT EXISTS rpc_error_code Nullable(Int32),
|
|
175
|
+
ADD COLUMN IF NOT EXISTS rpc_error_data_code Nullable(String),
|
|
176
|
+
ADD COLUMN IF NOT EXISTS rpc_error_count UInt16 DEFAULT 0
|
|
177
|
+
`,
|
|
178
|
+
},
|
|
159
179
|
]
|
|
160
180
|
|
|
161
181
|
/**
|
|
@@ -30,6 +30,12 @@ export type Table = {
|
|
|
30
30
|
principal_type: 'api_key' | 'public' | 'session' | 'super_admin' | 'unknown'
|
|
31
31
|
/** Rate-limit bucket scope, when a rate limit was applied. */
|
|
32
32
|
rate_limit_scope: string | null
|
|
33
|
+
/** Numeric JSON-RPC error code when every failed response shares one code. */
|
|
34
|
+
rpc_error_code: number | null
|
|
35
|
+
/** Stable application error code from JSON-RPC `error.data.code`. */
|
|
36
|
+
rpc_error_data_code: string | null
|
|
37
|
+
/** Number of failed JSON-RPC responses in the request. */
|
|
38
|
+
rpc_error_count: number
|
|
33
39
|
/** Stable per-request id (`entry.requestId`); dedups the billing count via `uniqExact`. */
|
|
34
40
|
request_id: string | null
|
|
35
41
|
/** Bounded matched route pattern. */
|
|
@@ -79,6 +85,14 @@ export type KeyBreakdown = {
|
|
|
79
85
|
requests: number
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
/** Latest authenticated request timestamp for one API key. */
|
|
89
|
+
export type LastUsedAt = {
|
|
90
|
+
/** API key id (`key_…`). */
|
|
91
|
+
apiKeyId: string
|
|
92
|
+
/** Latest request completion timestamp (ISO 8601). */
|
|
93
|
+
lastUsedAt: string
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
/** Time bucket size for the usage series. */
|
|
83
97
|
export type Interval = 'day' | 'hour'
|
|
84
98
|
|
|
@@ -227,6 +241,32 @@ export declare namespace insertMessages {
|
|
|
227
241
|
}
|
|
228
242
|
}
|
|
229
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Reads the latest authenticated request timestamp for each requested API key.
|
|
246
|
+
* Keys with no request events are omitted.
|
|
247
|
+
*/
|
|
248
|
+
export async function readLastUsedAt(
|
|
249
|
+
analytics: Analytics.Analytics,
|
|
250
|
+
apiKeyIds: readonly string[],
|
|
251
|
+
): Promise<LastUsedAt[]> {
|
|
252
|
+
if (apiKeyIds.length === 0) return []
|
|
253
|
+
const rows = await analytics.query<LastUsedAtRow>(
|
|
254
|
+
`
|
|
255
|
+
SELECT
|
|
256
|
+
key_id,
|
|
257
|
+
formatDateTime(max(timestamp), '%Y-%m-%dT%H:%i:%S.000Z', 'UTC') AS lastUsedAt
|
|
258
|
+
FROM ${table}
|
|
259
|
+
WHERE principal_type = 'api_key'
|
|
260
|
+
AND key_id IN (${apiKeyIds.map(literal).join(', ')})
|
|
261
|
+
GROUP BY key_id
|
|
262
|
+
`,
|
|
263
|
+
)
|
|
264
|
+
return rows.map((row) => ({
|
|
265
|
+
apiKeyId: String(row.key_id),
|
|
266
|
+
lastUsedAt: String(row.lastUsedAt),
|
|
267
|
+
}))
|
|
268
|
+
}
|
|
269
|
+
|
|
230
270
|
/**
|
|
231
271
|
* Reads request usage for one project from the `request_events` table.
|
|
232
272
|
*
|
|
@@ -247,7 +287,7 @@ export async function readProjectUsage(
|
|
|
247
287
|
SELECT
|
|
248
288
|
key_id,
|
|
249
289
|
count() AS requests,
|
|
250
|
-
countIf(status >= 400) AS errors,
|
|
290
|
+
countIf(status >= 400 OR rpc_error_count > 0) AS errors,
|
|
251
291
|
${average} AS averageDurationMs
|
|
252
292
|
FROM ${table}
|
|
253
293
|
WHERE ${where} AND key_id IS NOT NULL
|
|
@@ -261,7 +301,7 @@ export async function readProjectUsage(
|
|
|
261
301
|
SELECT
|
|
262
302
|
route,
|
|
263
303
|
count() AS requests,
|
|
264
|
-
countIf(status >= 400) AS errors,
|
|
304
|
+
countIf(status >= 400 OR rpc_error_count > 0) AS errors,
|
|
265
305
|
${average} AS averageDurationMs
|
|
266
306
|
FROM ${table}
|
|
267
307
|
WHERE ${where}
|
|
@@ -286,7 +326,7 @@ export async function readProjectUsage(
|
|
|
286
326
|
SELECT
|
|
287
327
|
formatDateTime(${bucket}, '%Y-%m-%dT%H:%i:%S.000Z', 'UTC') AS time,
|
|
288
328
|
count() AS requests,
|
|
289
|
-
countIf(status >= 400) AS errors
|
|
329
|
+
countIf(status >= 400 OR rpc_error_count > 0) AS errors
|
|
290
330
|
FROM ${table}
|
|
291
331
|
WHERE ${where}
|
|
292
332
|
GROUP BY ${bucket}
|
|
@@ -297,7 +337,7 @@ export async function readProjectUsage(
|
|
|
297
337
|
`
|
|
298
338
|
SELECT
|
|
299
339
|
count() AS requests,
|
|
300
|
-
countIf(status >= 400) AS errors,
|
|
340
|
+
countIf(status >= 400 OR rpc_error_count > 0) AS errors,
|
|
301
341
|
${average} AS averageDurationMs
|
|
302
342
|
FROM ${table}
|
|
303
343
|
WHERE ${where}
|
|
@@ -347,11 +387,16 @@ export async function readErrorBreakdown(
|
|
|
347
387
|
const rows = await analytics.query<ErrorRow>(
|
|
348
388
|
`
|
|
349
389
|
SELECT
|
|
350
|
-
coalesce(
|
|
390
|
+
coalesce(
|
|
391
|
+
error_code,
|
|
392
|
+
rpc_error_data_code,
|
|
393
|
+
if(rpc_error_count > 0 AND rpc_error_code IS NOT NULL, concat('rpc_', toString(rpc_error_code)), NULL),
|
|
394
|
+
if(rpc_error_count > 0, 'rpc_multiple', 'unknown')
|
|
395
|
+
) AS code,
|
|
351
396
|
count() AS requests
|
|
352
397
|
FROM ${table}
|
|
353
|
-
WHERE ${usageWhere(options)} AND status >= 400
|
|
354
|
-
GROUP BY
|
|
398
|
+
WHERE ${usageWhere(options)} AND (status >= 400 OR rpc_error_count > 0)
|
|
399
|
+
GROUP BY code
|
|
355
400
|
ORDER BY requests DESC, code ASC
|
|
356
401
|
LIMIT 25
|
|
357
402
|
`,
|
|
@@ -434,6 +479,11 @@ type KeyRow = {
|
|
|
434
479
|
requests: number | string
|
|
435
480
|
}
|
|
436
481
|
|
|
482
|
+
type LastUsedAtRow = {
|
|
483
|
+
key_id: string
|
|
484
|
+
lastUsedAt: string
|
|
485
|
+
}
|
|
486
|
+
|
|
437
487
|
type ErrorRow = {
|
|
438
488
|
code: string
|
|
439
489
|
requests: number | string
|
|
@@ -480,6 +530,9 @@ function event(entry: Log.Entry, context: Metrics.cloudflare.Context): Table {
|
|
|
480
530
|
rate_limit_scope: entry.rateLimit?.scope ?? null,
|
|
481
531
|
request_id: entry.requestId ?? null,
|
|
482
532
|
route: entry.route,
|
|
533
|
+
rpc_error_code: entry.rpc?.code ?? null,
|
|
534
|
+
rpc_error_count: entry.rpc?.errors ?? 0,
|
|
535
|
+
rpc_error_data_code: entry.rpc?.dataCode ?? null,
|
|
483
536
|
service: context.service ?? 'unknown',
|
|
484
537
|
status: entry.status,
|
|
485
538
|
timestamp: timestamp(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { App, Store } from 'tapimo'
|
|
2
|
-
import { data, management, relay } from 'tapimo/apps'
|
|
2
|
+
import { data, management, mpp, relay } from 'tapimo/apps'
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { describeRoute } from 'hono-openapi'
|
|
5
5
|
import * as TestApp from '../../test/App.js'
|
|
@@ -190,7 +190,7 @@ describe('data()', () => {
|
|
|
190
190
|
})
|
|
191
191
|
|
|
192
192
|
describe('relay()', () => {
|
|
193
|
-
test('mounts the relay after data()
|
|
193
|
+
test('mounts the relay after data() without documenting RPC routes', async () => {
|
|
194
194
|
const composed = App.create({ db: TestApp.database() }).route('/', data()).route('/', relay())
|
|
195
195
|
|
|
196
196
|
// Mounted but default-closed and hidden: unauthenticated requests are
|
|
@@ -209,6 +209,15 @@ describe('relay()', () => {
|
|
|
209
209
|
})
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
+
describe('mpp()', () => {
|
|
213
|
+
test('mounts documented MPP routes independently', async () => {
|
|
214
|
+
const app = App.create({ db: TestApp.database() }).route('/', mpp())
|
|
215
|
+
|
|
216
|
+
expect((await paths(app)).some((path) => path.startsWith('/v1/mpp'))).toBe(true)
|
|
217
|
+
expect((await app.request('/rpc/relay')).status).toBe(404)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
212
221
|
describe('management()', () => {
|
|
213
222
|
test('mounts management routes and omits data routes', async () => {
|
|
214
223
|
const app = App.create({
|
|
@@ -254,7 +254,7 @@ describe('zone chain gate', () => {
|
|
|
254
254
|
for (const path of [
|
|
255
255
|
`/v1/tokens?chainId=${zone}`,
|
|
256
256
|
`/v1/addresses/0x0000000000000000000000000000000000000001/activities?chainId=${zone}`,
|
|
257
|
-
`/v1/transfers?chainId=${zone}&include=token`,
|
|
257
|
+
`/v1/transfers?chainId=${zone}&include=token.verified`,
|
|
258
258
|
]) {
|
|
259
259
|
const response = await app.request(path)
|
|
260
260
|
const body = (await response.json()) as { error: { code: string; message: string } }
|
|
@@ -84,7 +84,6 @@ describe('FundingQuoteProviders', () => {
|
|
|
84
84
|
expiresAt: '2026-01-01T00:01:00.000Z',
|
|
85
85
|
minimumOutputAmount: '988000',
|
|
86
86
|
outputAmount: '990000',
|
|
87
|
-
outputAmountFormatted: '0.99',
|
|
88
87
|
quality: {
|
|
89
88
|
estimatedSeconds: 2,
|
|
90
89
|
liquiditySource: 'providerQuote',
|
|
@@ -185,7 +184,6 @@ describe('FundingQuoteProviders', () => {
|
|
|
185
184
|
expect(result).toMatchObject({
|
|
186
185
|
minimumOutputAmount: '980000',
|
|
187
186
|
outputAmount: '990000',
|
|
188
|
-
outputAmountFormatted: '0.99',
|
|
189
187
|
quality: {
|
|
190
188
|
estimatedSeconds: 15,
|
|
191
189
|
liquiditySource: 'providerQuote',
|
|
@@ -230,7 +228,6 @@ describe('FundingQuoteProviders', () => {
|
|
|
230
228
|
expect(result).toEqual({
|
|
231
229
|
minimumOutputAmount: '989622',
|
|
232
230
|
outputAmount: '999620',
|
|
233
|
-
outputAmountFormatted: '0.99962',
|
|
234
231
|
quality: {
|
|
235
232
|
estimatedSeconds: 10,
|
|
236
233
|
liquiditySource: 'providerQuote',
|
|
@@ -308,7 +305,6 @@ describe('FundingQuoteProviders', () => {
|
|
|
308
305
|
expect(result).toEqual({
|
|
309
306
|
minimumOutputAmount: '979000',
|
|
310
307
|
outputAmount: '989000',
|
|
311
|
-
outputAmountFormatted: '0.989',
|
|
312
308
|
quality: {
|
|
313
309
|
estimatedSeconds: 18,
|
|
314
310
|
liquiditySource: 'providerQuote',
|
|
@@ -480,7 +476,6 @@ describe('FundingQuoteProviders', () => {
|
|
|
480
476
|
})
|
|
481
477
|
expect(result).toEqual({
|
|
482
478
|
outputAmount: '983000',
|
|
483
|
-
outputAmountFormatted: '0.983',
|
|
484
479
|
quality: {
|
|
485
480
|
estimatedSeconds: 2,
|
|
486
481
|
liquiditySource: 'providerQuote',
|
|
@@ -292,7 +292,7 @@ export function relay(options: relay.Options): FundingQuotes.ProviderAdapter {
|
|
|
292
292
|
method: 'POST',
|
|
293
293
|
signal,
|
|
294
294
|
})
|
|
295
|
-
return formatRelayResult(
|
|
295
|
+
return formatRelayResult(quote, now())
|
|
296
296
|
} catch (cause) {
|
|
297
297
|
const unavailable = providerUnavailable(cause, [
|
|
298
298
|
'NO_QUOTES',
|
|
@@ -600,10 +600,6 @@ function formatAcrossResult(
|
|
|
600
600
|
expiresAt: expiresAt.toISOString(),
|
|
601
601
|
minimumOutputAmount: response.minOutputAmount,
|
|
602
602
|
outputAmount: response.expectedOutputAmount,
|
|
603
|
-
outputAmountFormatted: decimalFromBaseUnits(
|
|
604
|
-
response.expectedOutputAmount,
|
|
605
|
-
candidate.destinationToken.decimals,
|
|
606
|
-
),
|
|
607
603
|
quality: {
|
|
608
604
|
estimatedSeconds: response.expectedFillTime,
|
|
609
605
|
liquiditySource: 'providerQuote',
|
|
@@ -615,11 +611,7 @@ function formatAcrossResult(
|
|
|
615
611
|
}
|
|
616
612
|
}
|
|
617
613
|
|
|
618
|
-
function formatRelayResult(
|
|
619
|
-
candidate: FundingQuotes.QuoteCandidate,
|
|
620
|
-
quote: unknown,
|
|
621
|
-
now: Date,
|
|
622
|
-
): FundingQuotes.QuoteProbeResult {
|
|
614
|
+
function formatRelayResult(quote: unknown, now: Date): FundingQuotes.QuoteProbeResult {
|
|
623
615
|
const details = record(record(quote)['details'])
|
|
624
616
|
const currencyOut = record(details['currencyOut'])
|
|
625
617
|
const outputAmount = decimalString(currencyOut['amount'])
|
|
@@ -630,9 +622,6 @@ function formatRelayResult(
|
|
|
630
622
|
return {
|
|
631
623
|
minimumOutputAmount: decimalString(currencyOut['minimumAmount']),
|
|
632
624
|
outputAmount,
|
|
633
|
-
outputAmountFormatted: outputAmount
|
|
634
|
-
? decimalFromBaseUnits(outputAmount, candidate.destinationToken.decimals)
|
|
635
|
-
: undefined,
|
|
636
625
|
quality: {
|
|
637
626
|
estimatedSeconds: nonnegativeInteger(details['timeEstimate']),
|
|
638
627
|
liquiditySource: 'providerQuote',
|
|
@@ -669,7 +658,6 @@ function formatRhinoResult(
|
|
|
669
658
|
|
|
670
659
|
return {
|
|
671
660
|
outputAmount,
|
|
672
|
-
outputAmountFormatted: decimalFromBaseUnits(outputAmount, candidate.destinationToken.decimals),
|
|
673
661
|
quality: {
|
|
674
662
|
...(response.estimatedDuration === undefined || response.estimatedDuration === null
|
|
675
663
|
? {}
|
|
@@ -704,10 +692,6 @@ function formatSquidResult(
|
|
|
704
692
|
return {
|
|
705
693
|
minimumOutputAmount: response.toAmountMin,
|
|
706
694
|
outputAmount: response.toAmount,
|
|
707
|
-
outputAmountFormatted: decimalFromBaseUnits(
|
|
708
|
-
response.toAmount,
|
|
709
|
-
candidate.destinationToken.decimals,
|
|
710
|
-
),
|
|
711
695
|
quality: {
|
|
712
696
|
estimatedSeconds: Math.ceil(response.estimatedRouteDuration),
|
|
713
697
|
liquiditySource: 'providerQuote',
|
|
@@ -764,10 +748,6 @@ function formatSymbiosisResult(
|
|
|
764
748
|
return {
|
|
765
749
|
minimumOutputAmount: response.tokenAmountOutMin.amount,
|
|
766
750
|
outputAmount: response.tokenAmountOut.amount,
|
|
767
|
-
outputAmountFormatted: decimalFromBaseUnits(
|
|
768
|
-
response.tokenAmountOut.amount,
|
|
769
|
-
candidate.destinationToken.decimals,
|
|
770
|
-
),
|
|
771
751
|
quality: {
|
|
772
752
|
...(response.estimatedTime === undefined
|
|
773
753
|
? {}
|
|
@@ -1005,13 +985,6 @@ function nonnegativeInteger(input: unknown) {
|
|
|
1005
985
|
return undefined
|
|
1006
986
|
}
|
|
1007
987
|
|
|
1008
|
-
function decimalFromBaseUnits(baseUnits: string, decimals: number) {
|
|
1009
|
-
const padded = baseUnits.padStart(decimals + 1, '0')
|
|
1010
|
-
const whole = decimals === 0 ? padded : padded.slice(0, -decimals)
|
|
1011
|
-
const fraction = decimals === 0 ? '' : padded.slice(-decimals).replace(/0+$/, '')
|
|
1012
|
-
return fraction ? `${whole}.${fraction}` : whole
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
988
|
function baseUnitsFromDecimal(value: string, decimals: number) {
|
|
1016
989
|
if (!/^\d+(\.\d+)?$/.test(value)) return undefined
|
|
1017
990
|
const [whole, fraction = ''] = value.split('.')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as FxOracle from './FxOracle.js'
|
|
2
2
|
|
|
3
3
|
const set = {
|
|
4
|
-
asOf: '2026-01-
|
|
4
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
5
5
|
base: 'EUR',
|
|
6
6
|
rates: { AUD: '1.6', USD: '1.25' },
|
|
7
7
|
} satisfies FxOracle.RateSet
|
|
@@ -45,7 +45,7 @@ describe('ecb', () => {
|
|
|
45
45
|
const rates = await FxOracle.ecb({ url: feedUrl('1.25') }).rates()
|
|
46
46
|
expect(rates).toMatchInlineSnapshot(`
|
|
47
47
|
{
|
|
48
|
-
"asOf": "2026-01-
|
|
48
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
49
49
|
"base": "EUR",
|
|
50
50
|
"rates": {
|
|
51
51
|
"AUD": "1.6",
|
|
@@ -14,7 +14,7 @@ export type Oracle = {
|
|
|
14
14
|
|
|
15
15
|
/** One oracle-published set of exchange rates quoted against a base currency. */
|
|
16
16
|
export type RateSet = {
|
|
17
|
-
/** Publication date of the rate set
|
|
17
|
+
/** Publication date of the rate set as an ISO 8601 timestamp. */
|
|
18
18
|
asOf: string
|
|
19
19
|
/** Currency the rates are quoted against (e.g. `EUR`). */
|
|
20
20
|
base: string
|
|
@@ -88,7 +88,7 @@ export function ecb(options: ecb.Options = {}): Oracle {
|
|
|
88
88
|
rates[code] = value
|
|
89
89
|
}
|
|
90
90
|
if (!asOf || rates['USD'] === undefined) throw new ParseError()
|
|
91
|
-
return { asOf
|
|
91
|
+
return { asOf: `${asOf}T00:00:00.000Z`, base: 'EUR', rates }
|
|
92
92
|
},
|
|
93
93
|
})
|
|
94
94
|
}
|
|
@@ -12,7 +12,11 @@ import * as Activities from './activities.js'
|
|
|
12
12
|
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
13
13
|
const fixed = FxOracle.from({
|
|
14
14
|
name: 'fixed',
|
|
15
|
-
rates: async () => ({
|
|
15
|
+
rates: async () => ({
|
|
16
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
17
|
+
base: 'EUR',
|
|
18
|
+
rates: { AUD: '1.6', USD: '1.0' },
|
|
19
|
+
}),
|
|
16
20
|
})
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -312,7 +316,7 @@ describe('GET /addresses/:address/activities', () => {
|
|
|
312
316
|
})
|
|
313
317
|
expect(body.meta?.pricing).toMatchInlineSnapshot(`
|
|
314
318
|
{
|
|
315
|
-
"asOf": "2026-01-
|
|
319
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
316
320
|
"basis": "nominal",
|
|
317
321
|
"source": "fixed",
|
|
318
322
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono, type Context } from 'hono'
|
|
2
2
|
import { Address, Hex } from 'ox'
|
|
3
3
|
import { type Log, parseEventLogs, zeroAddress } from 'viem'
|
|
4
|
+
import { token as viem_Token } from 'viem/actions'
|
|
4
5
|
import { Abis, Addresses } from 'viem/tempo'
|
|
5
6
|
import * as z from 'zod/mini'
|
|
6
7
|
|
|
@@ -28,6 +29,10 @@ const exampleHash = '0x4845ae2098724ab26a5d89370dce5124d044be6a5acc164afc348662d
|
|
|
28
29
|
|
|
29
30
|
const tokenFields = ['token.logoUri', 'token.verified'] as const
|
|
30
31
|
|
|
32
|
+
// TIP-20 tokens live at deterministic `0x20c0…` addresses. Other contracts
|
|
33
|
+
// can emit the same `Transfer` signature but do not expose TIP-20 metadata.
|
|
34
|
+
const tip20AddressPrefix = '0x20c0'
|
|
35
|
+
|
|
31
36
|
/** Zod schemas owned by the activity handlers. */
|
|
32
37
|
export namespace schema {
|
|
33
38
|
/** A signer: the literal `self`, or the access-key address that signed. */
|
|
@@ -3982,6 +3987,39 @@ type ResolvedToken = {
|
|
|
3982
3987
|
verified?: boolean | undefined
|
|
3983
3988
|
}
|
|
3984
3989
|
|
|
3990
|
+
/** Resolves TIP-20 metadata or the standard ERC-20 subset used by activity transfers. */
|
|
3991
|
+
function getActivityTokenMetadata(
|
|
3992
|
+
c: Context<App.Environment>,
|
|
3993
|
+
options: getActivityTokenMetadata.Options,
|
|
3994
|
+
): Promise<Omit<ResolvedToken, 'verified'>> {
|
|
3995
|
+
const { address, chainId } = options
|
|
3996
|
+
if (address.startsWith(tip20AddressPrefix))
|
|
3997
|
+
return Tokens.getTokenMetadata(c, { address, chainId })
|
|
3998
|
+
|
|
3999
|
+
return Store.memoize(
|
|
4000
|
+
async () => {
|
|
4001
|
+
const metadata = await viem_Token.getMetadata(c.get('getClient')(chainId), { token: address })
|
|
4002
|
+
// ERC-20 has no currency field; its symbol is the amount's unit label.
|
|
4003
|
+
return {
|
|
4004
|
+
currency: metadata.symbol,
|
|
4005
|
+
decimals: metadata.decimals,
|
|
4006
|
+
name: metadata.name,
|
|
4007
|
+
symbol: metadata.symbol,
|
|
4008
|
+
}
|
|
4009
|
+
},
|
|
4010
|
+
{ key: `token:erc20:v1:${chainId}:${address}:metadata`, store: c.get('store'), ttl: Ttl.minutes(1) }, // prettier-ignore
|
|
4011
|
+
)
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
declare namespace getActivityTokenMetadata {
|
|
4015
|
+
type Options = {
|
|
4016
|
+
/** Token contract address. */
|
|
4017
|
+
address: Address.Address
|
|
4018
|
+
/** Source chain id. */
|
|
4019
|
+
chainId: z.output<typeof Schema.ChainId>
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
|
|
3985
4023
|
/** Resolves core metadata into immutable items. Optional logo and verification sources are best-effort. */
|
|
3986
4024
|
async function enrichTokens(
|
|
3987
4025
|
c: Context<App.Environment>,
|
|
@@ -4018,7 +4056,7 @@ async function enrichTokens(
|
|
|
4018
4056
|
includeLogoUri
|
|
4019
4057
|
? Tokens.getTokenLogo(c, { address, chainId }).catch(() => undefined)
|
|
4020
4058
|
: undefined,
|
|
4021
|
-
|
|
4059
|
+
getActivityTokenMetadata(c, { address, chainId }),
|
|
4022
4060
|
])
|
|
4023
4061
|
return [
|
|
4024
4062
|
address.toLowerCase(),
|
|
@@ -12,7 +12,11 @@ import * as Tokens from './tokens.js'
|
|
|
12
12
|
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
13
13
|
const fixed = FxOracle.from({
|
|
14
14
|
name: 'fixed',
|
|
15
|
-
rates: async () => ({
|
|
15
|
+
rates: async () => ({
|
|
16
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
17
|
+
base: 'EUR',
|
|
18
|
+
rates: { AUD: '1.6', USD: '1.0' },
|
|
19
|
+
}),
|
|
16
20
|
})
|
|
17
21
|
|
|
18
22
|
// Verified enrichment is store-backed now that the build-time bundle is gone, so
|
|
@@ -381,7 +385,7 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
381
385
|
// Consulted rates surface their provenance response-wide.
|
|
382
386
|
expect(body.meta?.pricing).toMatchInlineSnapshot(`
|
|
383
387
|
{
|
|
384
|
-
"asOf": "2026-01-
|
|
388
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
385
389
|
"basis": "nominal",
|
|
386
390
|
"source": "fixed",
|
|
387
391
|
}
|
|
@@ -33,7 +33,11 @@ type PairsResponse = z.output<typeof Exchanges.schema.getPairs.Response>
|
|
|
33
33
|
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
34
34
|
const fixed = FxOracle.from({
|
|
35
35
|
name: 'fixed',
|
|
36
|
-
rates: async () => ({
|
|
36
|
+
rates: async () => ({
|
|
37
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
38
|
+
base: 'EUR',
|
|
39
|
+
rates: { AUD: '1.6', USD: '1.0' },
|
|
40
|
+
}),
|
|
37
41
|
})
|
|
38
42
|
|
|
39
43
|
type Client = ReturnType<typeof TestApp.client>
|
|
@@ -652,7 +656,7 @@ describe('GET /exchange/swaps', () => {
|
|
|
652
656
|
})
|
|
653
657
|
expect(body.meta?.pricing).toMatchInlineSnapshot(`
|
|
654
658
|
{
|
|
655
|
-
"asOf": "2026-01-
|
|
659
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
656
660
|
"basis": "nominal",
|
|
657
661
|
"source": "fixed",
|
|
658
662
|
}
|
|
@@ -1165,7 +1169,7 @@ describe('POST /exchange/quotes', () => {
|
|
|
1165
1169
|
})
|
|
1166
1170
|
expect(body.meta?.pricing).toMatchInlineSnapshot(`
|
|
1167
1171
|
{
|
|
1168
|
-
"asOf": "2026-01-
|
|
1172
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
1169
1173
|
"basis": "nominal",
|
|
1170
1174
|
"source": "fixed",
|
|
1171
1175
|
}
|