tapimo 0.5.2 → 0.6.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 +13987 -5755
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +910 -412
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +4 -0
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/early-access.d.ts +148 -0
- package/dist/admin/apps/early-access.d.ts.map +1 -0
- package/dist/admin/apps/early-access.js +133 -0
- package/dist/admin/apps/early-access.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +643 -118
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.js +214 -1
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +45 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +3733 -2428
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +11 -6
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +193 -2
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +71 -0
- package/dist/apps/data/FxOracle.d.ts.map +1 -0
- package/dist/apps/data/FxOracle.js +97 -0
- package/dist/apps/data/FxOracle.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -0
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +1 -0
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +3575 -1358
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +230 -88
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +963 -342
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/addresses.js +11 -9
- package/dist/apps/data/routes/addresses.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +108 -57
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +88 -45
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/exchanges.d.ts +952 -201
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +518 -76
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/funding.d.ts +8 -2
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +32 -6
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts +392 -315
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +152 -77
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +166 -173
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +14 -12
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +277 -536
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +96 -77
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +82 -17
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +70 -13
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +307 -0
- package/dist/apps/data/routes/valuation.d.ts.map +1 -0
- package/dist/apps/data/routes/valuation.js +292 -0
- package/dist/apps/data/routes/valuation.js.map +1 -0
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +7 -5
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +459 -479
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +124 -25
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +8606 -1681
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +12 -1
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +23 -0
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +5 -3
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +88 -48
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +10 -2
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +218 -0
- package/dist/apps/management/routes/faucet.d.ts.map +1 -0
- package/dist/apps/management/routes/faucet.js +139 -0
- package/dist/apps/management/routes/faucet.js.map +1 -0
- package/dist/apps/management/routes/invitations.d.ts +121 -121
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +3 -0
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +197 -168
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
- package/dist/apps/management/routes/invite-links.js +26 -1
- package/dist/apps/management/routes/invite-links.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +2 -0
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +4 -0
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +49 -49
- package/dist/apps/management/routes/orgs.d.ts +91 -91
- package/dist/apps/management/routes/projects.d.ts +115 -115
- package/dist/apps/management/routes/usage.d.ts +35 -32
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +4 -0
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
- package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
- package/dist/apps/management/routes/verified-token-requests.js +271 -0
- package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
- package/dist/apps/management/routes/webhooks.d.ts +6977 -0
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
- package/dist/apps/management/routes/webhooks.js +573 -0
- package/dist/apps/management/routes/webhooks.js.map +1 -0
- package/dist/apps/mcp.d.ts +9 -2
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +9 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +7 -2
- package/dist/cli/Cli.js.map +1 -1
- package/dist/db/Db.d.ts +4 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +83 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +41 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +45 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/earlyAccess.d.ts +50 -0
- package/dist/db/tables/earlyAccess.d.ts.map +1 -0
- package/dist/db/tables/earlyAccess.js +63 -0
- package/dist/db/tables/earlyAccess.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +12 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +23 -0
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +10 -2
- package/dist/db/tables/inviteLinks.d.ts.map +1 -1
- package/dist/db/tables/inviteLinks.js +10 -1
- package/dist/db/tables/inviteLinks.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +2 -0
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
- package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
- package/dist/db/tables/verifiedTokenRequests.js +120 -0
- package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +16 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +43 -2
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +60 -45
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +69 -15
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +74 -25
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +19 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +2 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +9 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +2 -2
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +3 -0
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Schema.d.ts +14 -5
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +23 -5
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Value.d.ts +18 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +11 -1
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/VerifiedTokens.d.ts.map +1 -1
- package/dist/internal/VerifiedTokens.js +10 -1
- package/dist/internal/VerifiedTokens.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/dist/internal/Webhooks.d.ts +24 -3
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +34 -8
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +2 -2
- package/src/App.ts +9 -1
- package/src/Client.test-d.ts +638 -6
- package/src/admin/App.ts +4 -0
- package/src/admin/apps/early-access.test.ts +125 -0
- package/src/admin/apps/early-access.ts +175 -0
- package/src/admin/apps/verified-tokens.test.ts +201 -0
- package/src/admin/apps/verified-tokens.ts +532 -286
- package/src/admin/ui/plugin.ts +2 -1
- package/src/admin/ui/src/lib/early-access.ts +63 -0
- package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
- package/src/admin/ui/src/routeTree.gen.ts +33 -2
- package/src/admin/ui/src/routes/__root.tsx +2 -1
- package/src/admin/ui/src/routes/early-access.tsx +157 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
- package/src/admin/ui.generated.ts +48 -33
- package/src/apps/data/App.ts +22 -14
- package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
- package/src/apps/data/FundingQuoteProviders.ts +257 -4
- package/src/apps/data/FxOracle.test.ts +118 -0
- package/src/apps/data/FxOracle.ts +153 -0
- package/src/apps/data/index.ts +1 -0
- package/src/apps/data/routes/activities.test.ts +173 -3
- package/src/apps/data/routes/activities.ts +403 -132
- package/src/apps/data/routes/addresses.ts +19 -9
- package/src/apps/data/routes/balances.test.ts +144 -11
- package/src/apps/data/routes/balances.ts +140 -51
- package/src/apps/data/routes/exchanges.test.ts +540 -9
- package/src/apps/data/routes/exchanges.ts +690 -103
- package/src/apps/data/routes/funding.test.ts +84 -0
- package/src/apps/data/routes/funding.ts +38 -6
- package/src/apps/data/routes/receipts.test.ts +125 -23
- package/src/apps/data/routes/receipts.ts +205 -100
- package/src/apps/data/routes/tokens.test.ts +43 -0
- package/src/apps/data/routes/tokens.ts +27 -12
- package/src/apps/data/routes/transactions.test.ts +72 -19
- package/src/apps/data/routes/transactions.ts +162 -92
- package/src/apps/data/routes/transfers.test.ts +146 -4
- package/src/apps/data/routes/transfers.ts +91 -13
- package/src/apps/data/routes/valuation.test.ts +422 -0
- package/src/apps/data/routes/valuation.ts +423 -0
- package/src/apps/data/routes/verified-tokens.ts +14 -2
- package/src/apps/data/routes/webhooks.test.ts +56 -8
- package/src/apps/data/routes/webhooks.ts +144 -27
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +13 -0
- package/src/apps/management/routes/api-keys.test.ts +87 -0
- package/src/apps/management/routes/api-keys.ts +4 -3
- package/src/apps/management/routes/billing.test.ts +112 -1
- package/src/apps/management/routes/billing.ts +7 -2
- package/src/apps/management/routes/faucet.test.ts +79 -0
- package/src/apps/management/routes/faucet.ts +185 -0
- package/src/apps/management/routes/invitations.test.ts +172 -0
- package/src/apps/management/routes/invitations.ts +3 -0
- package/src/apps/management/routes/invite-links.test.ts +117 -0
- package/src/apps/management/routes/invite-links.ts +43 -1
- package/src/apps/management/routes/me.test.ts +140 -88
- package/src/apps/management/routes/me.ts +6 -0
- package/src/apps/management/routes/usage.test.ts +6 -0
- package/src/apps/management/routes/usage.ts +7 -0
- package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
- package/src/apps/management/routes/verified-token-requests.ts +397 -0
- package/src/apps/management/routes/webhooks.test.ts +426 -0
- package/src/apps/management/routes/webhooks.ts +672 -0
- package/src/apps/mcp.test.ts +13 -0
- package/src/apps/mcp.ts +18 -5
- package/src/cli/Cli.test.ts +36 -0
- package/src/cli/Cli.ts +7 -2
- package/src/db/Db.test.ts +19 -0
- package/src/db/Db.ts +95 -0
- package/src/db/Schema.ts +53 -0
- package/src/db/tables/earlyAccess.ts +86 -0
- package/src/db/tables/invitations.ts +26 -1
- package/src/db/tables/inviteLinks.ts +22 -3
- package/src/db/tables/sponsoredTransactions.ts +4 -0
- package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
- package/src/db/tables/verifiedTokenRequests.ts +182 -0
- package/src/db/tables/webhookSubscriptions.ts +31 -0
- package/src/internal/Auth.ts +73 -45
- package/src/internal/FundingQuotes.test.ts +143 -1
- package/src/internal/FundingQuotes.ts +95 -28
- package/src/internal/Log.test-d.ts +21 -0
- package/src/internal/Log.test.ts +50 -0
- package/src/internal/Log.ts +24 -1
- package/src/internal/MetricSink.test.ts +160 -0
- package/src/internal/MetricSink.ts +10 -0
- package/src/internal/OpenApi.ts +3 -0
- package/src/internal/Schema.ts +50 -5
- package/src/internal/Value.test.ts +20 -0
- package/src/internal/Value.ts +24 -1
- package/src/internal/VerifiedTokens.ts +9 -1
- package/src/internal/Webhooks.test.ts +140 -0
- package/src/internal/Webhooks.ts +53 -9
|
@@ -3,22 +3,32 @@ import { Hono } from 'hono'
|
|
|
3
3
|
import type * as App from '../../../App.js'
|
|
4
4
|
import * as Activities from './activities.js'
|
|
5
5
|
import * as Balances from './balances.js'
|
|
6
|
+
import * as Valuation from './valuation.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Composes the `/v1/addresses/:address/*` route surface from the
|
|
9
10
|
* address-scoped sub-routers exported by each resource module. The handlers,
|
|
10
11
|
* schemas, and paths live with the resource they describe (activities,
|
|
11
|
-
* balances); this composer only aggregates them so docs grouping
|
|
12
|
-
* while URLs stay nested under the parent resource.
|
|
12
|
+
* balances, valuation); this composer only aggregates them so docs grouping
|
|
13
|
+
* stays by tag while URLs stay nested under the parent resource.
|
|
13
14
|
*
|
|
14
15
|
* Only subject-relative resources live here — rows that cannot be interpreted
|
|
15
|
-
* without the path address (activity classification, held-token balances
|
|
16
|
-
* Plain filtered views of canonical collections are
|
|
17
|
-
* instead (`GET /transactions?address=…`,
|
|
18
|
-
* `GET /transactions/receipts?address=…`).
|
|
16
|
+
* without the path address (activity classification, held-token balances,
|
|
17
|
+
* holdings valuation). Plain filtered views of canonical collections are
|
|
18
|
+
* reached via search params instead (`GET /transactions?address=…`,
|
|
19
|
+
* `GET /transfers?address=…`, `GET /transactions/receipts?address=…`).
|
|
19
20
|
*/
|
|
20
|
-
export function addresses() {
|
|
21
|
+
export function addresses(options: addresses.Options = {}) {
|
|
21
22
|
return new Hono<App.Environment>()
|
|
22
|
-
.route('/', Activities.addresses())
|
|
23
|
-
.route('/', Balances.addresses())
|
|
23
|
+
.route('/', Activities.addresses({ fx: options.fx }))
|
|
24
|
+
.route('/', Balances.addresses({ fx: options.fx }))
|
|
25
|
+
.route('/', Valuation.addresses({ fx: options.fx }))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export declare namespace addresses {
|
|
29
|
+
/** Options for the address-scoped route composer. */
|
|
30
|
+
type Options = {
|
|
31
|
+
/** FX configuration for the valuation endpoint. */
|
|
32
|
+
fx?: Valuation.addresses.Fx | undefined
|
|
33
|
+
}
|
|
24
34
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Schema } from 'tapimo'
|
|
2
|
+
import { FxOracle } from 'tapimo/apps'
|
|
2
3
|
import { Value as core_Value } from 'ox'
|
|
3
4
|
import { Actions } from 'viem/tempo'
|
|
4
5
|
|
|
@@ -8,6 +9,12 @@ import * as TestViem from '../../../../test/Viem.js'
|
|
|
8
9
|
import * as Balances from './balances.js'
|
|
9
10
|
import * as Tokens from './tokens.js'
|
|
10
11
|
|
|
12
|
+
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
13
|
+
const fixed = FxOracle.from({
|
|
14
|
+
name: 'fixed',
|
|
15
|
+
rates: async () => ({ asOf: '2026-01-01', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' } }),
|
|
16
|
+
})
|
|
17
|
+
|
|
11
18
|
// Verified enrichment is store-backed now that the build-time bundle is gone, so
|
|
12
19
|
// seed a shared in-memory verified store from the curated seeds file. Tests use
|
|
13
20
|
// this wrapper instead of `TestApp.client` so the default app exposes the same
|
|
@@ -35,6 +42,21 @@ async function holderWithBalance(client: ReturnType<typeof app>) {
|
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
describe('GET /addresses/:address/balances', () => {
|
|
45
|
+
test('parses the total count include', () => {
|
|
46
|
+
expect(Balances.schema.getAddressBalances.Query.parse({ include: 'totalCount' }).include)
|
|
47
|
+
.toMatchInlineSnapshot(`
|
|
48
|
+
[
|
|
49
|
+
"totalCount",
|
|
50
|
+
]
|
|
51
|
+
`)
|
|
52
|
+
expect(Balances.schema.getAddressBalances.Query.safeParse({ include: 'token' }).success).toBe(
|
|
53
|
+
false,
|
|
54
|
+
)
|
|
55
|
+
expect(
|
|
56
|
+
Balances.schema.getAddressBalances.Query.safeParse({ include: 'token.logoUri' }).success,
|
|
57
|
+
).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
38
60
|
test('returns an amount-ranked balance page with per-row token references', async () => {
|
|
39
61
|
const client = app()
|
|
40
62
|
const holder = await holderWithBalance(client)
|
|
@@ -64,12 +86,15 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
64
86
|
// `formatted` is `amount` scaled down by `decimals` (round-trips back
|
|
65
87
|
// to the base-unit amount).
|
|
66
88
|
/^\d+(\.\d+)?$/.test(entry.formatted) &&
|
|
67
|
-
core_Value.from(entry.formatted, entry.
|
|
89
|
+
core_Value.from(entry.formatted, entry.decimals) === BigInt(entry.amount) &&
|
|
90
|
+
entry.currency === entry.token.currency &&
|
|
91
|
+
entry.decimals === entry.token.decimals &&
|
|
68
92
|
/^0x[0-9a-f]{40}$/.test(entry.token.address) &&
|
|
69
|
-
typeof entry.
|
|
70
|
-
Number.isInteger(entry.
|
|
93
|
+
typeof entry.currency === 'string' &&
|
|
94
|
+
Number.isInteger(entry.decimals) &&
|
|
71
95
|
typeof entry.token.name === 'string' &&
|
|
72
96
|
typeof entry.token.symbol === 'string' &&
|
|
97
|
+
(entry.token.logoUri === undefined || typeof entry.token.logoUri === 'string') &&
|
|
73
98
|
typeof entry.token.verified === 'boolean',
|
|
74
99
|
),
|
|
75
100
|
).toBe(true)
|
|
@@ -133,15 +158,17 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
133
158
|
// The count comes from the corrected candidate set and is never capped.
|
|
134
159
|
expect(body.meta!.totalCountCapped).toBe(false)
|
|
135
160
|
|
|
136
|
-
// Without the include, `meta`
|
|
137
|
-
// already supplies the count, so
|
|
161
|
+
// Without the include, counts are omitted (`meta` carries at most rate
|
|
162
|
+
// provenance). The corrected candidate set already supplies the count, so
|
|
163
|
+
// no second indexer query is needed.
|
|
138
164
|
const bareResponse = await client.v1.addresses[':address'].balances.$get(
|
|
139
165
|
{ param: { address: holder }, query: { limit: '5' } },
|
|
140
166
|
TestApp.auth,
|
|
141
167
|
)
|
|
142
168
|
const bare = await TestApp.json(bareResponse, Balances.schema.getAddressBalances.Response)
|
|
143
169
|
expect(bareResponse.status).toBe(200)
|
|
144
|
-
expect(bare.meta).toBeUndefined()
|
|
170
|
+
expect(bare.meta?.totalCount).toBeUndefined()
|
|
171
|
+
expect(bare.meta?.totalCountCapped).toBeUndefined()
|
|
145
172
|
expect(bareResponse.headers.get('server-timing')?.includes('balances_discovery;dur=')).toBe(
|
|
146
173
|
true,
|
|
147
174
|
)
|
|
@@ -235,24 +262,45 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
235
262
|
expect(stable.error.code).toMatchInlineSnapshot(`"query_invalid"`)
|
|
236
263
|
})
|
|
237
264
|
|
|
238
|
-
test('
|
|
239
|
-
const
|
|
265
|
+
test('includes token logo and verification fields by default', async () => {
|
|
266
|
+
const logoDb = TestApp.database()
|
|
267
|
+
await TestApp.verifiedSeed(logoDb, runtime.chainId, {
|
|
268
|
+
tokens: [
|
|
269
|
+
{
|
|
270
|
+
address: TestApp.tokenWithHolders,
|
|
271
|
+
currency: 'USD',
|
|
272
|
+
decimals: 6,
|
|
273
|
+
logoUri: 'https://example.com/pathusd.svg',
|
|
274
|
+
name: 'PathUSD',
|
|
275
|
+
symbol: 'pathUSD',
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
})
|
|
279
|
+
const client = app({ db: logoDb })
|
|
240
280
|
const holder = await holderWithBalance(client)
|
|
241
281
|
expect(holder).toMatch(/^0x[0-9a-f]{40}$/)
|
|
242
282
|
|
|
243
283
|
// The seeded holder is a holder of a verified token, so the verified
|
|
244
284
|
// balances page must include at least that token.
|
|
245
285
|
const response = await client.v1.addresses[':address'].balances.$get(
|
|
246
|
-
{
|
|
286
|
+
{
|
|
287
|
+
param: { address: holder },
|
|
288
|
+
query: {
|
|
289
|
+
limit: '20',
|
|
290
|
+
verified: 'true',
|
|
291
|
+
},
|
|
292
|
+
},
|
|
247
293
|
TestApp.auth,
|
|
248
294
|
)
|
|
249
295
|
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
250
296
|
expect(response.status).toBe(200)
|
|
251
297
|
expect(body.data.length).toBeGreaterThan(0)
|
|
252
298
|
for (const balance of body.data) expect(balance.token.verified).toBe(true)
|
|
299
|
+
const pathUsd = body.data.find((balance) => balance.token.address === TestApp.tokenWithHolders)
|
|
300
|
+
expect(pathUsd?.token.logoUri).toMatch(/^http/)
|
|
253
301
|
})
|
|
254
302
|
|
|
255
|
-
test('narrows balances by currency
|
|
303
|
+
test('narrows balances by currency and retains verification', async () => {
|
|
256
304
|
const client = app()
|
|
257
305
|
const holder = await holderWithBalance(client)
|
|
258
306
|
expect(holder).toMatch(/^0x[0-9a-f]{40}$/)
|
|
@@ -264,8 +312,9 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
264
312
|
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
265
313
|
expect(response.status).toBe(200)
|
|
266
314
|
for (const balance of body.data) {
|
|
267
|
-
expect(balance.
|
|
315
|
+
expect(balance.currency).toBe('USD')
|
|
268
316
|
expect(balance.token.currency).toBe('USD')
|
|
317
|
+
expect(balance.token.verified).toBe(true)
|
|
269
318
|
}
|
|
270
319
|
})
|
|
271
320
|
|
|
@@ -293,6 +342,90 @@ describe('GET /addresses/:address/balances', () => {
|
|
|
293
342
|
expect(pathUsd?.feeEligible).toBe(true)
|
|
294
343
|
})
|
|
295
344
|
|
|
345
|
+
test('values each verified holding in the default denomination', async () => {
|
|
346
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
347
|
+
const client = app()
|
|
348
|
+
const holder = await holderWithBalance(client)
|
|
349
|
+
|
|
350
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
351
|
+
{ param: { address: holder }, query: { limit: '50' } },
|
|
352
|
+
TestApp.auth,
|
|
353
|
+
)
|
|
354
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
355
|
+
expect(response.status).toBe(200)
|
|
356
|
+
|
|
357
|
+
// The guaranteed pathUSD holding is curated USD, so its USD value is the
|
|
358
|
+
// holding itself; unverified holdings are never valued.
|
|
359
|
+
const pathUsd = body.data.find((entry) => entry.token.address === TestApp.tokenWithHolders)
|
|
360
|
+
expect(pathUsd?.valuation).toEqual({ amount: pathUsd!.formatted, currency: 'USD' })
|
|
361
|
+
for (const entry of body.data) if (!entry.token.verified) expect(entry.valuation).toBeNull()
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test('values holdings in a requested denomination', async () => {
|
|
365
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
366
|
+
const client = app({ fx: { oracle: fixed } })
|
|
367
|
+
const holder = await holderWithBalance(client)
|
|
368
|
+
|
|
369
|
+
// Denominations are case-insensitive.
|
|
370
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
371
|
+
{ param: { address: holder }, query: { denomination: 'aud', limit: '50' } },
|
|
372
|
+
TestApp.auth,
|
|
373
|
+
)
|
|
374
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
375
|
+
expect(response.status).toBe(200)
|
|
376
|
+
|
|
377
|
+
// USD -> AUD at the fixed 1.6 cross rate, floored to 6 dp.
|
|
378
|
+
const pathUsd = body.data.find((entry) => entry.token.address === TestApp.tokenWithHolders)
|
|
379
|
+
const expected = core_Value.format((core_Value.from(pathUsd!.formatted, 6) * 16n) / 10n, 6)
|
|
380
|
+
expect(pathUsd?.valuation).toEqual({ amount: expected, currency: 'AUD' })
|
|
381
|
+
// Consulted rates surface their provenance response-wide.
|
|
382
|
+
expect(body.meta?.pricing).toMatchInlineSnapshot(`
|
|
383
|
+
{
|
|
384
|
+
"asOf": "2026-01-01",
|
|
385
|
+
"basis": "nominal",
|
|
386
|
+
"source": "fixed",
|
|
387
|
+
}
|
|
388
|
+
`)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('nulls valuations when rates are unavailable', async () => {
|
|
392
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
393
|
+
const client = app({ fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) } })
|
|
394
|
+
const holder = await holderWithBalance(client)
|
|
395
|
+
|
|
396
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
397
|
+
{ param: { address: holder }, query: { denomination: 'AUD', limit: '50' } },
|
|
398
|
+
TestApp.auth,
|
|
399
|
+
)
|
|
400
|
+
const body = await TestApp.json(response, Balances.schema.getAddressBalances.Response)
|
|
401
|
+
|
|
402
|
+
// The live balance page survives a rate outage; only values degrade, and
|
|
403
|
+
// no provenance is claimed.
|
|
404
|
+
expect(response.status).toBe(200)
|
|
405
|
+
expect(body.data.length).toBeGreaterThan(0)
|
|
406
|
+
for (const entry of body.data) expect(entry.valuation).toBeNull()
|
|
407
|
+
expect(body.meta?.pricing).toBeUndefined()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('rejects denominations the oracle does not price', async () => {
|
|
411
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
412
|
+
const client = app({ fx: { oracle: fixed } })
|
|
413
|
+
|
|
414
|
+
const response = await client.v1.addresses[':address'].balances.$get(
|
|
415
|
+
{
|
|
416
|
+
param: { address: `0x${'11'.repeat(20)}` },
|
|
417
|
+
query: { currency: 'JPY', denomination: 'JPY' },
|
|
418
|
+
},
|
|
419
|
+
TestApp.auth,
|
|
420
|
+
)
|
|
421
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
422
|
+
const { requestId, ...stable } = body
|
|
423
|
+
|
|
424
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
425
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
426
|
+
expect(stable.error.code).toMatchInlineSnapshot(`"query_invalid"`)
|
|
427
|
+
})
|
|
428
|
+
|
|
296
429
|
test('restricts the page to fee tokens when feeEligible=true', async () => {
|
|
297
430
|
const client = app()
|
|
298
431
|
const holder = await holderWithBalance(client)
|
|
@@ -12,8 +12,10 @@ import * as Response from '../../../internal/Response.js'
|
|
|
12
12
|
import * as Schema from '../../../internal/Schema.js'
|
|
13
13
|
import * as Timing from '../../../internal/Timing.js'
|
|
14
14
|
import * as VerifiedTokens from '../../../internal/VerifiedTokens.js'
|
|
15
|
+
import * as FxOracle from '../FxOracle.js'
|
|
15
16
|
import * as FeeAmm from './fee-amm.js'
|
|
16
17
|
import * as Tokens from './tokens.js'
|
|
18
|
+
import * as Valuation from './valuation.js'
|
|
17
19
|
|
|
18
20
|
// TIP-20 tokens are minted by the TIP-20 factory and live at deterministic
|
|
19
21
|
// `0x20c0…`-prefixed addresses (e.g. pathUSD `0x20c0…0000`). The indexer's
|
|
@@ -50,6 +52,7 @@ export namespace schema {
|
|
|
50
52
|
),
|
|
51
53
|
),
|
|
52
54
|
cursor: Schema.Cursor,
|
|
55
|
+
denomination: Schema.Denomination,
|
|
53
56
|
feeEligible: z
|
|
54
57
|
.optional(Schema.booleanQuery())
|
|
55
58
|
.check(
|
|
@@ -78,12 +81,7 @@ export namespace schema {
|
|
|
78
81
|
z.describe('Query parameters for listing an account’s token balances.'),
|
|
79
82
|
)
|
|
80
83
|
|
|
81
|
-
/**
|
|
82
|
-
* A TIP-20 token balance held by an address: the held `amount` in the
|
|
83
|
-
* token's base unit, its `formatted` value, and the trimmed `token`
|
|
84
|
-
* reference inlined per row. Rows whose token metadata cannot be resolved
|
|
85
|
-
* are omitted.
|
|
86
|
-
*/
|
|
84
|
+
/** A TIP-20 balance with raw and formatted amounts. Unresolvable token metadata omits the row. */
|
|
87
85
|
export const Balance = Schema.describe(
|
|
88
86
|
z.object({
|
|
89
87
|
amount: z
|
|
@@ -95,21 +93,23 @@ export namespace schema {
|
|
|
95
93
|
),
|
|
96
94
|
z.meta({ examples: ['1500000'] }),
|
|
97
95
|
),
|
|
98
|
-
|
|
96
|
+
currency: z
|
|
99
97
|
.string()
|
|
100
98
|
.check(
|
|
101
99
|
z.describe(
|
|
102
|
-
'The
|
|
100
|
+
'The currency label for this balance, such as `USD` for a USD-denominated token.',
|
|
103
101
|
),
|
|
104
|
-
z.meta({ examples: ['
|
|
102
|
+
z.meta({ examples: ['USD'] }),
|
|
105
103
|
),
|
|
106
|
-
|
|
107
|
-
.
|
|
104
|
+
decimals: z
|
|
105
|
+
.number()
|
|
108
106
|
.check(
|
|
107
|
+
z.int(),
|
|
108
|
+
z.nonnegative(),
|
|
109
109
|
z.describe(
|
|
110
|
-
'
|
|
110
|
+
'The number of decimal places used to convert this balance between base units and human-readable form.',
|
|
111
111
|
),
|
|
112
|
-
z.meta({ examples: [
|
|
112
|
+
z.meta({ examples: [6] }),
|
|
113
113
|
),
|
|
114
114
|
feeEligible: z
|
|
115
115
|
.optional(z.boolean())
|
|
@@ -123,11 +123,51 @@ export namespace schema {
|
|
|
123
123
|
),
|
|
124
124
|
z.meta({ examples: [true] }),
|
|
125
125
|
),
|
|
126
|
+
formatted: z
|
|
127
|
+
.string()
|
|
128
|
+
.check(
|
|
129
|
+
z.describe(
|
|
130
|
+
'The same balance in human-readable decimal form, using this balance’s `decimals`.',
|
|
131
|
+
),
|
|
132
|
+
z.meta({ examples: ['1.5'] }),
|
|
133
|
+
),
|
|
134
|
+
id: z
|
|
135
|
+
.string()
|
|
136
|
+
.check(
|
|
137
|
+
z.describe(
|
|
138
|
+
'A stable resource ID for this balance, equal to the token contract address.',
|
|
139
|
+
),
|
|
140
|
+
z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] }),
|
|
141
|
+
),
|
|
126
142
|
token: Tokens.schema.Token,
|
|
143
|
+
valuation: z
|
|
144
|
+
.nullable(Valuation.schema.Value)
|
|
145
|
+
.check(
|
|
146
|
+
z.describe(
|
|
147
|
+
'The holding’s nominal value in the requested `denomination`, or `null` when the ' +
|
|
148
|
+
'token is unverified, its display currency has no rate, or rates were unavailable.',
|
|
149
|
+
),
|
|
150
|
+
),
|
|
127
151
|
}),
|
|
128
152
|
'One TIP-20 token balance held by this account, with both raw and human-readable amounts.',
|
|
129
153
|
)
|
|
130
154
|
|
|
155
|
+
/** Page-level resources: opt-in counts plus valuation rate provenance. */
|
|
156
|
+
export const Meta = z
|
|
157
|
+
.object({
|
|
158
|
+
pricing: z
|
|
159
|
+
.optional(Valuation.schema.Pricing)
|
|
160
|
+
.check(
|
|
161
|
+
z.describe(
|
|
162
|
+
'Rate provenance for row valuations. Present when conversion rates were ' +
|
|
163
|
+
'consulted; absent when every value was identity-valued or rates were unavailable.',
|
|
164
|
+
),
|
|
165
|
+
),
|
|
166
|
+
totalCount: z.optional(Schema.TotalCount),
|
|
167
|
+
totalCountCapped: z.optional(Schema.TotalCountCapped),
|
|
168
|
+
})
|
|
169
|
+
.check(z.describe('Page-level resources attached to this response.'))
|
|
170
|
+
|
|
131
171
|
/** Page of TIP-20 token balances held by an address, ordered by amount descending. */
|
|
132
172
|
export const Response = z
|
|
133
173
|
.object({
|
|
@@ -135,8 +175,8 @@ export namespace schema {
|
|
|
135
175
|
.array(Balance)
|
|
136
176
|
.check(z.describe('The balances in this page, ordered from largest to smallest amount.')),
|
|
137
177
|
meta: z
|
|
138
|
-
.optional(
|
|
139
|
-
.check(z.describe('Page-level resources
|
|
178
|
+
.optional(Meta)
|
|
179
|
+
.check(z.describe('Page-level resources: opt-in counts and valuation rate provenance.')),
|
|
140
180
|
nextCursor: Schema.NextCursor,
|
|
141
181
|
})
|
|
142
182
|
.check(z.describe('A page of TIP-20 token balances held by this account.'))
|
|
@@ -147,7 +187,8 @@ export namespace schema {
|
|
|
147
187
|
* Creates address-scoped balance handlers, mounted under the `/addresses`
|
|
148
188
|
* composer. Exposes `GET /:address/balances`, the held-balance page.
|
|
149
189
|
*/
|
|
150
|
-
export function addresses() {
|
|
190
|
+
export function addresses(options: addresses.Options = {}) {
|
|
191
|
+
const oracle = options.fx?.oracle ?? FxOracle.ecb()
|
|
151
192
|
return new Hono<App.Environment>().get(
|
|
152
193
|
'/v1/addresses/:address/balances',
|
|
153
194
|
Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
|
|
@@ -161,10 +202,10 @@ export function addresses() {
|
|
|
161
202
|
}),
|
|
162
203
|
OpenApi.describeRoute({
|
|
163
204
|
description:
|
|
164
|
-
'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.',
|
|
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.',
|
|
165
206
|
operationId: 'getAddressBalances',
|
|
166
207
|
responses: OpenApi.responses({
|
|
167
|
-
errors: { 502: '
|
|
208
|
+
errors: { 502: 'Could not read balance or token data from an upstream service.' },
|
|
168
209
|
success: {
|
|
169
210
|
description: 'A page of token balances for this account.',
|
|
170
211
|
schema: schema.getAddressBalances.Response,
|
|
@@ -218,32 +259,57 @@ export function addresses() {
|
|
|
218
259
|
chainId,
|
|
219
260
|
currency: query.currency,
|
|
220
261
|
cursor: query.cursor,
|
|
262
|
+
denomination: query.denomination ?? 'USD',
|
|
221
263
|
feeEligible,
|
|
222
264
|
feeTokens,
|
|
223
265
|
includeTotalCount: query.include.includes('totalCount'),
|
|
224
266
|
limit: query.limit,
|
|
267
|
+
oracle,
|
|
225
268
|
page: query.page,
|
|
226
269
|
verified,
|
|
227
270
|
})
|
|
228
271
|
|
|
229
272
|
return c.json(Response.validated(schema.getAddressBalances.Response, balances), 200)
|
|
230
273
|
} catch (cause) {
|
|
274
|
+
if (cause instanceof Valuation.UnsupportedDenominationError)
|
|
275
|
+
return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
|
|
231
276
|
return Response.upstream(c, cause)
|
|
232
277
|
}
|
|
233
278
|
},
|
|
234
279
|
)
|
|
235
280
|
}
|
|
236
281
|
|
|
282
|
+
export declare namespace addresses {
|
|
283
|
+
/** Options for the address-scoped balance handlers. */
|
|
284
|
+
type Options = {
|
|
285
|
+
/** FX configuration backing per-row valuation. */
|
|
286
|
+
fx?: Valuation.addresses.Fx | undefined
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
237
290
|
async function getAddressBalances(
|
|
238
291
|
c: Context<App.Environment>,
|
|
239
292
|
options: getAddressBalances.Options,
|
|
240
293
|
) {
|
|
241
|
-
const {
|
|
242
|
-
|
|
294
|
+
const {
|
|
295
|
+
address,
|
|
296
|
+
chainId,
|
|
297
|
+
currency,
|
|
298
|
+
denomination,
|
|
299
|
+
feeEligible,
|
|
300
|
+
feeTokens,
|
|
301
|
+
includeTotalCount,
|
|
302
|
+
limit,
|
|
303
|
+
oracle,
|
|
304
|
+
verified,
|
|
305
|
+
} = options
|
|
243
306
|
const tidx = c.get('getTidx')(chainId)
|
|
244
307
|
const cursor = options.cursor ? Cursor.decode(options.cursor, ['uint', 'address']) : undefined
|
|
245
308
|
const offset = options.page !== undefined && options.page > 1 ? (options.page - 1) * limit : 0
|
|
246
309
|
|
|
310
|
+
// Validate the requested denomination independently of the current page.
|
|
311
|
+
await Valuation.ratesFor(c, { currencies: [], denomination, oracle })
|
|
312
|
+
|
|
247
313
|
// Build the optional `token IN (…)` restriction before hitting TIDX. The
|
|
248
314
|
// verified/`currency` filter and the `feeEligible` filter each contribute a set
|
|
249
315
|
// of allowed addresses (`currency` matched case-insensitively so callers can
|
|
@@ -338,58 +404,76 @@ async function getAddressBalances(
|
|
|
338
404
|
nextCursor: page.nextCursor,
|
|
339
405
|
}
|
|
340
406
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
407
|
+
const rates = await Valuation.ratesFor(c, {
|
|
408
|
+
currencies: page.rows.flatMap((row) => {
|
|
409
|
+
const held = snapshot.byAddress.get(row.address.toLowerCase())?.currency
|
|
410
|
+
return held === undefined ? [] : [held]
|
|
411
|
+
}),
|
|
412
|
+
denomination,
|
|
413
|
+
oracle,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// Balance tokens retain their logo and verification metadata by default.
|
|
351
417
|
const resolved = await Timing.time(c, 'tokens', () =>
|
|
352
418
|
Promise.all(
|
|
353
|
-
page.rows.map((holding) =>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
419
|
+
page.rows.map(async (holding) => {
|
|
420
|
+
try {
|
|
421
|
+
const token = Schema.Address.parse(holding.address.toLowerCase())
|
|
422
|
+
const [logoUri, metadata] = await Promise.all([
|
|
423
|
+
Timing.time(c, 'token_logo', () =>
|
|
424
|
+
Tokens.getTokenLogo(c, { address: token, chainId }),
|
|
425
|
+
).catch(() => undefined),
|
|
426
|
+
Timing.time(c, 'token_metadata', () =>
|
|
427
|
+
Tokens.getTokenMetadata(c, { address: token, chainId }),
|
|
428
|
+
),
|
|
429
|
+
])
|
|
430
|
+
return { amount: holding.amount, logoUri, metadata, token }
|
|
431
|
+
} catch {
|
|
432
|
+
return undefined
|
|
433
|
+
}
|
|
434
|
+
}),
|
|
362
435
|
),
|
|
363
436
|
)
|
|
364
437
|
|
|
365
438
|
const data: z.output<typeof schema.getAddressBalances.Balance>[] = []
|
|
366
439
|
for (const entry of resolved) {
|
|
367
|
-
if (!entry
|
|
440
|
+
if (!entry) continue
|
|
368
441
|
data.push({
|
|
369
442
|
amount: entry.amount,
|
|
370
|
-
|
|
371
|
-
|
|
443
|
+
currency: entry.metadata.currency,
|
|
444
|
+
decimals: entry.metadata.decimals,
|
|
372
445
|
// Best-effort: omit `feeEligible` entirely when the fee-token set was
|
|
373
446
|
// unavailable, rather than reporting a misleading `false`.
|
|
374
|
-
...(feeTokens ? { feeEligible: feeTokens.has(entry.
|
|
447
|
+
...(feeTokens ? { feeEligible: feeTokens.has(entry.token.toLowerCase()) } : {}),
|
|
448
|
+
formatted: core_Value.format(BigInt(entry.amount), entry.metadata.decimals),
|
|
449
|
+
id: entry.token,
|
|
375
450
|
token: {
|
|
376
|
-
address: entry.
|
|
451
|
+
address: entry.token,
|
|
377
452
|
currency: entry.metadata.currency,
|
|
378
453
|
decimals: entry.metadata.decimals,
|
|
379
|
-
id: entry.
|
|
380
|
-
logoUri:
|
|
454
|
+
id: entry.token,
|
|
455
|
+
logoUri:
|
|
456
|
+
entry.logoUri ?? snapshot.byAddress.get(entry.token)?.logoUri ?? entry.metadata.logoUri,
|
|
381
457
|
name: entry.metadata.name,
|
|
382
458
|
symbol: entry.metadata.symbol,
|
|
383
|
-
verified: entry.
|
|
459
|
+
verified: snapshot.byAddress.has(entry.token),
|
|
384
460
|
},
|
|
461
|
+
valuation: Valuation.valuationFor({
|
|
462
|
+
amount: BigInt(entry.amount),
|
|
463
|
+
denomination,
|
|
464
|
+
rates,
|
|
465
|
+
token: snapshot.byAddress.get(entry.token),
|
|
466
|
+
}),
|
|
385
467
|
})
|
|
386
468
|
}
|
|
387
469
|
|
|
470
|
+
const meta = {
|
|
471
|
+
...(rates ? { pricing: Valuation.pricing(rates, oracle) } : {}),
|
|
472
|
+
...(includeTotalCount ? { totalCount: corrected.length, totalCountCapped: false } : {}),
|
|
473
|
+
}
|
|
388
474
|
return {
|
|
389
475
|
data,
|
|
390
|
-
...(
|
|
391
|
-
? { meta: { totalCount: corrected.length, totalCountCapped: false } }
|
|
392
|
-
: {}),
|
|
476
|
+
...(Object.keys(meta).length > 0 ? { meta } : {}),
|
|
393
477
|
nextCursor: page.nextCursor,
|
|
394
478
|
}
|
|
395
479
|
}
|
|
@@ -402,6 +486,8 @@ declare namespace getAddressBalances {
|
|
|
402
486
|
currency?: string | undefined
|
|
403
487
|
/** Opaque keyset cursor anchoring the page; omit for the head page. */
|
|
404
488
|
cursor?: string | undefined
|
|
489
|
+
/** Currency the per-row valuations are denominated in (uppercase). */
|
|
490
|
+
denomination: string
|
|
405
491
|
/** When true, restrict the page to fee-usable tokens (members of `feeTokens`). */
|
|
406
492
|
feeEligible?: boolean | undefined
|
|
407
493
|
/**
|
|
@@ -413,6 +499,8 @@ declare namespace getAddressBalances {
|
|
|
413
499
|
/** When true, include the corrected candidate count in response metadata. */
|
|
414
500
|
includeTotalCount: boolean
|
|
415
501
|
limit: number
|
|
502
|
+
/** FX rate oracle backing per-row valuation. */
|
|
503
|
+
oracle: FxOracle.Oracle
|
|
416
504
|
/** 1-indexed page number (positional pagination; exclusive with `cursor`). */
|
|
417
505
|
page?: number | undefined
|
|
418
506
|
/** When true, restrict the page to addresses in the curated verified list. */
|
|
@@ -433,6 +521,7 @@ function allowedTokens(options: allowedTokens.Options): string[] | undefined {
|
|
|
433
521
|
const { currency, feeEligible, feeTokens, snapshot, verified } = options
|
|
434
522
|
let allowed: string[] | undefined
|
|
435
523
|
if (verified) {
|
|
524
|
+
if (!snapshot) throw new Error('Unable to resolve verified token metadata')
|
|
436
525
|
const matching = currency
|
|
437
526
|
? (snapshot.byCurrency.get(currency.toLowerCase()) ?? [])
|
|
438
527
|
: snapshot.list
|
|
@@ -459,7 +548,7 @@ declare namespace allowedTokens {
|
|
|
459
548
|
/** The chain's fee-token set, if resolved (best-effort). */
|
|
460
549
|
feeTokens?: ReadonlySet<string> | undefined
|
|
461
550
|
/** Compiled verified-token snapshot for the chain. */
|
|
462
|
-
snapshot
|
|
551
|
+
snapshot?: VerifiedTokens.Snapshot | undefined
|
|
463
552
|
/** When true, restrict to addresses in the curated verified list. */
|
|
464
553
|
verified?: boolean | undefined
|
|
465
554
|
}
|