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
|
@@ -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
|
}),
|
|
@@ -181,102 +186,241 @@ export namespace schema {
|
|
|
181
186
|
})
|
|
182
187
|
.check(z.describe('A page of TIP-20 token balances held by this account.'))
|
|
183
188
|
}
|
|
189
|
+
|
|
190
|
+
/** Schemas for the getAddressBalance operation. */
|
|
191
|
+
export namespace getAddressBalance {
|
|
192
|
+
/** Path parameters for a single address balance request. */
|
|
193
|
+
export const Params = z
|
|
194
|
+
.object({
|
|
195
|
+
address: Schema.Address.check(
|
|
196
|
+
z.describe('The account address whose token balance you want to read.'),
|
|
197
|
+
),
|
|
198
|
+
token: Schema.TokenAddress.check(z.describe('The TIP-20 token contract address.')),
|
|
199
|
+
})
|
|
200
|
+
.check(z.describe('Path parameters for reading one account token balance.'))
|
|
201
|
+
|
|
202
|
+
/** Query parameters for a single address balance request. */
|
|
203
|
+
export const Query = z
|
|
204
|
+
.strictObject({
|
|
205
|
+
chainId: Schema.ChainIdQuery,
|
|
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
|
+
),
|
|
212
|
+
})
|
|
213
|
+
.check(z.describe('Query parameters for reading one account token balance.'))
|
|
214
|
+
|
|
215
|
+
/** A single TIP-20 token balance held by an address. */
|
|
216
|
+
export const Response = getAddressBalances.Balance.check(
|
|
217
|
+
z.describe('One TIP-20 token balance held by this account.'),
|
|
218
|
+
)
|
|
219
|
+
}
|
|
184
220
|
}
|
|
185
221
|
|
|
186
222
|
/**
|
|
187
223
|
* Creates address-scoped balance handlers, mounted under the `/addresses`
|
|
188
|
-
* composer. Exposes
|
|
224
|
+
* composer. Exposes the held-balance page and individual token balances.
|
|
189
225
|
*/
|
|
190
226
|
export function addresses(options: addresses.Options = {}) {
|
|
191
227
|
const oracle = options.fx?.oracle ?? FxOracle.ecb()
|
|
192
|
-
return new Hono<App.Environment>()
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
OpenApi.validate('query', schema.getAddressBalances.Query, {
|
|
200
|
-
code: 'query_invalid',
|
|
201
|
-
message: 'Invalid query parameters',
|
|
202
|
-
}),
|
|
203
|
-
OpenApi.describeRoute({
|
|
204
|
-
description:
|
|
205
|
-
'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, and each verified holding carries its nominal value in the requested denomination.',
|
|
206
|
-
operationId: 'getAddressBalances',
|
|
207
|
-
responses: OpenApi.responses({
|
|
208
|
-
errors: { 502: 'Could not read balance or token data from an upstream service.' },
|
|
209
|
-
success: {
|
|
210
|
-
description: 'A page of token balances for this account.',
|
|
211
|
-
schema: schema.getAddressBalances.Response,
|
|
212
|
-
},
|
|
228
|
+
return new Hono<App.Environment>()
|
|
229
|
+
.get(
|
|
230
|
+
'/v1/addresses/:address/balances',
|
|
231
|
+
Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
|
|
232
|
+
OpenApi.validate('param', schema.getAddressBalances.Params, {
|
|
233
|
+
code: 'address_invalid',
|
|
234
|
+
message: 'Invalid account address',
|
|
213
235
|
}),
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
OpenApi.validate('query', schema.getAddressBalances.Query, {
|
|
237
|
+
code: 'query_invalid',
|
|
238
|
+
message: 'Invalid query parameters',
|
|
239
|
+
}),
|
|
240
|
+
OpenApi.describeRoute({
|
|
241
|
+
description:
|
|
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.',
|
|
243
|
+
operationId: 'getAddressBalances',
|
|
244
|
+
responses: OpenApi.responses({
|
|
245
|
+
errors: { 502: 'Could not read balance or token data from an upstream service.' },
|
|
246
|
+
success: {
|
|
247
|
+
description: 'A page of token balances for this account.',
|
|
248
|
+
schema: schema.getAddressBalances.Response,
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
summary: 'List address balances',
|
|
252
|
+
tags: ['Balances'],
|
|
253
|
+
}),
|
|
254
|
+
Cache.response<App.Environment>({
|
|
255
|
+
cacheControl: Cache.policies.noStore,
|
|
256
|
+
name: 'tempo-api:addresses:v1',
|
|
257
|
+
key: (c) => Cache.urlKey(c, schema.getAddressBalances.Query),
|
|
258
|
+
}),
|
|
259
|
+
async (c) => {
|
|
260
|
+
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
261
|
+
if (OpenApi.narrowValidation)
|
|
262
|
+
return OpenApi.validationError(c, {
|
|
263
|
+
code: 'address_invalid',
|
|
264
|
+
message: 'Invalid request parameters',
|
|
265
|
+
})
|
|
266
|
+
if (OpenApi.narrowValidation)
|
|
267
|
+
return OpenApi.validationError(c, {
|
|
268
|
+
code: 'query_invalid',
|
|
269
|
+
message: 'Invalid request parameters',
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const { address } = c.req.valid('param')
|
|
273
|
+
const query = c.req.valid('query')
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
277
|
+
// `currency` only makes sense against verified tokens, so it implies
|
|
278
|
+
// `verified=true` from the caller's perspective.
|
|
279
|
+
const verified = query.verified || query.currency !== undefined
|
|
280
|
+
const feeEligible = query.feeEligible ?? false
|
|
281
|
+
|
|
282
|
+
const balances = await getAddressBalances(c, {
|
|
283
|
+
address,
|
|
284
|
+
chainId,
|
|
285
|
+
currency: query.currency,
|
|
286
|
+
cursor: query.cursor,
|
|
287
|
+
denomination: query['valuation.currency'],
|
|
288
|
+
feeEligible,
|
|
289
|
+
includeTotalCount: query.include.includes('totalCount'),
|
|
290
|
+
limit: query.limit,
|
|
291
|
+
oracle,
|
|
292
|
+
page: query.page,
|
|
293
|
+
verified,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
return c.json(Response.validated(schema.getAddressBalances.Response, balances), 200)
|
|
297
|
+
} catch (cause) {
|
|
298
|
+
if (cause instanceof Valuation.UnsupportedDenominationError)
|
|
299
|
+
return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
|
|
300
|
+
return Response.upstream(c, cause)
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
.get(
|
|
305
|
+
'/v1/addresses/:address/balances/:token',
|
|
306
|
+
Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
|
|
307
|
+
OpenApi.validate('param', schema.getAddressBalance.Params, {
|
|
308
|
+
code: 'address_invalid',
|
|
309
|
+
message: 'Invalid address or token address',
|
|
310
|
+
}),
|
|
311
|
+
OpenApi.validate('query', schema.getAddressBalance.Query, {
|
|
312
|
+
code: 'query_invalid',
|
|
313
|
+
message: 'Invalid query parameters',
|
|
314
|
+
}),
|
|
315
|
+
OpenApi.describeRoute({
|
|
316
|
+
description:
|
|
317
|
+
'Returns the live balance of one TIP-20 token held by an account, including token metadata and a human-readable amount.',
|
|
318
|
+
operationId: 'getAddressBalance',
|
|
319
|
+
responses: OpenApi.responses({
|
|
320
|
+
errors: {
|
|
321
|
+
404: {
|
|
322
|
+
description: 'The token is not a registered TIP-20 token.',
|
|
323
|
+
codes: ['token_not_found'],
|
|
324
|
+
},
|
|
325
|
+
502: 'Could not read balance or token data from an upstream service.',
|
|
326
|
+
},
|
|
327
|
+
success: {
|
|
328
|
+
description: 'The requested token balance for this account.',
|
|
329
|
+
schema: schema.getAddressBalance.Response,
|
|
330
|
+
},
|
|
331
|
+
}),
|
|
332
|
+
summary: 'Get address balance',
|
|
333
|
+
tags: ['Balances'],
|
|
334
|
+
}),
|
|
335
|
+
Cache.response<App.Environment>({
|
|
336
|
+
cacheControl: Cache.policies.noStore,
|
|
337
|
+
name: 'tempo-api:addresses:v1',
|
|
338
|
+
key: (c) => Cache.urlKey(c, schema.getAddressBalance.Query),
|
|
339
|
+
}),
|
|
340
|
+
async (c) => {
|
|
341
|
+
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
342
|
+
if (OpenApi.narrowValidation)
|
|
343
|
+
return OpenApi.validationError(c, {
|
|
344
|
+
code: 'address_invalid',
|
|
345
|
+
message: 'Invalid request parameters',
|
|
346
|
+
})
|
|
347
|
+
if (OpenApi.narrowValidation)
|
|
348
|
+
return OpenApi.validationError(c, {
|
|
349
|
+
code: 'query_invalid',
|
|
350
|
+
message: 'Invalid request parameters',
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const { address, token } = c.req.valid('param')
|
|
354
|
+
const query = c.req.valid('query')
|
|
239
355
|
const chainId = query.chainId ?? c.get('chainId')
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
356
|
+
const denomination = query['valuation.currency']
|
|
357
|
+
|
|
358
|
+
try {
|
|
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([
|
|
378
|
+
Timing.time(c, 'balance_rpc', async () => {
|
|
379
|
+
const result = await Actions.token.getBalance(c.get('getClient')(chainId), {
|
|
380
|
+
account: address,
|
|
381
|
+
token,
|
|
382
|
+
})
|
|
383
|
+
return result.amount
|
|
384
|
+
}),
|
|
385
|
+
FeeAmm.feeTokenSet(c, chainId).catch(() => undefined),
|
|
386
|
+
snapshotPromise,
|
|
387
|
+
resolvedTokenPromise,
|
|
388
|
+
])
|
|
389
|
+
return c.json(
|
|
390
|
+
Response.validated(schema.getAddressBalance.Response, {
|
|
391
|
+
amount: balance.toString(),
|
|
392
|
+
currency: resolvedToken.currency,
|
|
393
|
+
decimals: resolvedToken.decimals,
|
|
394
|
+
...(feeTokens ? { feeEligible: feeTokens.has(token) } : {}),
|
|
395
|
+
formatted: core_Value.format(balance, resolvedToken.decimals),
|
|
396
|
+
id: token,
|
|
397
|
+
token: resolvedToken,
|
|
398
|
+
...(denomination
|
|
399
|
+
? {
|
|
400
|
+
valuation: Valuation.valuationFor({
|
|
401
|
+
amount: balance,
|
|
402
|
+
denomination,
|
|
403
|
+
rates,
|
|
404
|
+
token: snapshot.byAddress.get(token),
|
|
405
|
+
}),
|
|
406
|
+
}
|
|
407
|
+
: {}),
|
|
408
|
+
}),
|
|
409
|
+
200,
|
|
410
|
+
)
|
|
411
|
+
} catch (cause) {
|
|
412
|
+
if (Tokens.isTokenNotFound(cause))
|
|
413
|
+
return Response.error(c, {
|
|
414
|
+
code: 'token_not_found',
|
|
415
|
+
message: 'Token not found',
|
|
416
|
+
status: 404,
|
|
417
|
+
})
|
|
418
|
+
if (cause instanceof Valuation.UnsupportedDenominationError)
|
|
419
|
+
return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
|
|
420
|
+
return Response.upstream(c, cause)
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
)
|
|
280
424
|
}
|
|
281
425
|
|
|
282
426
|
export declare namespace addresses {
|
|
@@ -297,7 +441,6 @@ async function getAddressBalances(
|
|
|
297
441
|
currency,
|
|
298
442
|
denomination,
|
|
299
443
|
feeEligible,
|
|
300
|
-
feeTokens,
|
|
301
444
|
includeTotalCount,
|
|
302
445
|
limit,
|
|
303
446
|
oracle,
|
|
@@ -307,8 +450,9 @@ async function getAddressBalances(
|
|
|
307
450
|
const cursor = options.cursor ? Cursor.decode(options.cursor, ['uint', 'address']) : undefined
|
|
308
451
|
const offset = options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : 0
|
|
309
452
|
|
|
310
|
-
// Validate
|
|
311
|
-
|
|
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 })
|
|
312
456
|
|
|
313
457
|
// Build the optional `token IN (…)` restriction before hitting TIDX. The
|
|
314
458
|
// verified/`currency` filter and the `feeEligible` filter each contribute a set
|
|
@@ -317,17 +461,32 @@ async function getAddressBalances(
|
|
|
317
461
|
// intersection. If any active filter rules every token out, return an empty
|
|
318
462
|
// page without a round trip. The snapshot is reused for the page enrichment
|
|
319
463
|
// below.
|
|
320
|
-
const
|
|
321
|
-
|
|
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
|
+
})
|
|
322
479
|
const tokenFilter = allowed?.map((address) => `'${address}'`)
|
|
323
|
-
if (tokenFilter?.length === 0)
|
|
480
|
+
if (tokenFilter?.length === 0) {
|
|
481
|
+
await snapshotPromise
|
|
324
482
|
return {
|
|
325
483
|
data: [],
|
|
326
484
|
...(includeTotalCount ? { meta: { totalCount: 0, totalCountCapped: false } } : {}),
|
|
327
485
|
nextCursor: null,
|
|
328
486
|
}
|
|
487
|
+
}
|
|
329
488
|
|
|
330
|
-
const
|
|
489
|
+
const holdingsPromise = Timing.time(c, 'balances_discovery', async () => {
|
|
331
490
|
const tokenIn = tokenFilter ? ` AND token IN (${tokenFilter.join(', ')})` : ''
|
|
332
491
|
|
|
333
492
|
const result = await tidx.fetch({
|
|
@@ -348,36 +507,54 @@ async function getAddressBalances(
|
|
|
348
507
|
})
|
|
349
508
|
})
|
|
350
509
|
|
|
351
|
-
|
|
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) {
|
|
352
552
|
return {
|
|
353
553
|
data: [],
|
|
354
554
|
...(includeTotalCount ? { meta: { totalCount: 0, totalCountCapped: false } } : {}),
|
|
355
555
|
nextCursor: null,
|
|
356
556
|
}
|
|
357
|
-
|
|
358
|
-
const client = c.get('getClient')(chainId)
|
|
359
|
-
const corrected = await Timing.time(c, 'balances_rpc', async () => {
|
|
360
|
-
// Concurrent reads enter viem's shared batch window and become one
|
|
361
|
-
// deployless multicall. A failed read fails the request rather than serving
|
|
362
|
-
// a stale or partial balance page.
|
|
363
|
-
const results = await Promise.all(
|
|
364
|
-
holdings.map(async (holding) => {
|
|
365
|
-
const { amount } = await Actions.token.getBalance(client, {
|
|
366
|
-
account: address,
|
|
367
|
-
token: holding.address,
|
|
368
|
-
})
|
|
369
|
-
return amount === 0n ? undefined : { address: holding.address, amount: amount.toString() }
|
|
370
|
-
}),
|
|
371
|
-
)
|
|
372
|
-
const rows = results.filter((holding) => holding !== undefined)
|
|
373
|
-
|
|
374
|
-
rows.sort((a, b) => {
|
|
375
|
-
const delta = BigInt(b.amount) - BigInt(a.amount)
|
|
376
|
-
if (delta !== 0n) return delta > 0n ? 1 : -1
|
|
377
|
-
return a.address < b.address ? -1 : a.address > b.address ? 1 : 0
|
|
378
|
-
})
|
|
379
|
-
return rows
|
|
380
|
-
})
|
|
557
|
+
}
|
|
381
558
|
|
|
382
559
|
const positioned = (() => {
|
|
383
560
|
if (!cursor) return corrected.slice(offset)
|
|
@@ -395,7 +572,7 @@ async function getAddressBalances(
|
|
|
395
572
|
key: (holding) => [holding.amount, holding.address],
|
|
396
573
|
})
|
|
397
574
|
|
|
398
|
-
if (page.rows.length === 0)
|
|
575
|
+
if (page.rows.length === 0) {
|
|
399
576
|
return {
|
|
400
577
|
data: [],
|
|
401
578
|
...(includeTotalCount
|
|
@@ -403,22 +580,38 @@ async function getAddressBalances(
|
|
|
403
580
|
: {}),
|
|
404
581
|
nextCursor: page.nextCursor,
|
|
405
582
|
}
|
|
583
|
+
}
|
|
406
584
|
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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)
|
|
415
595
|
|
|
416
596
|
// Balance tokens retain their logo and verification metadata by default.
|
|
417
|
-
const
|
|
597
|
+
const resolvedPromise = Timing.time(c, 'tokens', () =>
|
|
418
598
|
Promise.all(
|
|
419
599
|
page.rows.map(async (holding) => {
|
|
420
600
|
try {
|
|
421
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
|
+
}
|
|
422
615
|
const [logoUri, metadata] = await Promise.all([
|
|
423
616
|
Timing.time(c, 'token_logo', () =>
|
|
424
617
|
Tokens.getTokenLogo(c, { address: token, chainId }),
|
|
@@ -434,6 +627,7 @@ async function getAddressBalances(
|
|
|
434
627
|
}),
|
|
435
628
|
),
|
|
436
629
|
)
|
|
630
|
+
const [rates, resolved] = await Promise.all([ratesPromise, resolvedPromise])
|
|
437
631
|
|
|
438
632
|
const data: z.output<typeof schema.getAddressBalances.Balance>[] = []
|
|
439
633
|
for (const entry of resolved) {
|
|
@@ -458,12 +652,16 @@ async function getAddressBalances(
|
|
|
458
652
|
symbol: entry.metadata.symbol,
|
|
459
653
|
verified: snapshot.byAddress.has(entry.token),
|
|
460
654
|
},
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
: {}),
|
|
467
665
|
})
|
|
468
666
|
}
|
|
469
667
|
|
|
@@ -487,15 +685,9 @@ declare namespace getAddressBalances {
|
|
|
487
685
|
/** Opaque keyset cursor anchoring the page; omit for the head page. */
|
|
488
686
|
cursor?: string | undefined
|
|
489
687
|
/** Currency the per-row valuations are denominated in (uppercase). */
|
|
490
|
-
denomination
|
|
491
|
-
/** When true, restrict the page to fee-usable tokens
|
|
688
|
+
denomination?: string | undefined
|
|
689
|
+
/** When true, restrict the page to fee-usable tokens. */
|
|
492
690
|
feeEligible?: boolean | undefined
|
|
493
|
-
/**
|
|
494
|
-
* The chain's fee-token set, if resolved (best-effort): flags each row's
|
|
495
|
-
* `feeEligible` and backs the `feeEligible` filter. Undefined when the lookup
|
|
496
|
-
* failed, in which case `feeEligible` is omitted from rows.
|
|
497
|
-
*/
|
|
498
|
-
feeTokens?: ReadonlySet<string> | undefined
|
|
499
691
|
/** When true, include the corrected candidate count in response metadata. */
|
|
500
692
|
includeTotalCount: boolean
|
|
501
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 () => {
|