tapimo 0.8.0 → 0.10.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 +7410 -7613
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +0 -1
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +21 -20
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +66 -23
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/apps/Zones.d.ts +34 -22
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +9 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3120 -2782
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +4 -5
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +19 -4
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +32 -27
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +283 -52
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +291 -21
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +232 -66
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/coingecko.d.ts +54 -54
- package/dist/apps/data/routes/earn.d.ts +642 -126
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +627 -58
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +120 -120
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +114 -118
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +68 -47
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +2 -0
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +59 -51
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +25 -23
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +40 -18
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +618 -105
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +2 -2
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +102 -505
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +484 -342
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +12 -12
- package/dist/apps/data/routes/zones.d.ts.map +1 -1
- package/dist/apps/data/routes/zones.js +3 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +127 -386
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/App.js +20 -5
- package/dist/apps/funding/App.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +21 -103
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +4 -4
- package/dist/apps/funding/routes/transfers.js +5 -5
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +429 -727
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +24 -24
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/faucet.d.ts +6 -6
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/projects.d.ts +20 -20
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/management/routes/webhooks.d.ts +158 -573
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/mpp/App.d.ts +12 -0
- package/dist/apps/mpp/App.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.d.ts +3 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.js +3 -0
- package/dist/apps/mpp/Mpp.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +9 -2
- package/dist/apps/mpp/internal/Adapter.js.map +1 -1
- package/dist/db/Db.d.ts +2 -2
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +25 -15
- package/dist/db/Db.js.map +1 -1
- package/dist/db/tables/fundingDeposits.d.ts +18 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +46 -2
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +4 -1
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +76 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +4 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +11 -2
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +112 -3
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +116 -3
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Schema.js +1 -1
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Tidx.d.ts +1 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +1 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +2 -1
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts +2 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +11 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +55 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -0
- package/dist/internal/WebhookTransfer.js +296 -0
- package/dist/internal/WebhookTransfer.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +37 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +87 -13
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +10 -6
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +11 -7
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +229 -16
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -0
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +18 -3
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/App.test.ts +32 -0
- package/src/App.ts +4 -1
- package/src/Client.test-d.ts +116 -38
- package/src/Webhooks.bench.ts +714 -0
- package/src/Webhooks.memory.bench.ts +250 -0
- package/src/Webhooks.test.ts +77 -6
- package/src/Webhooks.ts +83 -29
- package/src/apps/Zones.test.ts +5 -4
- package/src/apps/Zones.ts +9 -8
- package/src/apps/composition.test.ts +39 -10
- package/src/apps/data/FxOracle.test.ts +18 -0
- package/src/apps/data/FxOracle.ts +28 -5
- package/src/apps/data/routes/activities.test.ts +19 -0
- package/src/apps/data/routes/activities.ts +34 -30
- package/src/apps/data/routes/balances.test.ts +161 -10
- package/src/apps/data/routes/balances.ts +339 -147
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/earn.test.ts +286 -9
- package/src/apps/data/routes/earn.ts +841 -58
- package/src/apps/data/routes/exchanges.test.ts +7 -6
- package/src/apps/data/routes/exchanges.ts +124 -117
- package/src/apps/data/routes/receipts.test.ts +24 -7
- package/src/apps/data/routes/receipts.ts +67 -50
- package/src/apps/data/routes/tokens.test.ts +51 -0
- package/src/apps/data/routes/transactions.test.ts +42 -0
- package/src/apps/data/routes/transactions.ts +44 -39
- package/src/apps/data/routes/transfers.test.ts +402 -24
- package/src/apps/data/routes/transfers.ts +708 -101
- package/src/apps/data/routes/valuation.test.ts +26 -0
- package/src/apps/data/routes/valuation.ts +6 -2
- package/src/apps/data/routes/webhooks.test.ts +414 -130
- package/src/apps/data/routes/webhooks.ts +603 -419
- package/src/apps/data/routes/zones.ts +6 -1
- package/src/apps/funding/App.ts +20 -5
- package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
- package/src/apps/funding/routes/deposit-addresses.ts +34 -117
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +6 -25
- package/src/apps/funding/routes/transfers.ts +5 -5
- package/src/apps/mpp/Mpp.test.ts +13 -1
- package/src/apps/mpp/Mpp.ts +8 -0
- package/src/apps/mpp/internal/Adapter.ts +10 -2
- package/src/db/Db.test.ts +40 -6
- package/src/db/Db.ts +42 -19
- package/src/db/tables/fundingDeposits.test.ts +101 -0
- package/src/db/tables/fundingDeposits.ts +71 -2
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookSubscriptions.ts +116 -4
- package/src/internal/Auth.test.ts +28 -0
- package/src/internal/Auth.ts +15 -1
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +518 -0
- package/src/internal/MetricSink.ts +266 -9
- package/src/internal/Schema.ts +1 -1
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +1 -3
- package/src/internal/Viem.test.ts +8 -0
- package/src/internal/Viem.ts +2 -1
- package/src/internal/WebhookDestination.test.ts +20 -0
- package/src/internal/WebhookDestination.ts +10 -0
- package/src/internal/WebhookTransfer.ts +426 -0
- package/src/internal/Webhooks.test.ts +137 -0
- package/src/internal/Webhooks.ts +131 -17
- package/src/internal/funding/Provider.test.ts +47 -7
- package/src/internal/funding/Provider.ts +16 -11
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1532 -54
- package/src/internal/funding/Reconciliation.ts +296 -53
- package/src/internal/funding/SourceObservation.test.ts +3 -2
- package/src/internal/funding/providers/relay.test.ts +1 -4
- package/src/internal/funding/providers/relay.ts +23 -3
|
@@ -18,7 +18,12 @@ async function document(app: { request: (path: string) => Promise<Response> | Re
|
|
|
18
18
|
string,
|
|
19
19
|
Partial<Record<'get' | 'post', { operationId: string; tags?: readonly string[] | undefined }>>
|
|
20
20
|
>
|
|
21
|
-
tags: readonly {
|
|
21
|
+
tags: readonly {
|
|
22
|
+
description: string
|
|
23
|
+
name: string
|
|
24
|
+
'x-displayName'?: string | undefined
|
|
25
|
+
'x-pagePath'?: string | undefined
|
|
26
|
+
}[]
|
|
22
27
|
webhooks?: Record<string, unknown> | undefined
|
|
23
28
|
'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
|
|
24
29
|
}
|
|
@@ -294,7 +299,6 @@ describe('funding()', () => {
|
|
|
294
299
|
'/v1/funding/chains',
|
|
295
300
|
'/v1/funding/deposit-addresses',
|
|
296
301
|
'/v1/funding/deposit-addresses/{id}',
|
|
297
|
-
'/v1/funding/deposit-addresses/{id}/deposits',
|
|
298
302
|
'/v1/funding/deposit-addresses/{id}/reconcile',
|
|
299
303
|
'/v1/funding/deposits',
|
|
300
304
|
'/v1/funding/deposits/{id}',
|
|
@@ -311,7 +315,7 @@ describe('funding()', () => {
|
|
|
311
315
|
expect(spec['x-tagGroups']).toEqual([
|
|
312
316
|
{
|
|
313
317
|
name: 'Funding & Bridge API',
|
|
314
|
-
tags: ['Chains', 'Deposit Addresses', 'Providers', 'Quotes', 'Transfers'],
|
|
318
|
+
tags: ['Chains', 'Deposit Addresses', 'Providers', 'Quotes', 'Funding Transfers'],
|
|
315
319
|
},
|
|
316
320
|
])
|
|
317
321
|
expect(
|
|
@@ -325,13 +329,12 @@ describe('funding()', () => {
|
|
|
325
329
|
'reconcileFundingDepositAddress',
|
|
326
330
|
'getFundingDepositAddress',
|
|
327
331
|
'getFundingDeposit',
|
|
328
|
-
'listFundingDepositAddressDeposits',
|
|
329
332
|
'listFundingDeposits',
|
|
330
333
|
])
|
|
331
334
|
expect((await app.request('/v1/tokens')).status).toBe(404)
|
|
332
335
|
})
|
|
333
336
|
|
|
334
|
-
test('
|
|
337
|
+
test('keeps data and funding transfers separate with the same display name', async () => {
|
|
335
338
|
const app = App.create({ auth: false, db: TestApp.database() })
|
|
336
339
|
.route('/', data())
|
|
337
340
|
.route('/', funding())
|
|
@@ -344,9 +347,10 @@ describe('funding()', () => {
|
|
|
344
347
|
expect({
|
|
345
348
|
chains: operationTag('/v1/funding/chains', 'get'),
|
|
346
349
|
depositAddress: operationTag('/v1/funding/deposit-addresses', 'post'),
|
|
347
|
-
deposits: operationTag('/v1/funding/
|
|
350
|
+
deposits: operationTag('/v1/funding/deposits', 'get'),
|
|
348
351
|
providers: operationTag('/v1/funding/providers', 'get'),
|
|
349
352
|
quotes: operationTag('/v1/funding/quotes', 'get'),
|
|
353
|
+
tokenTransfers: operationTag('/v1/transfers', 'get'),
|
|
350
354
|
transfers: operationTag('/v1/funding/transfers', 'get'),
|
|
351
355
|
vault: operationTag('/v1/funding/transfers/vault', 'post'),
|
|
352
356
|
zone: operationTag('/v1/funding/transfers/zone', 'post'),
|
|
@@ -356,17 +360,42 @@ describe('funding()', () => {
|
|
|
356
360
|
deposits: ['Deposit Addresses'],
|
|
357
361
|
providers: ['Providers'],
|
|
358
362
|
quotes: ['Quotes'],
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
363
|
+
tokenTransfers: ['Transfers'],
|
|
364
|
+
transfers: ['Funding Transfers'],
|
|
365
|
+
vault: ['Funding Transfers'],
|
|
366
|
+
zone: ['Funding Transfers'],
|
|
362
367
|
})
|
|
363
368
|
|
|
364
369
|
const groups = Object.fromEntries(
|
|
365
370
|
(spec['x-tagGroups'] ?? []).map((group) => [group.name, group.tags]),
|
|
366
371
|
)
|
|
367
372
|
expect(groups['Data API']).toContain('Transfers')
|
|
368
|
-
expect(groups['
|
|
373
|
+
expect(groups['Data API']).not.toContain('Funding Transfers')
|
|
374
|
+
expect(groups['Funding & Bridge API']).toContain('Funding Transfers')
|
|
375
|
+
expect(groups['Funding & Bridge API']).not.toContain('Transfers')
|
|
369
376
|
expect(spec.tags.filter((tag) => tag.name === 'Transfers')).toHaveLength(1)
|
|
377
|
+
expect(spec.tags.filter((tag) => tag.name === 'Funding Transfers')).toEqual([
|
|
378
|
+
{
|
|
379
|
+
description: 'Inbound funding transfers.',
|
|
380
|
+
name: 'Funding Transfers',
|
|
381
|
+
'x-displayName': 'Transfers',
|
|
382
|
+
'x-pagePath': 'funding/transfers',
|
|
383
|
+
},
|
|
384
|
+
])
|
|
385
|
+
expect(
|
|
386
|
+
Object.fromEntries(
|
|
387
|
+
(groups['Funding & Bridge API'] ?? []).map((name) => {
|
|
388
|
+
const tag = spec.tags.find((tag) => tag.name === name)
|
|
389
|
+
return [tag?.['x-displayName'] ?? tag?.name, tag?.['x-pagePath']]
|
|
390
|
+
}),
|
|
391
|
+
),
|
|
392
|
+
).toEqual({
|
|
393
|
+
Chains: 'funding/chains',
|
|
394
|
+
'Deposit Addresses': 'funding/deposit-addresses',
|
|
395
|
+
Providers: 'funding/providers',
|
|
396
|
+
Quotes: 'funding/quotes',
|
|
397
|
+
Transfers: 'funding/transfers',
|
|
398
|
+
})
|
|
370
399
|
})
|
|
371
400
|
})
|
|
372
401
|
|
|
@@ -41,6 +41,24 @@ describe('from', () => {
|
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
describe('ecb', () => {
|
|
44
|
+
test('caches the daily feed for one day', () => {
|
|
45
|
+
expect(FxOracle.ecb({ url: feedUrl('1.25') }).ttl).toMatchInlineSnapshot(`86400000`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('rotates the cache key after the daily publication window', () => {
|
|
49
|
+
vi.useFakeTimers()
|
|
50
|
+
try {
|
|
51
|
+
const oracle = FxOracle.ecb({ url: feedUrl('1.25') })
|
|
52
|
+
|
|
53
|
+
vi.setSystemTime('2026-01-02T15:59:00.000Z')
|
|
54
|
+
expect(oracle.cacheKey?.()).toMatchInlineSnapshot(`"ecb:2026-01-01"`)
|
|
55
|
+
vi.setSystemTime('2026-01-02T16:00:00.000Z')
|
|
56
|
+
expect(oracle.cacheKey?.()).toMatchInlineSnapshot(`"ecb:2026-01-02"`)
|
|
57
|
+
} finally {
|
|
58
|
+
vi.useRealTimers()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
44
62
|
test('fetches and parses the daily reference rates', async () => {
|
|
45
63
|
const rates = await FxOracle.ecb({ url: feedUrl('1.25') }).rates()
|
|
46
64
|
expect(rates).toMatchInlineSnapshot(`
|
|
@@ -4,6 +4,8 @@ import * as Ttl from '../../internal/Ttl.js'
|
|
|
4
4
|
|
|
5
5
|
/** A pluggable fiat FX rate oracle consumed by the valuation endpoint. */
|
|
6
6
|
export type Oracle = {
|
|
7
|
+
/** Returns the cache-key suffix for the oracle's current publication window. */
|
|
8
|
+
cacheKey?: (() => string) | undefined
|
|
7
9
|
/** Oracle identifier, surfaced as valuation rate provenance (e.g. `ecb`). */
|
|
8
10
|
name: string
|
|
9
11
|
/** Fetches the oracle's current rate set. */
|
|
@@ -34,6 +36,7 @@ export const ecbUrl = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily
|
|
|
34
36
|
*/
|
|
35
37
|
export function from(oracle: from.Value): Oracle {
|
|
36
38
|
return {
|
|
39
|
+
...(oracle.cacheKey ? { cacheKey: oracle.cacheKey } : {}),
|
|
37
40
|
name: oracle.name,
|
|
38
41
|
rates: async () => {
|
|
39
42
|
try {
|
|
@@ -48,19 +51,17 @@ export function from(oracle: from.Value): Oracle {
|
|
|
48
51
|
|
|
49
52
|
export declare namespace from {
|
|
50
53
|
/** Oracle definition accepted by {@link from}. */
|
|
51
|
-
type Value = Pick<Oracle, 'name' | 'rates'> & {
|
|
54
|
+
type Value = Pick<Oracle, 'cacheKey' | 'name' | 'rates'> & {
|
|
52
55
|
/** Cache lifetime for a fetched rate set, in milliseconds. @default `Ttl.hours(1)` */
|
|
53
56
|
ttl?: number | undefined
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
/**
|
|
58
|
-
* European Central Bank euro reference rates: one EUR-based set covering ~30
|
|
59
|
-
* currencies, published once per working day.
|
|
60
|
-
*/
|
|
60
|
+
/** European Central Bank reference rates, cached for one day to match their publication cadence. */
|
|
61
61
|
export function ecb(options: ecb.Options = {}): Oracle {
|
|
62
62
|
const { fetch = globalThis.fetch, url = ecbUrl } = options
|
|
63
63
|
return from({
|
|
64
|
+
cacheKey: () => `ecb:${ecbPublication(new Date())}`,
|
|
64
65
|
name: 'ecb',
|
|
65
66
|
rates: async () => {
|
|
66
67
|
const response = await fetch(url, {
|
|
@@ -90,9 +91,31 @@ export function ecb(options: ecb.Options = {}): Oracle {
|
|
|
90
91
|
if (!asOf || rates['USD'] === undefined) throw new ParseError()
|
|
91
92
|
return { asOf: `${asOf}T00:00:00.000Z`, base: 'EUR', rates }
|
|
92
93
|
},
|
|
94
|
+
ttl: Ttl.days(1),
|
|
93
95
|
})
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
const ecbTime = new Intl.DateTimeFormat('en-CA', {
|
|
99
|
+
day: '2-digit',
|
|
100
|
+
hour: '2-digit',
|
|
101
|
+
hourCycle: 'h23',
|
|
102
|
+
month: '2-digit',
|
|
103
|
+
timeZone: 'Europe/Brussels',
|
|
104
|
+
year: 'numeric',
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function ecbPublication(now: Date) {
|
|
108
|
+
const parts = Object.fromEntries(
|
|
109
|
+
ecbTime.formatToParts(now).map((part) => [part.type, part.value]),
|
|
110
|
+
)
|
|
111
|
+
// Rotate one hour after the ECB's usual 16:00 CET publication time.
|
|
112
|
+
const date = Number(parts['hour']) >= 17 ? now : new Date(now.getTime() - Ttl.days(1))
|
|
113
|
+
const publication = Object.fromEntries(
|
|
114
|
+
ecbTime.formatToParts(date).map((part) => [part.type, part.value]),
|
|
115
|
+
)
|
|
116
|
+
return `${publication['year']}-${publication['month']}-${publication['day']}`
|
|
117
|
+
}
|
|
118
|
+
|
|
96
119
|
export declare namespace ecb {
|
|
97
120
|
/** Options for the ECB oracle. */
|
|
98
121
|
type Options = {
|
|
@@ -328,6 +328,25 @@ describe('GET /addresses/:address/activities', () => {
|
|
|
328
328
|
`)
|
|
329
329
|
})
|
|
330
330
|
|
|
331
|
+
test.skipIf(runtime.mode !== 'localnet')('omits activity valuations by default', async () => {
|
|
332
|
+
const response = await client.v1.transactions[':transactionHash'].activities.$get(
|
|
333
|
+
{
|
|
334
|
+
param: { transactionHash: runtime.fixtures!.faucetTransactionHashes[0]! },
|
|
335
|
+
query: {},
|
|
336
|
+
},
|
|
337
|
+
TestApp.auth,
|
|
338
|
+
)
|
|
339
|
+
const body = await TestApp.json(response, Activities.schema.getTransactionActivities.Response)
|
|
340
|
+
expect(response.status).toBe(200)
|
|
341
|
+
|
|
342
|
+
const amounts = body.data.flatMap((item) =>
|
|
343
|
+
item.type === 'transfer' || item.type === 'mint' ? [item.data.sourceAmount] : [],
|
|
344
|
+
)
|
|
345
|
+
expect(amounts.length).toBeGreaterThan(0)
|
|
346
|
+
for (const amount of amounts) expect('valuation' in amount).toBe(false)
|
|
347
|
+
expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=')
|
|
348
|
+
})
|
|
349
|
+
|
|
331
350
|
test('returns every item at the root by default (no group entries)', async () => {
|
|
332
351
|
const body = await activities(accounts.channel)
|
|
333
352
|
expect(body.data.every((entry) => entry.type !== 'group')).toBe(true)
|
|
@@ -1853,7 +1853,7 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
1853
1853
|
const { address } = c.req.valid('param')
|
|
1854
1854
|
const query = c.req.valid('query')
|
|
1855
1855
|
|
|
1856
|
-
// Cursor pages carry current valuations and curated verification data.
|
|
1856
|
+
// Cursor pages can carry current valuations and curated verification data.
|
|
1857
1857
|
if (query.cursor !== undefined) Cache.setPolicy(c, Cache.policies.metadata)
|
|
1858
1858
|
|
|
1859
1859
|
try {
|
|
@@ -1884,7 +1884,7 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
1884
1884
|
// Build immutable token and structured amount copies from RPC metadata.
|
|
1885
1885
|
const { items, rates } = await enrichTokens(c, {
|
|
1886
1886
|
chainId,
|
|
1887
|
-
denomination: query['valuation.currency']
|
|
1887
|
+
denomination: query['valuation.currency'],
|
|
1888
1888
|
include: query.include,
|
|
1889
1889
|
items: unenrichedItems,
|
|
1890
1890
|
oracle,
|
|
@@ -1964,7 +1964,7 @@ export function transactions(options: transactions.Options = {}) {
|
|
|
1964
1964
|
}),
|
|
1965
1965
|
Cache.response<App.Environment>({
|
|
1966
1966
|
// Default to no-store so a 404 does not mask the eventual mined result.
|
|
1967
|
-
// Successful responses use the metadata policy for
|
|
1967
|
+
// Successful responses use the metadata policy for optional valuations.
|
|
1968
1968
|
cacheControl: Cache.policies.noStore,
|
|
1969
1969
|
name: 'tempo-api:transactions:v1',
|
|
1970
1970
|
key: (c) => Cache.urlKey(c, schema.getTransactionActivities.Query),
|
|
@@ -2012,13 +2012,13 @@ export function transactions(options: transactions.Options = {}) {
|
|
|
2012
2012
|
status: 404,
|
|
2013
2013
|
})
|
|
2014
2014
|
|
|
2015
|
-
// Mined activity
|
|
2015
|
+
// Mined activity can carry current valuations and verification data.
|
|
2016
2016
|
Cache.setPolicy(c, Cache.policies.metadata)
|
|
2017
2017
|
|
|
2018
2018
|
// Build immutable token and structured amount copies from RPC metadata.
|
|
2019
2019
|
const { items, rates } = await enrichTokens(c, {
|
|
2020
2020
|
chainId,
|
|
2021
|
-
denomination: query['valuation.currency']
|
|
2021
|
+
denomination: query['valuation.currency'],
|
|
2022
2022
|
include: query.include,
|
|
2023
2023
|
items: unenrichedItems,
|
|
2024
2024
|
oracle,
|
|
@@ -4088,27 +4088,27 @@ async function enrichTokens(
|
|
|
4088
4088
|
const { chainId, denomination, include, items, oracle } = options
|
|
4089
4089
|
const addresses = new Set<Address.Address>()
|
|
4090
4090
|
for (const item of items) collectTokens(item, addresses)
|
|
4091
|
-
|
|
4092
|
-
|
|
4091
|
+
const includeLogoUri = include.includes('token.logoUri')
|
|
4092
|
+
const includeVerified = include.includes('token.verified')
|
|
4093
|
+
// Curated data is needed only for requested valuation or token fields.
|
|
4093
4094
|
const snapshot =
|
|
4094
|
-
addresses.size > 0
|
|
4095
|
+
addresses.size > 0 && (denomination !== undefined || includeLogoUri || includeVerified)
|
|
4095
4096
|
? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
|
|
4096
4097
|
: undefined
|
|
4097
|
-
const rates =
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4098
|
+
const rates = denomination
|
|
4099
|
+
? await Valuation.ratesFor(c, {
|
|
4100
|
+
currencies: snapshot
|
|
4101
|
+
? [...addresses].flatMap((address) => {
|
|
4102
|
+
const held = snapshot.byAddress.get(address)?.currency
|
|
4103
|
+
return held === undefined ? [] : [held]
|
|
4104
|
+
})
|
|
4105
|
+
: [],
|
|
4106
|
+
denomination,
|
|
4107
|
+
oracle,
|
|
4108
|
+
})
|
|
4109
|
+
: undefined
|
|
4107
4110
|
if (addresses.size === 0) return { items: items as Item[], rates }
|
|
4108
4111
|
|
|
4109
|
-
const includeLogoUri = include.includes('token.logoUri')
|
|
4110
|
-
const includeVerified = include.includes('token.verified')
|
|
4111
|
-
|
|
4112
4112
|
const resolved = await Timing.time(c, 'tokens', () =>
|
|
4113
4113
|
Promise.all(
|
|
4114
4114
|
[...addresses].map(async (address) => {
|
|
@@ -4145,7 +4145,7 @@ declare namespace enrichTokens {
|
|
|
4145
4145
|
type Options = {
|
|
4146
4146
|
chainId: z.output<typeof Schema.ChainId>
|
|
4147
4147
|
/** Currency the amount valuations are denominated in (uppercase). */
|
|
4148
|
-
denomination
|
|
4148
|
+
denomination?: string | undefined
|
|
4149
4149
|
include: readonly z.output<typeof schema.Include>[]
|
|
4150
4150
|
items: readonly UnenrichedItem[]
|
|
4151
4151
|
/** FX rate oracle backing amount valuation. */
|
|
@@ -4173,13 +4173,17 @@ function enrichItem(
|
|
|
4173
4173
|
currency: meta.currency,
|
|
4174
4174
|
decimals: meta.decimals,
|
|
4175
4175
|
}),
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4176
|
+
...(valuation.denomination
|
|
4177
|
+
? {
|
|
4178
|
+
// Valued via the curated display currency, never the on-chain metadata.
|
|
4179
|
+
valuation: Valuation.valuationFor({
|
|
4180
|
+
amount: BigInt(t.amount),
|
|
4181
|
+
denomination: valuation.denomination,
|
|
4182
|
+
rates: valuation.rates,
|
|
4183
|
+
token: valuation.snapshot?.byAddress.get(t.address.toLowerCase()),
|
|
4184
|
+
}),
|
|
4185
|
+
}
|
|
4186
|
+
: {}),
|
|
4183
4187
|
}
|
|
4184
4188
|
data[key] = {
|
|
4185
4189
|
address: t.address,
|
|
@@ -4198,7 +4202,7 @@ declare namespace enrichItem {
|
|
|
4198
4202
|
/** Valuation inputs threaded from {@link enrichTokens}. */
|
|
4199
4203
|
type Valuation = {
|
|
4200
4204
|
/** Currency the amount valuations are denominated in (uppercase). */
|
|
4201
|
-
denomination
|
|
4205
|
+
denomination?: string | undefined
|
|
4202
4206
|
/** Oracle rate set, when loaded. */
|
|
4203
4207
|
rates: FxOracle.RateSet | undefined
|
|
4204
4208
|
/** Compiled verified-token snapshot, when available. */
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Schema } from 'tapimo'
|
|
4
4
|
import { FxOracle } from 'tapimo/apps'
|
|
5
5
|
import { Value as core_Value } from 'ox'
|
|
6
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
6
7
|
import { Actions } from 'viem/tempo'
|
|
7
8
|
|
|
8
9
|
import * as TestApp from '../../../../test/App.js'
|
|
@@ -296,8 +297,8 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
296
297
|
currency: 'USD',
|
|
297
298
|
decimals: 6,
|
|
298
299
|
logoUri: 'https://example.com/pathusd.svg',
|
|
299
|
-
name: 'PathUSD',
|
|
300
|
-
symbol: '
|
|
300
|
+
name: 'Curated PathUSD',
|
|
301
|
+
symbol: 'curatedUSD',
|
|
301
302
|
},
|
|
302
303
|
],
|
|
303
304
|
})
|
|
@@ -320,9 +321,49 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
320
321
|
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
321
322
|
expect(response.status).toBe(200)
|
|
322
323
|
expect(body.data.length).toBeGreaterThan(0)
|
|
324
|
+
expect(response.headers.get('server-timing')).not.toContain('token_logo;dur=')
|
|
325
|
+
expect(response.headers.get('server-timing')).not.toContain('token_metadata;dur=')
|
|
323
326
|
for (const balance of body.data) expect(balance.token.verified).toBe(true)
|
|
324
327
|
const pathUsd = body.data.find((balance) => balance.token.address === TestApp.tokenWithHolders)
|
|
325
|
-
expect(pathUsd?.token
|
|
328
|
+
expect(pathUsd?.token).toMatchObject({
|
|
329
|
+
logoUri: 'https://example.com/pathusd.svg',
|
|
330
|
+
name: 'Curated PathUSD',
|
|
331
|
+
symbol: 'curatedUSD',
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('includes uploaded logos for verified tokens without a logo URI', async () => {
|
|
336
|
+
const logoDb = TestApp.database()
|
|
337
|
+
await TestApp.verifiedSeed(logoDb, runtime.chainId, {
|
|
338
|
+
tokens: [
|
|
339
|
+
{
|
|
340
|
+
address: TestApp.tokenWithHolders,
|
|
341
|
+
currency: 'USD',
|
|
342
|
+
decimals: 6,
|
|
343
|
+
name: 'Curated PathUSD',
|
|
344
|
+
symbol: 'curatedUSD',
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
})
|
|
348
|
+
const client = app({ db: logoDb })
|
|
349
|
+
const holder = await holderWithBalance
|
|
350
|
+
|
|
351
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
352
|
+
{
|
|
353
|
+
param: { address: holder },
|
|
354
|
+
query: { limit: '20', verified: 'true' },
|
|
355
|
+
},
|
|
356
|
+
TestApp.auth,
|
|
357
|
+
)
|
|
358
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
359
|
+
const pathUsd = body.data.find((balance) => balance.token.address === TestApp.tokenWithHolders)
|
|
360
|
+
|
|
361
|
+
expect(response.status).toBe(200)
|
|
362
|
+
expect(response.headers.get('server-timing')).toContain('token_logo;dur=')
|
|
363
|
+
expect(response.headers.get('server-timing')).not.toContain('token_metadata;dur=')
|
|
364
|
+
expect(pathUsd?.token.logoUri).toBe(
|
|
365
|
+
`http://localhost/assets/${runtime.chainId}/icons/${TestApp.tokenWithHolders}`,
|
|
366
|
+
)
|
|
326
367
|
})
|
|
327
368
|
|
|
328
369
|
test('narrows balances by currency and retains verification', async () => {
|
|
@@ -359,7 +400,23 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
359
400
|
for (const balance of body.data) expect(typeof balance.feeEligible).toBe('boolean')
|
|
360
401
|
})
|
|
361
402
|
|
|
362
|
-
test('
|
|
403
|
+
test('skips the fee-token lookup when an address has no holdings', async () => {
|
|
404
|
+
const client = app()
|
|
405
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
406
|
+
{
|
|
407
|
+
param: { address: privateKeyToAccount(generatePrivateKey()).address },
|
|
408
|
+
query: {},
|
|
409
|
+
},
|
|
410
|
+
TestApp.auth,
|
|
411
|
+
)
|
|
412
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
413
|
+
|
|
414
|
+
expect(response.status).toBe(200)
|
|
415
|
+
expect(body.data).toEqual([])
|
|
416
|
+
expect(response.headers.get('server-timing')).not.toContain('fee_token_set;dur=')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
test('omits valuations unless a denomination is requested', async () => {
|
|
363
420
|
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
364
421
|
const client = app()
|
|
365
422
|
const holder = await holderWithBalance
|
|
@@ -370,12 +427,8 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
370
427
|
)
|
|
371
428
|
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
372
429
|
expect(response.status).toBe(200)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// holding itself; unverified holdings are never valued.
|
|
376
|
-
const pathUsd = body.data.find((entry) => entry.token.address === TestApp.tokenWithHolders)
|
|
377
|
-
expect(pathUsd?.valuation).toEqual({ amount: pathUsd!.formatted, currency: 'USD' })
|
|
378
|
-
for (const entry of body.data) if (!entry.token.verified) expect(entry.valuation).toBeNull()
|
|
430
|
+
expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=')
|
|
431
|
+
for (const entry of body.data) expect('valuation' in entry).toBe(false)
|
|
379
432
|
})
|
|
380
433
|
|
|
381
434
|
test('values holdings in a requested denomination', async () => {
|
|
@@ -443,6 +496,26 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
443
496
|
expect(stable.error.code).toMatchInlineSnapshot(`"query_invalid"`)
|
|
444
497
|
})
|
|
445
498
|
|
|
499
|
+
test('rejects unsupported denominations before balance RPC failures', async () => {
|
|
500
|
+
const client = app({
|
|
501
|
+
fx: { oracle: fixed },
|
|
502
|
+
rpc: { url: () => 'http://127.0.0.1:1' },
|
|
503
|
+
})
|
|
504
|
+
const holder = await holderWithBalance
|
|
505
|
+
|
|
506
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
507
|
+
{
|
|
508
|
+
param: { address: holder },
|
|
509
|
+
query: { 'valuation.currency': 'JPY' },
|
|
510
|
+
},
|
|
511
|
+
TestApp.auth,
|
|
512
|
+
)
|
|
513
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
514
|
+
|
|
515
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
516
|
+
expect(body.error.code).toMatchInlineSnapshot(`"query_invalid"`)
|
|
517
|
+
})
|
|
518
|
+
|
|
446
519
|
test('restricts the page to fee tokens when feeEligible=true', async () => {
|
|
447
520
|
const client = app()
|
|
448
521
|
const holder = await holderWithBalance
|
|
@@ -469,3 +542,81 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
469
542
|
expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
|
|
470
543
|
})
|
|
471
544
|
})
|
|
545
|
+
|
|
546
|
+
describe('GET /v1/addresses/:address/balances/:token', () => {
|
|
547
|
+
test('returns one live balance without indexed holder discovery', async () => {
|
|
548
|
+
const client = app()
|
|
549
|
+
const holder = await holderWithBalance
|
|
550
|
+
const response = await client.v1.addresses[':address'].balances[':token'].$get(
|
|
551
|
+
{
|
|
552
|
+
param: { address: holder, token: TestApp.tokenWithHolders },
|
|
553
|
+
query: {},
|
|
554
|
+
},
|
|
555
|
+
TestApp.auth,
|
|
556
|
+
)
|
|
557
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response)
|
|
558
|
+
|
|
559
|
+
expect(response.status).toBe(200)
|
|
560
|
+
expect(body.id).toBe(TestApp.tokenWithHolders)
|
|
561
|
+
expect(body.token.address).toBe(TestApp.tokenWithHolders)
|
|
562
|
+
expect(body.amount).toMatch(/^\d+$/)
|
|
563
|
+
expect(core_Value.from(body.formatted, body.decimals)).toBe(BigInt(body.amount))
|
|
564
|
+
expect('valuation' in body).toBe(false)
|
|
565
|
+
expect(response.headers.get('server-timing')).not.toContain('valuation_rates;dur=')
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
test('values one balance when a denomination is requested', async () => {
|
|
569
|
+
const client = app()
|
|
570
|
+
const holder = await holderWithBalance
|
|
571
|
+
const response = await client.v1.addresses[':address'].balances[':token'].$get(
|
|
572
|
+
{
|
|
573
|
+
param: { address: holder, token: TestApp.tokenWithHolders },
|
|
574
|
+
query: { 'valuation.currency': 'USD' },
|
|
575
|
+
},
|
|
576
|
+
TestApp.auth,
|
|
577
|
+
)
|
|
578
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response)
|
|
579
|
+
|
|
580
|
+
expect(response.status).toBe(200)
|
|
581
|
+
expect(body.valuation).toEqual({ amount: body.formatted, currency: 'USD' })
|
|
582
|
+
expect(response.headers.get('server-timing')).toContain('valuation_rates;dur=')
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
test('rejects an unsupported denomination before a balance RPC failure', async () => {
|
|
586
|
+
const client = app({
|
|
587
|
+
fx: { oracle: fixed },
|
|
588
|
+
rpc: { url: () => 'http://127.0.0.1:1' },
|
|
589
|
+
})
|
|
590
|
+
const holder = await holderWithBalance
|
|
591
|
+
const response = await client.v1.addresses[':address'].balances[':token'].$get(
|
|
592
|
+
{
|
|
593
|
+
param: { address: holder, token: TestApp.tokenWithHolders },
|
|
594
|
+
query: { 'valuation.currency': 'JPY' },
|
|
595
|
+
},
|
|
596
|
+
TestApp.auth,
|
|
597
|
+
)
|
|
598
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
599
|
+
|
|
600
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
601
|
+
expect(body.error.code).toMatchInlineSnapshot(`"query_invalid"`)
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
test('returns zero for a token the account does not hold', async () => {
|
|
605
|
+
const client = app()
|
|
606
|
+
const response = await client.v1.addresses[':address'].balances[':token'].$get(
|
|
607
|
+
{
|
|
608
|
+
param: {
|
|
609
|
+
address: privateKeyToAccount(generatePrivateKey()).address,
|
|
610
|
+
token: TestApp.tokenWithHolders,
|
|
611
|
+
},
|
|
612
|
+
query: {},
|
|
613
|
+
},
|
|
614
|
+
TestApp.auth,
|
|
615
|
+
)
|
|
616
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response)
|
|
617
|
+
|
|
618
|
+
expect(response.status).toBe(200)
|
|
619
|
+
expect(body.amount).toBe('0')
|
|
620
|
+
expect(body.formatted).toBe('0')
|
|
621
|
+
})
|
|
622
|
+
})
|