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.
Files changed (299) hide show
  1. package/dist/App.d.ts +13987 -5755
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/admin/App.d.ts +910 -412
  5. package/dist/admin/App.d.ts.map +1 -1
  6. package/dist/admin/App.js +4 -0
  7. package/dist/admin/App.js.map +1 -1
  8. package/dist/admin/apps/early-access.d.ts +148 -0
  9. package/dist/admin/apps/early-access.d.ts.map +1 -0
  10. package/dist/admin/apps/early-access.js +133 -0
  11. package/dist/admin/apps/early-access.js.map +1 -0
  12. package/dist/admin/apps/verified-tokens.d.ts +643 -118
  13. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  14. package/dist/admin/apps/verified-tokens.js +214 -1
  15. package/dist/admin/apps/verified-tokens.js.map +1 -1
  16. package/dist/admin/ui.generated.d.ts +1 -1
  17. package/dist/admin/ui.generated.d.ts.map +1 -1
  18. package/dist/admin/ui.generated.js +45 -33
  19. package/dist/admin/ui.generated.js.map +1 -1
  20. package/dist/apps/data/App.d.ts +3733 -2428
  21. package/dist/apps/data/App.d.ts.map +1 -1
  22. package/dist/apps/data/App.js +11 -6
  23. package/dist/apps/data/App.js.map +1 -1
  24. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  25. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  26. package/dist/apps/data/FundingQuoteProviders.js +193 -2
  27. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  28. package/dist/apps/data/FxOracle.d.ts +71 -0
  29. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  30. package/dist/apps/data/FxOracle.js +97 -0
  31. package/dist/apps/data/FxOracle.js.map +1 -0
  32. package/dist/apps/data/index.d.ts +1 -0
  33. package/dist/apps/data/index.d.ts.map +1 -1
  34. package/dist/apps/data/index.js +1 -0
  35. package/dist/apps/data/index.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +3575 -1358
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +230 -88
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +963 -342
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/addresses.js +11 -9
  43. package/dist/apps/data/routes/addresses.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +108 -57
  45. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.js +88 -45
  47. package/dist/apps/data/routes/balances.js.map +1 -1
  48. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  49. package/dist/apps/data/routes/exchanges.d.ts +952 -201
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +518 -76
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  54. package/dist/apps/data/routes/funding.d.ts +8 -2
  55. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  56. package/dist/apps/data/routes/funding.js +32 -6
  57. package/dist/apps/data/routes/funding.js.map +1 -1
  58. package/dist/apps/data/routes/indexer.d.ts +5 -5
  59. package/dist/apps/data/routes/receipts.d.ts +392 -315
  60. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  61. package/dist/apps/data/routes/receipts.js +152 -77
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/tokens.d.ts +166 -173
  64. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  65. package/dist/apps/data/routes/tokens.js +14 -12
  66. package/dist/apps/data/routes/tokens.js.map +1 -1
  67. package/dist/apps/data/routes/transactions.d.ts +277 -536
  68. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +96 -77
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts +82 -17
  72. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  73. package/dist/apps/data/routes/transfers.js +70 -13
  74. package/dist/apps/data/routes/transfers.js.map +1 -1
  75. package/dist/apps/data/routes/valuation.d.ts +307 -0
  76. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  77. package/dist/apps/data/routes/valuation.js +292 -0
  78. package/dist/apps/data/routes/valuation.js.map +1 -0
  79. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.js +7 -5
  81. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  82. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +124 -25
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/index.d.ts +1 -1
  87. package/dist/apps/index.d.ts.map +1 -1
  88. package/dist/apps/index.js +1 -1
  89. package/dist/apps/index.js.map +1 -1
  90. package/dist/apps/management/App.d.ts +8606 -1681
  91. package/dist/apps/management/App.d.ts.map +1 -1
  92. package/dist/apps/management/App.js +12 -1
  93. package/dist/apps/management/App.js.map +1 -1
  94. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  95. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  96. package/dist/apps/management/routes/api-keys.js +5 -3
  97. package/dist/apps/management/routes/api-keys.js.map +1 -1
  98. package/dist/apps/management/routes/billing.d.ts +88 -48
  99. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  100. package/dist/apps/management/routes/billing.js +10 -2
  101. package/dist/apps/management/routes/billing.js.map +1 -1
  102. package/dist/apps/management/routes/faucet.d.ts +218 -0
  103. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  104. package/dist/apps/management/routes/faucet.js +139 -0
  105. package/dist/apps/management/routes/faucet.js.map +1 -0
  106. package/dist/apps/management/routes/invitations.d.ts +121 -121
  107. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  108. package/dist/apps/management/routes/invitations.js +3 -0
  109. package/dist/apps/management/routes/invitations.js.map +1 -1
  110. package/dist/apps/management/routes/invite-links.d.ts +197 -168
  111. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  112. package/dist/apps/management/routes/invite-links.js +26 -1
  113. package/dist/apps/management/routes/invite-links.js.map +1 -1
  114. package/dist/apps/management/routes/me.d.ts +2 -0
  115. package/dist/apps/management/routes/me.d.ts.map +1 -1
  116. package/dist/apps/management/routes/me.js +4 -0
  117. package/dist/apps/management/routes/me.js.map +1 -1
  118. package/dist/apps/management/routes/members.d.ts +49 -49
  119. package/dist/apps/management/routes/orgs.d.ts +91 -91
  120. package/dist/apps/management/routes/projects.d.ts +115 -115
  121. package/dist/apps/management/routes/usage.d.ts +35 -32
  122. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  123. package/dist/apps/management/routes/usage.js +4 -0
  124. package/dist/apps/management/routes/usage.js.map +1 -1
  125. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  126. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  127. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  128. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  129. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  130. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  131. package/dist/apps/management/routes/webhooks.js +573 -0
  132. package/dist/apps/management/routes/webhooks.js.map +1 -0
  133. package/dist/apps/mcp.d.ts +9 -2
  134. package/dist/apps/mcp.d.ts.map +1 -1
  135. package/dist/apps/mcp.js +9 -4
  136. package/dist/apps/mcp.js.map +1 -1
  137. package/dist/cli/Cli.d.ts.map +1 -1
  138. package/dist/cli/Cli.js +7 -2
  139. package/dist/cli/Cli.js.map +1 -1
  140. package/dist/db/Db.d.ts +4 -0
  141. package/dist/db/Db.d.ts.map +1 -1
  142. package/dist/db/Db.js +83 -0
  143. package/dist/db/Db.js.map +1 -1
  144. package/dist/db/Schema.d.ts +41 -0
  145. package/dist/db/Schema.d.ts.map +1 -1
  146. package/dist/db/Schema.js +45 -0
  147. package/dist/db/Schema.js.map +1 -1
  148. package/dist/db/tables/earlyAccess.d.ts +50 -0
  149. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  150. package/dist/db/tables/earlyAccess.js +63 -0
  151. package/dist/db/tables/earlyAccess.js.map +1 -0
  152. package/dist/db/tables/invitations.d.ts +12 -1
  153. package/dist/db/tables/invitations.d.ts.map +1 -1
  154. package/dist/db/tables/invitations.js +23 -0
  155. package/dist/db/tables/invitations.js.map +1 -1
  156. package/dist/db/tables/inviteLinks.d.ts +10 -2
  157. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  158. package/dist/db/tables/inviteLinks.js +10 -1
  159. package/dist/db/tables/inviteLinks.js.map +1 -1
  160. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  161. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.js +2 -0
  163. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  164. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  165. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  166. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  167. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  168. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  169. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  170. package/dist/db/tables/webhookSubscriptions.js +16 -0
  171. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  172. package/dist/internal/Auth.d.ts +43 -2
  173. package/dist/internal/Auth.d.ts.map +1 -1
  174. package/dist/internal/Auth.js +60 -45
  175. package/dist/internal/Auth.js.map +1 -1
  176. package/dist/internal/FundingQuotes.d.ts +69 -15
  177. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  178. package/dist/internal/FundingQuotes.js +74 -25
  179. package/dist/internal/FundingQuotes.js.map +1 -1
  180. package/dist/internal/Log.d.ts +19 -1
  181. package/dist/internal/Log.d.ts.map +1 -1
  182. package/dist/internal/Log.js +2 -0
  183. package/dist/internal/Log.js.map +1 -1
  184. package/dist/internal/MetricSink.d.ts.map +1 -1
  185. package/dist/internal/MetricSink.js +9 -0
  186. package/dist/internal/MetricSink.js.map +1 -1
  187. package/dist/internal/OpenApi.d.ts +2 -2
  188. package/dist/internal/OpenApi.d.ts.map +1 -1
  189. package/dist/internal/OpenApi.js +3 -0
  190. package/dist/internal/OpenApi.js.map +1 -1
  191. package/dist/internal/Schema.d.ts +14 -5
  192. package/dist/internal/Schema.d.ts.map +1 -1
  193. package/dist/internal/Schema.js +23 -5
  194. package/dist/internal/Schema.js.map +1 -1
  195. package/dist/internal/Value.d.ts +18 -0
  196. package/dist/internal/Value.d.ts.map +1 -1
  197. package/dist/internal/Value.js +11 -1
  198. package/dist/internal/Value.js.map +1 -1
  199. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  200. package/dist/internal/VerifiedTokens.js +10 -1
  201. package/dist/internal/VerifiedTokens.js.map +1 -1
  202. package/dist/internal/Viem.d.ts +1 -1
  203. package/dist/internal/Webhooks.d.ts +24 -3
  204. package/dist/internal/Webhooks.d.ts.map +1 -1
  205. package/dist/internal/Webhooks.js +34 -8
  206. package/dist/internal/Webhooks.js.map +1 -1
  207. package/package.json +2 -2
  208. package/src/App.ts +9 -1
  209. package/src/Client.test-d.ts +638 -6
  210. package/src/admin/App.ts +4 -0
  211. package/src/admin/apps/early-access.test.ts +125 -0
  212. package/src/admin/apps/early-access.ts +175 -0
  213. package/src/admin/apps/verified-tokens.test.ts +201 -0
  214. package/src/admin/apps/verified-tokens.ts +532 -286
  215. package/src/admin/ui/plugin.ts +2 -1
  216. package/src/admin/ui/src/lib/early-access.ts +63 -0
  217. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  218. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  219. package/src/admin/ui/src/routes/__root.tsx +2 -1
  220. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  221. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  222. package/src/admin/ui.generated.ts +48 -33
  223. package/src/apps/data/App.ts +22 -14
  224. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  225. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  226. package/src/apps/data/FxOracle.test.ts +118 -0
  227. package/src/apps/data/FxOracle.ts +153 -0
  228. package/src/apps/data/index.ts +1 -0
  229. package/src/apps/data/routes/activities.test.ts +173 -3
  230. package/src/apps/data/routes/activities.ts +403 -132
  231. package/src/apps/data/routes/addresses.ts +19 -9
  232. package/src/apps/data/routes/balances.test.ts +144 -11
  233. package/src/apps/data/routes/balances.ts +140 -51
  234. package/src/apps/data/routes/exchanges.test.ts +540 -9
  235. package/src/apps/data/routes/exchanges.ts +690 -103
  236. package/src/apps/data/routes/funding.test.ts +84 -0
  237. package/src/apps/data/routes/funding.ts +38 -6
  238. package/src/apps/data/routes/receipts.test.ts +125 -23
  239. package/src/apps/data/routes/receipts.ts +205 -100
  240. package/src/apps/data/routes/tokens.test.ts +43 -0
  241. package/src/apps/data/routes/tokens.ts +27 -12
  242. package/src/apps/data/routes/transactions.test.ts +72 -19
  243. package/src/apps/data/routes/transactions.ts +162 -92
  244. package/src/apps/data/routes/transfers.test.ts +146 -4
  245. package/src/apps/data/routes/transfers.ts +91 -13
  246. package/src/apps/data/routes/valuation.test.ts +422 -0
  247. package/src/apps/data/routes/valuation.ts +423 -0
  248. package/src/apps/data/routes/verified-tokens.ts +14 -2
  249. package/src/apps/data/routes/webhooks.test.ts +56 -8
  250. package/src/apps/data/routes/webhooks.ts +144 -27
  251. package/src/apps/index.ts +1 -1
  252. package/src/apps/management/App.ts +13 -0
  253. package/src/apps/management/routes/api-keys.test.ts +87 -0
  254. package/src/apps/management/routes/api-keys.ts +4 -3
  255. package/src/apps/management/routes/billing.test.ts +112 -1
  256. package/src/apps/management/routes/billing.ts +7 -2
  257. package/src/apps/management/routes/faucet.test.ts +79 -0
  258. package/src/apps/management/routes/faucet.ts +185 -0
  259. package/src/apps/management/routes/invitations.test.ts +172 -0
  260. package/src/apps/management/routes/invitations.ts +3 -0
  261. package/src/apps/management/routes/invite-links.test.ts +117 -0
  262. package/src/apps/management/routes/invite-links.ts +43 -1
  263. package/src/apps/management/routes/me.test.ts +140 -88
  264. package/src/apps/management/routes/me.ts +6 -0
  265. package/src/apps/management/routes/usage.test.ts +6 -0
  266. package/src/apps/management/routes/usage.ts +7 -0
  267. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  268. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  269. package/src/apps/management/routes/webhooks.test.ts +426 -0
  270. package/src/apps/management/routes/webhooks.ts +672 -0
  271. package/src/apps/mcp.test.ts +13 -0
  272. package/src/apps/mcp.ts +18 -5
  273. package/src/cli/Cli.test.ts +36 -0
  274. package/src/cli/Cli.ts +7 -2
  275. package/src/db/Db.test.ts +19 -0
  276. package/src/db/Db.ts +95 -0
  277. package/src/db/Schema.ts +53 -0
  278. package/src/db/tables/earlyAccess.ts +86 -0
  279. package/src/db/tables/invitations.ts +26 -1
  280. package/src/db/tables/inviteLinks.ts +22 -3
  281. package/src/db/tables/sponsoredTransactions.ts +4 -0
  282. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  283. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  284. package/src/db/tables/webhookSubscriptions.ts +31 -0
  285. package/src/internal/Auth.ts +73 -45
  286. package/src/internal/FundingQuotes.test.ts +143 -1
  287. package/src/internal/FundingQuotes.ts +95 -28
  288. package/src/internal/Log.test-d.ts +21 -0
  289. package/src/internal/Log.test.ts +50 -0
  290. package/src/internal/Log.ts +24 -1
  291. package/src/internal/MetricSink.test.ts +160 -0
  292. package/src/internal/MetricSink.ts +10 -0
  293. package/src/internal/OpenApi.ts +3 -0
  294. package/src/internal/Schema.ts +50 -5
  295. package/src/internal/Value.test.ts +20 -0
  296. package/src/internal/Value.ts +24 -1
  297. package/src/internal/VerifiedTokens.ts +9 -1
  298. package/src/internal/Webhooks.test.ts +140 -0
  299. 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
+ })