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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import { Value as core_Value } from 'ox'
|
|
3
|
+
import { Schema } from 'tapimo'
|
|
4
|
+
import { FxOracle } from 'tapimo/apps'
|
|
5
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
6
|
+
import { Actions } from 'viem/tempo'
|
|
7
|
+
|
|
8
|
+
import * as TestApp from '../../../../test/App.js'
|
|
9
|
+
import * as Runtime from '../../../../test/runtime.js'
|
|
10
|
+
import * as TestViem from '../../../../test/Viem.js'
|
|
11
|
+
import * as Tokens from './tokens.js'
|
|
12
|
+
import * as Valuation from './valuation.js'
|
|
13
|
+
|
|
14
|
+
const runtime = Runtime.get()
|
|
15
|
+
const db = TestApp.database()
|
|
16
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
17
|
+
const ecbXml = `<Envelope><Cube><Cube time="2026-01-01"><Cube currency="AUD" rate="1.6"/><Cube currency="USD" rate="1.25"/></Cube></Cube></Envelope>`
|
|
18
|
+
const ecb = FxOracle.ecb({
|
|
19
|
+
url: `data:application/xml,${encodeURIComponent(ecbXml)}`,
|
|
20
|
+
})
|
|
21
|
+
function app(options: TestApp.create.Options = {}) {
|
|
22
|
+
return TestApp.client({ db, fx: { oracle: ecb }, ...options })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Verified snapshots cache per isolate and `verifiedSeed` primes that cache
|
|
26
|
+
// (read-your-write), so tests that depend on a specific list re-seed at their
|
|
27
|
+
// own start. `usdDb` restricts the list to pathUSD so every holding shares one
|
|
28
|
+
// currency, independent of which seed tokens exist on the dev chain.
|
|
29
|
+
const usdDb = TestApp.database()
|
|
30
|
+
const usdTokens = [
|
|
31
|
+
{
|
|
32
|
+
address: TestApp.tokenWithHolders,
|
|
33
|
+
currency: 'USD',
|
|
34
|
+
decimals: 6,
|
|
35
|
+
name: 'PathUSD',
|
|
36
|
+
symbol: 'pathUSD',
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
// Curated seed backing `verifiedSeed`, used to recompute expected sums from
|
|
41
|
+
// chain state. Localnet reuses the Moderato testnet list (see `verifiedSeed`).
|
|
42
|
+
type SeedToken = { address: `0x${string}`; currency: string; decimals: number }
|
|
43
|
+
const seedChainId = runtime.chainId === 1337 ? 42431 : runtime.chainId
|
|
44
|
+
const seed: readonly SeedToken[] = JSON.parse(
|
|
45
|
+
fs.readFileSync(
|
|
46
|
+
new URL(`../../../../seeds/${seedChainId}/verified-tokens.json`, import.meta.url),
|
|
47
|
+
'utf8',
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
52
|
+
const fixed = FxOracle.from({
|
|
53
|
+
name: 'fixed',
|
|
54
|
+
rates: async () => ({ asOf: '2026-01-01', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' } }),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
async function holderWithBalance(client: ReturnType<typeof app>) {
|
|
58
|
+
if (runtime.mode === 'localnet')
|
|
59
|
+
return runtime.fixtures!.fundedAccounts[0]!.toLowerCase() as `0x${string}`
|
|
60
|
+
const holdersResponse = await client.v1.tokens[':token'].holders.$get(
|
|
61
|
+
{ param: { token: TestApp.tokenWithHolders }, query: { limit: '5' } },
|
|
62
|
+
TestApp.auth,
|
|
63
|
+
)
|
|
64
|
+
const holders = await TestApp.json(holdersResponse, Tokens.schema.getTokenHolders.Response)
|
|
65
|
+
expect(holdersResponse.status).toBe(200)
|
|
66
|
+
const holder = holders.data[0]?.address
|
|
67
|
+
if (!holder) throw new Error('expected at least one holder')
|
|
68
|
+
return holder
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Fresh account address guaranteed to hold nothing. */
|
|
72
|
+
function emptyAccount() {
|
|
73
|
+
return privateKeyToAccount(generatePrivateKey()).address.toLowerCase() as `0x${string}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sums the holder's on-chain balances across curated tokens of one currency,
|
|
78
|
+
* in 6-dp base units. Unreadable tokens (undeployed or uninitialized on this
|
|
79
|
+
* chain) count as empty, matching the endpoint's declared scope.
|
|
80
|
+
*/
|
|
81
|
+
async function chainSum(holder: `0x${string}`, currency: string) {
|
|
82
|
+
const rpc = TestViem.getClient()
|
|
83
|
+
const amounts = await Promise.all(
|
|
84
|
+
seed
|
|
85
|
+
.filter((token) => token.currency === currency)
|
|
86
|
+
.map((token) =>
|
|
87
|
+
Actions.token
|
|
88
|
+
.getBalance(rpc, { account: holder, decimals: token.decimals, token: token.address })
|
|
89
|
+
.catch(() => ({ amount: 0n })),
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
return amounts.reduce((sum, { amount }) => sum + amount, 0n)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('GET /addresses/:address/valuation', () => {
|
|
96
|
+
test('normalizes the denomination', () => {
|
|
97
|
+
expect(
|
|
98
|
+
Valuation.schema.getAddressValuation.Query.parse({ denomination: 'aud' }).denomination,
|
|
99
|
+
).toMatchInlineSnapshot(`"AUD"`)
|
|
100
|
+
expect(
|
|
101
|
+
Valuation.schema.getAddressValuation.Query.safeParse({ denomination: 'DOLLARS' }).success,
|
|
102
|
+
).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('values verified holdings in the default denomination', async () => {
|
|
106
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
107
|
+
const client = app()
|
|
108
|
+
const holder = await holderWithBalance(client)
|
|
109
|
+
|
|
110
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
111
|
+
{ param: { address: holder }, query: {} },
|
|
112
|
+
TestApp.auth,
|
|
113
|
+
)
|
|
114
|
+
const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response)
|
|
115
|
+
|
|
116
|
+
expect(response.status).toBe(200)
|
|
117
|
+
expect(response.headers.get('server-timing')?.includes('valuation_balances;dur=')).toBe(true)
|
|
118
|
+
expect(body.address).toBe(holder)
|
|
119
|
+
expect(body.id).toBe(holder)
|
|
120
|
+
expect(body.currency).toBe('USD')
|
|
121
|
+
// The total is the exact on-chain sum of the curated USD holdings; every
|
|
122
|
+
// non-USD currency in the seed is unpriced by ECB, so none of it leaks in.
|
|
123
|
+
expect(body.amount).toBe(core_Value.format(await chainSum(holder, 'USD'), 6))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('returns a zero valuation for an address with no holdings', async () => {
|
|
127
|
+
const account = emptyAccount()
|
|
128
|
+
const response = await app().v1.addresses[':address'].valuation.$get(
|
|
129
|
+
{ param: { address: account }, query: {} },
|
|
130
|
+
TestApp.auth,
|
|
131
|
+
)
|
|
132
|
+
const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response)
|
|
133
|
+
const { address, id, ...stable } = body
|
|
134
|
+
|
|
135
|
+
expect(response.status).toBe(200)
|
|
136
|
+
expect(address).toBe(account)
|
|
137
|
+
expect(id).toBe(account)
|
|
138
|
+
expect(stable).toMatchInlineSnapshot(`
|
|
139
|
+
{
|
|
140
|
+
"amount": "0",
|
|
141
|
+
"currency": "USD",
|
|
142
|
+
"pricing": null,
|
|
143
|
+
"unpriced": [],
|
|
144
|
+
}
|
|
145
|
+
`)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test.skipIf(runtime.mode !== 'localnet')(
|
|
149
|
+
'converts holdings into a requested denomination',
|
|
150
|
+
async () => {
|
|
151
|
+
await TestApp.verifiedSeed(usdDb, runtime.chainId, { tokens: usdTokens })
|
|
152
|
+
const client = app({ db: usdDb, fx: { oracle: fixed } })
|
|
153
|
+
const holder = await holderWithBalance(client)
|
|
154
|
+
|
|
155
|
+
const usdResponse = await client.v1.addresses[':address'].valuation.$get(
|
|
156
|
+
{ param: { address: holder }, query: {} },
|
|
157
|
+
TestApp.auth,
|
|
158
|
+
)
|
|
159
|
+
const usd = await TestApp.json(usdResponse, Valuation.schema.getAddressValuation.Response)
|
|
160
|
+
// Denominations are case-insensitive.
|
|
161
|
+
const audResponse = await client.v1.addresses[':address'].valuation.$get(
|
|
162
|
+
{ param: { address: holder }, query: { denomination: 'aud' } },
|
|
163
|
+
TestApp.auth,
|
|
164
|
+
)
|
|
165
|
+
const aud = await TestApp.json(audResponse, Valuation.schema.getAddressValuation.Response)
|
|
166
|
+
|
|
167
|
+
expect(usdResponse.status).toBe(200)
|
|
168
|
+
expect(audResponse.status).toBe(200)
|
|
169
|
+
// Every verified holding is USD here, so the USD valuation needs no rates.
|
|
170
|
+
expect(usd.pricing).toBeNull()
|
|
171
|
+
expect(Number(usd.amount)).toBeGreaterThan(0)
|
|
172
|
+
|
|
173
|
+
// USD -> AUD at the fixed 1.6 cross rate, floored to 6 dp.
|
|
174
|
+
const sum = core_Value.from(usd.amount, 6)
|
|
175
|
+
expect(aud.currency).toBe('AUD')
|
|
176
|
+
expect(aud.amount).toBe(core_Value.format((sum * 16n) / 10n, 6))
|
|
177
|
+
expect(aud.pricing).toMatchInlineSnapshot(`
|
|
178
|
+
{
|
|
179
|
+
"asOf": "2026-01-01",
|
|
180
|
+
"basis": "nominal",
|
|
181
|
+
"source": "fixed",
|
|
182
|
+
}
|
|
183
|
+
`)
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
test('converts via the ECB oracle', async () => {
|
|
188
|
+
await TestApp.verifiedSeed(db, runtime.chainId)
|
|
189
|
+
const client = app()
|
|
190
|
+
const holder = await holderWithBalance(client)
|
|
191
|
+
|
|
192
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
193
|
+
{ param: { address: holder }, query: { denomination: 'EUR' } },
|
|
194
|
+
TestApp.auth,
|
|
195
|
+
)
|
|
196
|
+
const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response)
|
|
197
|
+
|
|
198
|
+
expect(response.status).toBe(200)
|
|
199
|
+
expect(response.headers.get('server-timing')?.includes('valuation_rates;dur=')).toBe(true)
|
|
200
|
+
expect(body.currency).toBe('EUR')
|
|
201
|
+
expect(Number(body.amount)).toBeGreaterThan(0)
|
|
202
|
+
expect(body.pricing?.basis).toBe('nominal')
|
|
203
|
+
expect(body.pricing?.source).toBe('ecb')
|
|
204
|
+
expect(body.pricing?.asOf).toMatch(/^\d{4}-\d{2}-\d{2}$/)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test.skipIf(runtime.mode !== 'localnet')(
|
|
208
|
+
'discloses holdings the oracle cannot price',
|
|
209
|
+
async () => {
|
|
210
|
+
// Publish a curated list where one genuinely held token carries a
|
|
211
|
+
// non-fiat display currency the oracle has no rate for.
|
|
212
|
+
const rpc = TestViem.getClient()
|
|
213
|
+
const holder = runtime.fixtures!.fundedAccounts[0]!.toLowerCase() as `0x${string}`
|
|
214
|
+
const held = await (async () => {
|
|
215
|
+
for (const token of runtime.fixtures!.transferTokenAddresses) {
|
|
216
|
+
if (token.toLowerCase() === TestApp.tokenWithHolders) continue
|
|
217
|
+
const { amount } = await Actions.token.getBalance(rpc, { account: holder, token })
|
|
218
|
+
if (amount > 0n) return token.toLowerCase() as `0x${string}`
|
|
219
|
+
}
|
|
220
|
+
throw new Error('expected a held fixture token')
|
|
221
|
+
})()
|
|
222
|
+
|
|
223
|
+
const unpricedDb = TestApp.database()
|
|
224
|
+
await TestApp.verifiedSeed(unpricedDb, runtime.chainId, {
|
|
225
|
+
tokens: [
|
|
226
|
+
{
|
|
227
|
+
address: TestApp.tokenWithHolders,
|
|
228
|
+
currency: 'USD',
|
|
229
|
+
decimals: 6,
|
|
230
|
+
name: 'PathUSD',
|
|
231
|
+
symbol: 'pathUSD',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
address: held,
|
|
235
|
+
currency: 'sUSDe',
|
|
236
|
+
decimals: 6,
|
|
237
|
+
name: 'Staked USDe',
|
|
238
|
+
symbol: 'sUSDe',
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
const client = app({ db: unpricedDb, fx: { oracle: fixed } })
|
|
243
|
+
|
|
244
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
245
|
+
{ param: { address: holder }, query: {} },
|
|
246
|
+
TestApp.auth,
|
|
247
|
+
)
|
|
248
|
+
const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response)
|
|
249
|
+
|
|
250
|
+
expect(response.status).toBe(200)
|
|
251
|
+
// The unpriced holding is excluded from the total and disclosed.
|
|
252
|
+
expect(body.unpriced).toMatchInlineSnapshot(`
|
|
253
|
+
[
|
|
254
|
+
"sUSDe",
|
|
255
|
+
]
|
|
256
|
+
`)
|
|
257
|
+
expect(body.pricing?.source).toBe('fixed')
|
|
258
|
+
const { amount } = await Actions.token.getBalance(rpc, {
|
|
259
|
+
account: holder,
|
|
260
|
+
token: TestApp.tokenWithHolders,
|
|
261
|
+
})
|
|
262
|
+
expect(body.amount).toBe(core_Value.format(amount, 6))
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
test.skipIf(runtime.mode !== 'localnet')(
|
|
267
|
+
'ignores the FX oracle when no conversion is needed',
|
|
268
|
+
async () => {
|
|
269
|
+
// Every verified holding is already USD, so an unreachable oracle must
|
|
270
|
+
// not affect the default valuation.
|
|
271
|
+
await TestApp.verifiedSeed(usdDb, runtime.chainId, { tokens: usdTokens })
|
|
272
|
+
const client = app({
|
|
273
|
+
db: usdDb,
|
|
274
|
+
fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) },
|
|
275
|
+
})
|
|
276
|
+
const holder = await holderWithBalance(client)
|
|
277
|
+
|
|
278
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
279
|
+
{ param: { address: holder }, query: {} },
|
|
280
|
+
TestApp.auth,
|
|
281
|
+
)
|
|
282
|
+
const body = await TestApp.json(response, Valuation.schema.getAddressValuation.Response)
|
|
283
|
+
|
|
284
|
+
expect(response.status).toBe(200)
|
|
285
|
+
expect(body.pricing).toBeNull()
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
test.skipIf(runtime.mode !== 'localnet')(
|
|
290
|
+
'surfaces a 502 when rates are unavailable',
|
|
291
|
+
async () => {
|
|
292
|
+
const client = app({ fx: { oracle: FxOracle.ecb({ url: 'http://127.0.0.1:1' }) } })
|
|
293
|
+
const holder = await holderWithBalance(client)
|
|
294
|
+
|
|
295
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
296
|
+
{ param: { address: holder }, query: { denomination: 'AUD' } },
|
|
297
|
+
TestApp.auth,
|
|
298
|
+
)
|
|
299
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
300
|
+
const { requestId, error } = body
|
|
301
|
+
const { message, ...stableError } = error
|
|
302
|
+
|
|
303
|
+
// Rate failures report honestly rather than serving a partial total.
|
|
304
|
+
expect(response.status).toMatchInlineSnapshot(`502`)
|
|
305
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
306
|
+
expect(typeof message).toMatchInlineSnapshot(`"string"`)
|
|
307
|
+
expect(stableError).toMatchInlineSnapshot(`
|
|
308
|
+
{
|
|
309
|
+
"code": "upstream_error",
|
|
310
|
+
}
|
|
311
|
+
`)
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
test.skipIf(runtime.mode !== 'localnet')(
|
|
316
|
+
'rejects denominations the oracle does not price',
|
|
317
|
+
async () => {
|
|
318
|
+
const client = app({ fx: { oracle: fixed } })
|
|
319
|
+
const holder = await holderWithBalance(client)
|
|
320
|
+
|
|
321
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
322
|
+
{ param: { address: holder }, query: { denomination: 'JPY' } },
|
|
323
|
+
TestApp.auth,
|
|
324
|
+
)
|
|
325
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
326
|
+
const { requestId, ...stable } = body
|
|
327
|
+
|
|
328
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
329
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
330
|
+
expect(stable).toMatchInlineSnapshot(`
|
|
331
|
+
{
|
|
332
|
+
"error": {
|
|
333
|
+
"code": "query_invalid",
|
|
334
|
+
"message": "Denomination "JPY" is not priced by the "fixed" FX oracle",
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
`)
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
test('rejects unsupported denominations for an address with no holdings', async () => {
|
|
342
|
+
const response = await app({ fx: { oracle: fixed } }).v1.addresses[':address'].valuation.$get(
|
|
343
|
+
{ param: { address: emptyAccount() }, query: { denomination: 'JPY' } },
|
|
344
|
+
TestApp.auth,
|
|
345
|
+
)
|
|
346
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
347
|
+
const { requestId, ...stable } = body
|
|
348
|
+
|
|
349
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
350
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
351
|
+
expect(stable).toMatchInlineSnapshot(`
|
|
352
|
+
{
|
|
353
|
+
"error": {
|
|
354
|
+
"code": "query_invalid",
|
|
355
|
+
"message": "Denomination "JPY" is not priced by the "fixed" FX oracle",
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
`)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('surfaces a 502 when balance reads are unavailable', async () => {
|
|
362
|
+
const client = app({ rpc: { url: () => 'http://127.0.0.1:1' } })
|
|
363
|
+
const response = await client.v1.addresses[':address'].valuation.$get(
|
|
364
|
+
{ param: { address: emptyAccount() }, query: {} },
|
|
365
|
+
TestApp.auth,
|
|
366
|
+
)
|
|
367
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
368
|
+
const { requestId, error } = body
|
|
369
|
+
const { message, ...stableError } = error
|
|
370
|
+
|
|
371
|
+
expect(response.status).toMatchInlineSnapshot(`502`)
|
|
372
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
373
|
+
expect(typeof message).toMatchInlineSnapshot(`"string"`)
|
|
374
|
+
expect(stableError).toMatchInlineSnapshot(`
|
|
375
|
+
{
|
|
376
|
+
"code": "upstream_error",
|
|
377
|
+
}
|
|
378
|
+
`)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
test('rejects invalid account addresses', async () => {
|
|
382
|
+
// The typed client requires `address: `Hex.Hex`` so an invalid literal
|
|
383
|
+
// cannot pass through it; exercise the validator via `app.request`.
|
|
384
|
+
const response = await TestApp.create().request('/v1/addresses/not-an-address/valuation', {
|
|
385
|
+
headers: { authorization: `Bearer ${TestApp.key.token}` },
|
|
386
|
+
})
|
|
387
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
388
|
+
const { requestId, ...stable } = body
|
|
389
|
+
|
|
390
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
391
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
392
|
+
expect(stable).toMatchInlineSnapshot(`
|
|
393
|
+
{
|
|
394
|
+
"error": {
|
|
395
|
+
"code": "address_invalid",
|
|
396
|
+
"details": [
|
|
397
|
+
{
|
|
398
|
+
"message": "Invalid input",
|
|
399
|
+
"path": [
|
|
400
|
+
"address",
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
"message": "Invalid account address",
|
|
405
|
+
},
|
|
406
|
+
}
|
|
407
|
+
`)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('rejects malformed denominations', async () => {
|
|
411
|
+
const response = await app().v1.addresses[':address'].valuation.$get(
|
|
412
|
+
{ param: { address: emptyAccount() }, query: { denomination: 'DOLLARS' } },
|
|
413
|
+
TestApp.auth,
|
|
414
|
+
)
|
|
415
|
+
const body = await TestApp.json(response, Schema.ErrorResponse)
|
|
416
|
+
const { requestId, ...stable } = body
|
|
417
|
+
|
|
418
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
419
|
+
expect(typeof requestId).toMatchInlineSnapshot(`"string"`)
|
|
420
|
+
expect(stable.error.code).toMatchInlineSnapshot(`"query_invalid"`)
|
|
421
|
+
})
|
|
422
|
+
})
|