tapimo 0.9.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 +4887 -5074
- package/dist/App.d.ts.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 +168 -168
- 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 +5198 -5099
- 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 +270 -270
- 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 +52 -52
- package/dist/apps/data/routes/balances.d.ts +36 -36
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +135 -81
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +125 -125
- package/dist/apps/data/routes/coingecko.d.ts +54 -54
- package/dist/apps/data/routes/earn.d.ts +512 -58
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +368 -44
- 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/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokens.d.ts +461 -459
- 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 +108 -86
- 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 +96 -96
- package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
- package/dist/apps/funding/routes/quotes.d.ts +89 -89
- package/dist/apps/funding/routes/transfers.d.ts +4 -4
- 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/invitations.d.ts +134 -134
- 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/orgs.d.ts +98 -98
- 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/verified-token-requests.d.ts +83 -83
- 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 +56 -44
- 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 +17 -15
- package/dist/db/Db.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/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 +4 -0
- 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 +128 -0
- 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 +12 -1
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.test-d.ts +60 -5
- 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/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 +119 -10
- package/src/apps/data/routes/balances.ts +154 -95
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/earn.test.ts +197 -0
- package/src/apps/data/routes/earn.ts +486 -44
- 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/routes/quotes.test.ts +1 -1
- 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 +33 -19
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookSubscriptions.ts +116 -4
- 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 +12 -7
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1240 -0
- package/src/internal/funding/Reconciliation.ts +136 -0
- package/src/internal/funding/SourceObservation.test.ts +1 -0
- package/src/internal/funding/providers/relay.test.ts +1 -4
- package/src/internal/funding/providers/relay.ts +16 -1
|
@@ -66,7 +66,12 @@ export namespace schema {
|
|
|
66
66
|
include: Schema.totalCountInclude,
|
|
67
67
|
limit: Schema.Limit,
|
|
68
68
|
page: Schema.Page,
|
|
69
|
-
'valuation.currency': Schema.Denomination
|
|
69
|
+
'valuation.currency': Schema.Denomination.check(
|
|
70
|
+
z.describe(
|
|
71
|
+
'When present, include each holding’s nominal value in this denomination. ' +
|
|
72
|
+
'Case-insensitive and must be priced by the configured FX oracle.',
|
|
73
|
+
),
|
|
74
|
+
),
|
|
70
75
|
verified: z
|
|
71
76
|
.optional(Schema.booleanQuery())
|
|
72
77
|
.check(
|
|
@@ -141,11 +146,11 @@ export namespace schema {
|
|
|
141
146
|
),
|
|
142
147
|
token: Tokens.schema.Token,
|
|
143
148
|
valuation: z
|
|
144
|
-
.nullable(Valuation.schema.Value)
|
|
149
|
+
.optional(z.nullable(Valuation.schema.Value))
|
|
145
150
|
.check(
|
|
146
151
|
z.describe(
|
|
147
|
-
'The holding’s nominal value
|
|
148
|
-
'token is unverified, its display currency has no rate, or rates were unavailable.',
|
|
152
|
+
'The holding’s nominal value when `valuation.currency` is requested, or `null` when ' +
|
|
153
|
+
'the token is unverified, its display currency has no rate, or rates were unavailable.',
|
|
149
154
|
),
|
|
150
155
|
),
|
|
151
156
|
}),
|
|
@@ -198,7 +203,12 @@ export namespace schema {
|
|
|
198
203
|
export const Query = z
|
|
199
204
|
.strictObject({
|
|
200
205
|
chainId: Schema.ChainIdQuery,
|
|
201
|
-
'valuation.currency': Schema.Denomination
|
|
206
|
+
'valuation.currency': Schema.Denomination.check(
|
|
207
|
+
z.describe(
|
|
208
|
+
'When present, include the holding’s nominal value in this denomination. ' +
|
|
209
|
+
'Case-insensitive and must be priced by the configured FX oracle.',
|
|
210
|
+
),
|
|
211
|
+
),
|
|
202
212
|
})
|
|
203
213
|
.check(z.describe('Query parameters for reading one account token balance.'))
|
|
204
214
|
|
|
@@ -229,7 +239,7 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
229
239
|
}),
|
|
230
240
|
OpenApi.describeRoute({
|
|
231
241
|
description:
|
|
232
|
-
'Lists how much of each TIP-20 token an account holds, ordered from largest to smallest balance. Amounts are returned in raw base units and human-readable form
|
|
242
|
+
'Lists how much of each TIP-20 token an account holds, ordered from largest to smallest balance. Amounts are returned in raw base units and human-readable form. Pass `valuation.currency` to include nominal values.',
|
|
233
243
|
operationId: 'getAddressBalances',
|
|
234
244
|
responses: OpenApi.responses({
|
|
235
245
|
errors: { 502: 'Could not read balance or token data from an upstream service.' },
|
|
@@ -269,26 +279,13 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
269
279
|
const verified = query.verified || query.currency !== undefined
|
|
270
280
|
const feeEligible = query.feeEligible ?? false
|
|
271
281
|
|
|
272
|
-
// The fee-token set powers both the per-row `feeEligible` flag (always)
|
|
273
|
-
// and the optional `feeEligible=true` page restriction. It is memoized
|
|
274
|
-
// per chain (a near-constant), so one read feeds the page, the count,
|
|
275
|
-
// and the row enrichment. It is a best-effort enrichment: a lookup
|
|
276
|
-
// failure omits `feeEligible` rather than failing the page — unless the
|
|
277
|
-
// caller explicitly asked to filter by `feeEligible`, where the filter
|
|
278
|
-
// can't be honored, so the failure surfaces as the declared `502`.
|
|
279
|
-
const feeTokens = await FeeAmm.feeTokenSet(c, chainId).catch((cause) => {
|
|
280
|
-
if (feeEligible) throw cause
|
|
281
|
-
return undefined
|
|
282
|
-
})
|
|
283
|
-
|
|
284
282
|
const balances = await getAddressBalances(c, {
|
|
285
283
|
address,
|
|
286
284
|
chainId,
|
|
287
285
|
currency: query.currency,
|
|
288
286
|
cursor: query.cursor,
|
|
289
|
-
denomination: query['valuation.currency']
|
|
287
|
+
denomination: query['valuation.currency'],
|
|
290
288
|
feeEligible,
|
|
291
|
-
feeTokens,
|
|
292
289
|
includeTotalCount: query.include.includes('totalCount'),
|
|
293
290
|
limit: query.limit,
|
|
294
291
|
oracle,
|
|
@@ -356,10 +353,28 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
356
353
|
const { address, token } = c.req.valid('param')
|
|
357
354
|
const query = c.req.valid('query')
|
|
358
355
|
const chainId = query.chainId ?? c.get('chainId')
|
|
356
|
+
const denomination = query['valuation.currency']
|
|
359
357
|
|
|
360
358
|
try {
|
|
361
|
-
const
|
|
362
|
-
|
|
359
|
+
const rates = denomination
|
|
360
|
+
? await Valuation.rateSet(c, oracle)
|
|
361
|
+
.then((rates) => {
|
|
362
|
+
if (denomination !== rates.base && rates.rates[denomination] === undefined)
|
|
363
|
+
throw new Valuation.UnsupportedDenominationError(denomination, oracle.name)
|
|
364
|
+
return rates
|
|
365
|
+
})
|
|
366
|
+
.catch((cause) => {
|
|
367
|
+
if (cause instanceof Valuation.UnsupportedDenominationError) throw cause
|
|
368
|
+
return undefined
|
|
369
|
+
})
|
|
370
|
+
: undefined
|
|
371
|
+
const snapshotPromise = VerifiedTokens.snapshot(c, chainId)
|
|
372
|
+
const resolvedTokenPromise = snapshotPromise.then((snapshot) =>
|
|
373
|
+
Timing.time(c, 'token', () =>
|
|
374
|
+
Tokens.resolveToken(c, { address: token, chainId, include: [], snapshot }),
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
const [balance, feeTokens, snapshot, resolvedToken] = await Promise.all([
|
|
363
378
|
Timing.time(c, 'balance_rpc', async () => {
|
|
364
379
|
const result = await Actions.token.getBalance(c.get('getClient')(chainId), {
|
|
365
380
|
account: address,
|
|
@@ -367,18 +382,10 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
367
382
|
})
|
|
368
383
|
return result.amount
|
|
369
384
|
}),
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
385
|
+
FeeAmm.feeTokenSet(c, chainId).catch(() => undefined),
|
|
386
|
+
snapshotPromise,
|
|
387
|
+
resolvedTokenPromise,
|
|
373
388
|
])
|
|
374
|
-
const denomination = query['valuation.currency'] ?? 'USD'
|
|
375
|
-
const rates = await Valuation.ratesFor(c, {
|
|
376
|
-
currencies: snapshot.byAddress.has(token) ? [resolvedToken.currency] : [],
|
|
377
|
-
denomination,
|
|
378
|
-
oracle,
|
|
379
|
-
})
|
|
380
|
-
const feeTokens = await FeeAmm.feeTokenSet(c, chainId).catch(() => undefined)
|
|
381
|
-
|
|
382
389
|
return c.json(
|
|
383
390
|
Response.validated(schema.getAddressBalance.Response, {
|
|
384
391
|
amount: balance.toString(),
|
|
@@ -388,12 +395,16 @@ export function addresses(options: addresses.Options = {}) {
|
|
|
388
395
|
formatted: core_Value.format(balance, resolvedToken.decimals),
|
|
389
396
|
id: token,
|
|
390
397
|
token: resolvedToken,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
398
|
+
...(denomination
|
|
399
|
+
? {
|
|
400
|
+
valuation: Valuation.valuationFor({
|
|
401
|
+
amount: balance,
|
|
402
|
+
denomination,
|
|
403
|
+
rates,
|
|
404
|
+
token: snapshot.byAddress.get(token),
|
|
405
|
+
}),
|
|
406
|
+
}
|
|
407
|
+
: {}),
|
|
397
408
|
}),
|
|
398
409
|
200,
|
|
399
410
|
)
|
|
@@ -430,7 +441,6 @@ async function getAddressBalances(
|
|
|
430
441
|
currency,
|
|
431
442
|
denomination,
|
|
432
443
|
feeEligible,
|
|
433
|
-
feeTokens,
|
|
434
444
|
includeTotalCount,
|
|
435
445
|
limit,
|
|
436
446
|
oracle,
|
|
@@ -440,8 +450,9 @@ async function getAddressBalances(
|
|
|
440
450
|
const cursor = options.cursor ? Cursor.decode(options.cursor, ['uint', 'address']) : undefined
|
|
441
451
|
const offset = options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : 0
|
|
442
452
|
|
|
443
|
-
// Validate
|
|
444
|
-
|
|
453
|
+
// Validate an explicit denomination before balance discovery and correction
|
|
454
|
+
// so dependency failures cannot mask a caller error.
|
|
455
|
+
if (denomination) await Valuation.ratesFor(c, { currencies: [], denomination, oracle })
|
|
445
456
|
|
|
446
457
|
// Build the optional `token IN (…)` restriction before hitting TIDX. The
|
|
447
458
|
// verified/`currency` filter and the `feeEligible` filter each contribute a set
|
|
@@ -450,17 +461,32 @@ async function getAddressBalances(
|
|
|
450
461
|
// intersection. If any active filter rules every token out, return an empty
|
|
451
462
|
// page without a round trip. The snapshot is reused for the page enrichment
|
|
452
463
|
// below.
|
|
453
|
-
const
|
|
454
|
-
|
|
464
|
+
const snapshotPromise = VerifiedTokens.snapshot(c, chainId)
|
|
465
|
+
// Fee filtering can fail before the concurrently started snapshot is awaited.
|
|
466
|
+
// Observing this branch prevents an unhandled rejection; the later await still propagates it.
|
|
467
|
+
void snapshotPromise.catch(() => undefined)
|
|
468
|
+
const [snapshot_filter, feeTokens_filter] = await Promise.all([
|
|
469
|
+
verified ? snapshotPromise : Promise.resolve(undefined),
|
|
470
|
+
feeEligible ? FeeAmm.feeTokenSet(c, chainId) : Promise.resolve(undefined),
|
|
471
|
+
])
|
|
472
|
+
const allowed = allowedTokens({
|
|
473
|
+
currency,
|
|
474
|
+
feeEligible,
|
|
475
|
+
feeTokens: feeTokens_filter,
|
|
476
|
+
snapshot: snapshot_filter,
|
|
477
|
+
verified,
|
|
478
|
+
})
|
|
455
479
|
const tokenFilter = allowed?.map((address) => `'${address}'`)
|
|
456
|
-
if (tokenFilter?.length === 0)
|
|
480
|
+
if (tokenFilter?.length === 0) {
|
|
481
|
+
await snapshotPromise
|
|
457
482
|
return {
|
|
458
483
|
data: [],
|
|
459
484
|
...(includeTotalCount ? { meta: { totalCount: 0, totalCountCapped: false } } : {}),
|
|
460
485
|
nextCursor: null,
|
|
461
486
|
}
|
|
487
|
+
}
|
|
462
488
|
|
|
463
|
-
const
|
|
489
|
+
const holdingsPromise = Timing.time(c, 'balances_discovery', async () => {
|
|
464
490
|
const tokenIn = tokenFilter ? ` AND token IN (${tokenFilter.join(', ')})` : ''
|
|
465
491
|
|
|
466
492
|
const result = await tidx.fetch({
|
|
@@ -481,36 +507,54 @@ async function getAddressBalances(
|
|
|
481
507
|
})
|
|
482
508
|
})
|
|
483
509
|
|
|
484
|
-
|
|
510
|
+
// Correction starts as soon as discovery completes. Snapshot and fee-token
|
|
511
|
+
// cache misses continue concurrently until enrichment needs their results.
|
|
512
|
+
const correctedPromise = holdingsPromise.then(async (holdings) => {
|
|
513
|
+
if (holdings.length === 0) return []
|
|
514
|
+
const client = c.get('getClient')(chainId)
|
|
515
|
+
return Timing.time(c, 'balances_rpc', async () => {
|
|
516
|
+
// Concurrent reads enter viem's shared batch window and become one
|
|
517
|
+
// deployless multicall. A failed read fails the request rather than serving
|
|
518
|
+
// a stale or partial balance page.
|
|
519
|
+
const results = await Promise.all(
|
|
520
|
+
holdings.map(async (holding) => {
|
|
521
|
+
const { amount } = await Actions.token.getBalance(client, {
|
|
522
|
+
account: address,
|
|
523
|
+
token: holding.address,
|
|
524
|
+
})
|
|
525
|
+
return amount === 0n ? undefined : { address: holding.address, amount: amount.toString() }
|
|
526
|
+
}),
|
|
527
|
+
)
|
|
528
|
+
const rows = results.filter((holding) => holding !== undefined)
|
|
529
|
+
|
|
530
|
+
rows.sort((a, b) => {
|
|
531
|
+
const delta = BigInt(b.amount) - BigInt(a.amount)
|
|
532
|
+
if (delta !== 0n) return delta > 0n ? 1 : -1
|
|
533
|
+
return a.address < b.address ? -1 : a.address > b.address ? 1 : 0
|
|
534
|
+
})
|
|
535
|
+
return rows
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
const feeTokensPromise = feeEligible
|
|
539
|
+
? Promise.resolve(feeTokens_filter)
|
|
540
|
+
: holdingsPromise.then((holdings) =>
|
|
541
|
+
holdings.length === 0 ? undefined : FeeAmm.feeTokenSet(c, chainId).catch(() => undefined),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const [holdings, snapshot, feeTokens, corrected] = await Promise.all([
|
|
545
|
+
holdingsPromise,
|
|
546
|
+
snapshotPromise,
|
|
547
|
+
feeTokensPromise,
|
|
548
|
+
correctedPromise,
|
|
549
|
+
])
|
|
550
|
+
|
|
551
|
+
if (holdings.length === 0) {
|
|
485
552
|
return {
|
|
486
553
|
data: [],
|
|
487
554
|
...(includeTotalCount ? { meta: { totalCount: 0, totalCountCapped: false } } : {}),
|
|
488
555
|
nextCursor: null,
|
|
489
556
|
}
|
|
490
|
-
|
|
491
|
-
const client = c.get('getClient')(chainId)
|
|
492
|
-
const corrected = await Timing.time(c, 'balances_rpc', async () => {
|
|
493
|
-
// Concurrent reads enter viem's shared batch window and become one
|
|
494
|
-
// deployless multicall. A failed read fails the request rather than serving
|
|
495
|
-
// a stale or partial balance page.
|
|
496
|
-
const results = await Promise.all(
|
|
497
|
-
holdings.map(async (holding) => {
|
|
498
|
-
const { amount } = await Actions.token.getBalance(client, {
|
|
499
|
-
account: address,
|
|
500
|
-
token: holding.address,
|
|
501
|
-
})
|
|
502
|
-
return amount === 0n ? undefined : { address: holding.address, amount: amount.toString() }
|
|
503
|
-
}),
|
|
504
|
-
)
|
|
505
|
-
const rows = results.filter((holding) => holding !== undefined)
|
|
506
|
-
|
|
507
|
-
rows.sort((a, b) => {
|
|
508
|
-
const delta = BigInt(b.amount) - BigInt(a.amount)
|
|
509
|
-
if (delta !== 0n) return delta > 0n ? 1 : -1
|
|
510
|
-
return a.address < b.address ? -1 : a.address > b.address ? 1 : 0
|
|
511
|
-
})
|
|
512
|
-
return rows
|
|
513
|
-
})
|
|
557
|
+
}
|
|
514
558
|
|
|
515
559
|
const positioned = (() => {
|
|
516
560
|
if (!cursor) return corrected.slice(offset)
|
|
@@ -528,7 +572,7 @@ async function getAddressBalances(
|
|
|
528
572
|
key: (holding) => [holding.amount, holding.address],
|
|
529
573
|
})
|
|
530
574
|
|
|
531
|
-
if (page.rows.length === 0)
|
|
575
|
+
if (page.rows.length === 0) {
|
|
532
576
|
return {
|
|
533
577
|
data: [],
|
|
534
578
|
...(includeTotalCount
|
|
@@ -536,22 +580,38 @@ async function getAddressBalances(
|
|
|
536
580
|
: {}),
|
|
537
581
|
nextCursor: page.nextCursor,
|
|
538
582
|
}
|
|
583
|
+
}
|
|
539
584
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
585
|
+
const ratesPromise = denomination
|
|
586
|
+
? Valuation.ratesFor(c, {
|
|
587
|
+
currencies: page.rows.flatMap((row) => {
|
|
588
|
+
const held = snapshot.byAddress.get(row.address.toLowerCase())?.currency
|
|
589
|
+
return held === undefined ? [] : [held]
|
|
590
|
+
}),
|
|
591
|
+
denomination,
|
|
592
|
+
oracle,
|
|
593
|
+
})
|
|
594
|
+
: Promise.resolve(undefined)
|
|
548
595
|
|
|
549
596
|
// Balance tokens retain their logo and verification metadata by default.
|
|
550
|
-
const
|
|
597
|
+
const resolvedPromise = Timing.time(c, 'tokens', () =>
|
|
551
598
|
Promise.all(
|
|
552
599
|
page.rows.map(async (holding) => {
|
|
553
600
|
try {
|
|
554
601
|
const token = Schema.Address.parse(holding.address.toLowerCase())
|
|
602
|
+
const verifiedToken = snapshot.byAddress.get(token)
|
|
603
|
+
// The curated list owns verified-token metadata, but uploaded logos live only in the asset store.
|
|
604
|
+
if (verifiedToken)
|
|
605
|
+
return {
|
|
606
|
+
amount: holding.amount,
|
|
607
|
+
logoUri:
|
|
608
|
+
verifiedToken.logoUri ??
|
|
609
|
+
(await Timing.time(c, 'token_logo', () =>
|
|
610
|
+
Tokens.getTokenLogo(c, { address: token, chainId }),
|
|
611
|
+
).catch(() => undefined)),
|
|
612
|
+
metadata: verifiedToken,
|
|
613
|
+
token,
|
|
614
|
+
}
|
|
555
615
|
const [logoUri, metadata] = await Promise.all([
|
|
556
616
|
Timing.time(c, 'token_logo', () =>
|
|
557
617
|
Tokens.getTokenLogo(c, { address: token, chainId }),
|
|
@@ -567,6 +627,7 @@ async function getAddressBalances(
|
|
|
567
627
|
}),
|
|
568
628
|
),
|
|
569
629
|
)
|
|
630
|
+
const [rates, resolved] = await Promise.all([ratesPromise, resolvedPromise])
|
|
570
631
|
|
|
571
632
|
const data: z.output<typeof schema.getAddressBalances.Balance>[] = []
|
|
572
633
|
for (const entry of resolved) {
|
|
@@ -591,12 +652,16 @@ async function getAddressBalances(
|
|
|
591
652
|
symbol: entry.metadata.symbol,
|
|
592
653
|
verified: snapshot.byAddress.has(entry.token),
|
|
593
654
|
},
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
655
|
+
...(denomination
|
|
656
|
+
? {
|
|
657
|
+
valuation: Valuation.valuationFor({
|
|
658
|
+
amount: BigInt(entry.amount),
|
|
659
|
+
denomination,
|
|
660
|
+
rates,
|
|
661
|
+
token: snapshot.byAddress.get(entry.token),
|
|
662
|
+
}),
|
|
663
|
+
}
|
|
664
|
+
: {}),
|
|
600
665
|
})
|
|
601
666
|
}
|
|
602
667
|
|
|
@@ -620,15 +685,9 @@ declare namespace getAddressBalances {
|
|
|
620
685
|
/** Opaque keyset cursor anchoring the page; omit for the head page. */
|
|
621
686
|
cursor?: string | undefined
|
|
622
687
|
/** Currency the per-row valuations are denominated in (uppercase). */
|
|
623
|
-
denomination
|
|
624
|
-
/** When true, restrict the page to fee-usable tokens
|
|
688
|
+
denomination?: string | undefined
|
|
689
|
+
/** When true, restrict the page to fee-usable tokens. */
|
|
625
690
|
feeEligible?: boolean | undefined
|
|
626
|
-
/**
|
|
627
|
-
* The chain's fee-token set, if resolved (best-effort): flags each row's
|
|
628
|
-
* `feeEligible` and backs the `feeEligible` filter. Undefined when the lookup
|
|
629
|
-
* failed, in which case `feeEligible` is omitted from rows.
|
|
630
|
-
*/
|
|
631
|
-
feeTokens?: ReadonlySet<string> | undefined
|
|
632
691
|
/** When true, include the corrected candidate count in response metadata. */
|
|
633
692
|
includeTotalCount: boolean
|
|
634
693
|
limit: number
|
|
@@ -232,20 +232,26 @@ describe('GET /blocks', () => {
|
|
|
232
232
|
|
|
233
233
|
describe('GET /blocks/:block (ISO 8601 timestamp)', () => {
|
|
234
234
|
test('resolves an ISO 8601 timestamp to the first block at or after it', async () => {
|
|
235
|
-
expect(recent).toBeDefined()
|
|
236
|
-
const recentBlock = await Viem.getClient().getBlock({ blockNumber: recent!.number })
|
|
237
|
-
const seconds = Number(recentBlock.timestamp)
|
|
238
|
-
const iso = new Date(seconds * 1000).toISOString()
|
|
239
235
|
const client = TestApp.client()
|
|
236
|
+
// Seed from TIDX rather than the RPC head, which can advance beyond the
|
|
237
|
+
// indexer's timestamp lookup watermark.
|
|
238
|
+
const blocksResponse = await client.v1.blocks.$get({ query: { limit: '5' } }, TestApp.auth)
|
|
239
|
+
const blocks = await TestApp.json(blocksResponse, Blocks.schema.getBlocks.Response)
|
|
240
|
+
const indexed = blocks.data[0]
|
|
241
|
+
expect(blocksResponse.status).toBe(200)
|
|
242
|
+
expect(indexed).toBeDefined()
|
|
243
|
+
|
|
240
244
|
const response = await client.v1.blocks[':block'].$get(
|
|
241
|
-
{ param: { block:
|
|
245
|
+
{ param: { block: indexed!.timestamp }, query: {} },
|
|
242
246
|
TestApp.auth,
|
|
243
247
|
)
|
|
244
248
|
const body = await TestApp.json(response, Blocks.schema.getBlock.Response)
|
|
245
249
|
expect(response.status).toBe(200)
|
|
246
250
|
expect(typeof body.number).toBe('number')
|
|
247
251
|
// The returned block's timestamp must be >= the requested timestamp.
|
|
248
|
-
expect(new Date(body.timestamp).getTime()
|
|
252
|
+
expect(new Date(body.timestamp).getTime()).toBeGreaterThanOrEqual(
|
|
253
|
+
new Date(indexed!.timestamp).getTime(),
|
|
254
|
+
)
|
|
249
255
|
})
|
|
250
256
|
|
|
251
257
|
test('returns 404 for a far-future timestamp with no indexed block', async () => {
|
|
@@ -88,6 +88,33 @@ describe('schema.getEarnVaults.Query', () => {
|
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
describe('schema.getEarnAddressPositions.Query', () => {
|
|
92
|
+
test('parses collection filters', () => {
|
|
93
|
+
const query = Earn.schema.getEarnAddressPositions.Query.parse({
|
|
94
|
+
limit: '5',
|
|
95
|
+
'valuation.currency': 'usd',
|
|
96
|
+
verified: 'true',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(query.limit).toBe(5)
|
|
100
|
+
expect(query['valuation.currency']).toBe('USD')
|
|
101
|
+
expect(query.verified).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('defaults the page size', () => {
|
|
105
|
+
expect(Earn.schema.getEarnAddressPositions.Query.parse({}).limit).toBe(10)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('rejects vault-collection filters', () => {
|
|
109
|
+
expect(() =>
|
|
110
|
+
Earn.schema.getEarnAddressPositions.Query.parse({ capability: 'deposit' }),
|
|
111
|
+
).toThrow()
|
|
112
|
+
expect(() =>
|
|
113
|
+
Earn.schema.getEarnAddressPositions.Query.parse({ asset: `0x${'aa'.repeat(20)}` }),
|
|
114
|
+
).toThrow()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
91
118
|
describe('schema.getEarnVaultEarnings.Response', () => {
|
|
92
119
|
test('enforces status-specific earnings fields', () => {
|
|
93
120
|
const response = {
|
|
@@ -540,6 +567,144 @@ describe('GET /v1/earn/vaults/verified', () => {
|
|
|
540
567
|
)
|
|
541
568
|
})
|
|
542
569
|
|
|
570
|
+
describe('GET /v1/earn/addresses/:address/positions', () => {
|
|
571
|
+
test('returns chain selection errors', async () => {
|
|
572
|
+
const app = TestApp.create({ auth: false, cache })
|
|
573
|
+
const invalid = await app.request(
|
|
574
|
+
`/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=abc`,
|
|
575
|
+
)
|
|
576
|
+
const unsupported = await app.request(
|
|
577
|
+
`/v1/earn/addresses/${TestEarn.positionAddress}/positions?chainId=999999`,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
expect([
|
|
581
|
+
(await TestApp.json(invalid, Schema.ErrorResponse)).error.code,
|
|
582
|
+
(await TestApp.json(unsupported, Schema.ErrorResponse)).error.code,
|
|
583
|
+
]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported'])
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
test('rejects a malformed account address', async () => {
|
|
587
|
+
const response = await TestApp.create({ auth: false, cache }).request(
|
|
588
|
+
'/v1/earn/addresses/not-an-address/positions',
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
expect(response.status).toBe(400)
|
|
592
|
+
expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({
|
|
593
|
+
error: { code: 'positions_invalid' },
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
test('returns an empty verified page without curated vaults', async () => {
|
|
598
|
+
const app = TestApp.create({ auth: false, cache })
|
|
599
|
+
const response = await app.request(
|
|
600
|
+
`/v1/earn/addresses/${TestEarn.positionAddress}/positions?verified=true`,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
expect(response.status).toBe(200)
|
|
604
|
+
expect(
|
|
605
|
+
await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response),
|
|
606
|
+
).toStrictEqual({ data: [], nextCursor: null })
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
test.runIf(Runtime.get().mode === 'testnet')(
|
|
610
|
+
'lists non-zero verified positions for an account',
|
|
611
|
+
async () => {
|
|
612
|
+
const db = TestApp.database()
|
|
613
|
+
// Valuation resolves through the curated verified list, so seed it.
|
|
614
|
+
await TestApp.verifiedSeed(db, TestEarn.chain.id)
|
|
615
|
+
await core_EarnVaults.upsert(db, TestEarn.verifiedVault)
|
|
616
|
+
await core_EarnVaults.upsert(db, {
|
|
617
|
+
...TestEarn.verifiedVault,
|
|
618
|
+
description: 'Active-earnings Earn deployment.',
|
|
619
|
+
label: 'Active btPATHUSD Earn',
|
|
620
|
+
vaultAddress: TestEarn.activeEarningsVaultAddress,
|
|
621
|
+
})
|
|
622
|
+
const app = TestApp.create({ auth: false, cache, db, zones: [TestEarn.zone] })
|
|
623
|
+
const response = await app.request(
|
|
624
|
+
`/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}&verified=true&valuation.currency=USD`,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
expect(response.status).toBe(200)
|
|
628
|
+
const body = await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response)
|
|
629
|
+
expect(body.data.every((position) => BigInt(position.shareAmount.amount) > 0n)).toBe(true)
|
|
630
|
+
const row = body.data.find(
|
|
631
|
+
(position) => position.vaultAddress === TestEarn.activeEarningsVaultAddress,
|
|
632
|
+
)
|
|
633
|
+
expect(row).toBeDefined()
|
|
634
|
+
if (!row) return
|
|
635
|
+
// Amounts render in their own token's display currency and precision.
|
|
636
|
+
expect(row.shareAmount.currency).toBe(row.shareToken.currency)
|
|
637
|
+
expect(row.shareAmount.decimals).toBe(row.shareToken.decimals)
|
|
638
|
+
expect(row.assetAmount.currency).toBe(row.assetToken.currency)
|
|
639
|
+
expect(row.assetAmount.decimals).toBe(row.assetToken.decimals)
|
|
640
|
+
// A USD-denominated asset values identically without consulting rates.
|
|
641
|
+
expect(row.valuation).toStrictEqual({ amount: row.assetAmount.formatted, currency: 'USD' })
|
|
642
|
+
expect(body.meta).toBeUndefined()
|
|
643
|
+
expect({
|
|
644
|
+
assetToken: row.assetToken.address,
|
|
645
|
+
formattedShape: /^\d+(\.\d+)?$/.test(row.assetAmount.formatted),
|
|
646
|
+
id: row.id,
|
|
647
|
+
sharesHeld: BigInt(row.shareAmount.amount) > 0n,
|
|
648
|
+
valued: BigInt(row.assetAmount.amount) > 0n,
|
|
649
|
+
vaultAddress: row.vaultAddress,
|
|
650
|
+
verified: row.verified,
|
|
651
|
+
}).toMatchInlineSnapshot(`
|
|
652
|
+
{
|
|
653
|
+
"assetToken": "0x20c0000000000000000000000000000000000001",
|
|
654
|
+
"formattedShape": true,
|
|
655
|
+
"id": "0x3e4405801a1bee1b58394228ea0b44b667e222fc",
|
|
656
|
+
"sharesHeld": true,
|
|
657
|
+
"valued": true,
|
|
658
|
+
"vaultAddress": "0x3e4405801a1bee1b58394228ea0b44b667e222fc",
|
|
659
|
+
"verified": true,
|
|
660
|
+
}
|
|
661
|
+
`)
|
|
662
|
+
},
|
|
663
|
+
60_000,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
test.runIf(Runtime.get().mode === 'testnet')(
|
|
667
|
+
'discovers held vaults from indexed share balances',
|
|
668
|
+
async () => {
|
|
669
|
+
const app = TestApp.create({ auth: false, cache, zones: [TestEarn.zone] })
|
|
670
|
+
const response = await app.request(
|
|
671
|
+
`/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}`,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
expect(response.status).toBe(200)
|
|
675
|
+
const body = await TestApp.json(response, Earn.schema.getEarnAddressPositions.Response)
|
|
676
|
+
expect(body.data.map((position) => position.vaultAddress)).toContain(
|
|
677
|
+
TestEarn.activeEarningsVaultAddress,
|
|
678
|
+
)
|
|
679
|
+
expect(
|
|
680
|
+
body.data.every(
|
|
681
|
+
(position) =>
|
|
682
|
+
position.id === position.vaultAddress &&
|
|
683
|
+
!position.verified &&
|
|
684
|
+
/^\d+$/.test(position.assetAmount.amount) &&
|
|
685
|
+
BigInt(position.shareAmount.amount) > 0n &&
|
|
686
|
+
position.assetToken.address.startsWith('0x20c0') &&
|
|
687
|
+
position.shareToken.address.startsWith('0x20c0'),
|
|
688
|
+
),
|
|
689
|
+
).toBe(true)
|
|
690
|
+
|
|
691
|
+
// An address-keyset cursor excludes every vault at or before it.
|
|
692
|
+
const cursor = Cursor.encode([TestEarn.activeEarningsVaultAddress])
|
|
693
|
+
const paged = await app.request(
|
|
694
|
+
`/v1/earn/addresses/${TestEarn.activeEarningsAddress}/positions?chainId=${TestEarn.chain.id}&cursor=${encodeURIComponent(cursor)}`,
|
|
695
|
+
)
|
|
696
|
+
expect(paged.status).toBe(200)
|
|
697
|
+
const pagedBody = await TestApp.json(paged, Earn.schema.getEarnAddressPositions.Response)
|
|
698
|
+
expect(
|
|
699
|
+
pagedBody.data.every(
|
|
700
|
+
(position) => position.vaultAddress > TestEarn.activeEarningsVaultAddress,
|
|
701
|
+
),
|
|
702
|
+
).toBe(true)
|
|
703
|
+
},
|
|
704
|
+
90_000,
|
|
705
|
+
)
|
|
706
|
+
})
|
|
707
|
+
|
|
543
708
|
describe('GET /v1/earn/vaults/:vaultId', () => {
|
|
544
709
|
test('rejects a malformed vault address', async () => {
|
|
545
710
|
const app = TestApp.create({ auth: false, cache })
|
|
@@ -1375,6 +1540,7 @@ describe('OpenAPI', () => {
|
|
|
1375
1540
|
|
|
1376
1541
|
expect(Object.keys(spec.paths)).toEqual(
|
|
1377
1542
|
expect.arrayContaining([
|
|
1543
|
+
'/v1/earn/addresses/{address}/positions',
|
|
1378
1544
|
'/v1/earn/vaults',
|
|
1379
1545
|
'/v1/earn/vaults/verified',
|
|
1380
1546
|
'/v1/earn/vaults/{vaultId}',
|
|
@@ -1446,6 +1612,37 @@ describe('OpenAPI', () => {
|
|
|
1446
1612
|
expect(vaultId?.schema?.examples).toStrictEqual([TestEarn.mainnetVaultAddress])
|
|
1447
1613
|
})
|
|
1448
1614
|
|
|
1615
|
+
test('documents address-position inputs and responses', async () => {
|
|
1616
|
+
const app = TestApp.create({ auth: false, cache })
|
|
1617
|
+
const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument
|
|
1618
|
+
const operation = spec.paths['/v1/earn/addresses/{address}/positions']?.get
|
|
1619
|
+
const address = operation?.parameters?.find((parameter) => parameter.name === 'address')
|
|
1620
|
+
const denomination = operation?.parameters?.find(
|
|
1621
|
+
(parameter) => parameter.name === 'valuation.currency',
|
|
1622
|
+
)
|
|
1623
|
+
const verified = operation?.parameters?.find((parameter) => parameter.name === 'verified')
|
|
1624
|
+
const response = JSON.stringify(operation?.responses)
|
|
1625
|
+
|
|
1626
|
+
expect(operation?.description).toContain('currently holds shares')
|
|
1627
|
+
expect(address?.schema?.examples).toStrictEqual([TestEarn.positionAddress])
|
|
1628
|
+
expect(denomination?.description).toContain('nominal value')
|
|
1629
|
+
expect(verified?.description).toContain('registry-curated')
|
|
1630
|
+
expect(verified?.schema?.examples).toStrictEqual([true])
|
|
1631
|
+
expect(response).toContain('"chain_id_invalid"')
|
|
1632
|
+
expect(response).toContain('"chain_id_unsupported"')
|
|
1633
|
+
expect(response).toContain('"positions_invalid"')
|
|
1634
|
+
expect(response).toContain('"query_invalid"')
|
|
1635
|
+
expect(response).toContain('Stable resource ID for this position')
|
|
1636
|
+
expect(response).toContain('Current asset value of the held shares')
|
|
1637
|
+
expect(response).toContain('"assetAmount"')
|
|
1638
|
+
expect(response).toContain('"shareAmount"')
|
|
1639
|
+
expect(response).toContain('"valuation"')
|
|
1640
|
+
expect(response).toContain('"vaultAddress"')
|
|
1641
|
+
expect(response).toContain('"nextCursor"')
|
|
1642
|
+
// Token references embed the trimmed token object, not a bare address.
|
|
1643
|
+
expect(response).toContain('"symbol"')
|
|
1644
|
+
})
|
|
1645
|
+
|
|
1449
1646
|
test('documents vault-earnings variants', async () => {
|
|
1450
1647
|
const app = TestApp.create({ auth: false, cache })
|
|
1451
1648
|
const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument
|