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
@@ -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 stays by tag
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 reached via search params
17
- * instead (`GET /transactions?address=…`, `GET /transfers?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.token.decimals) === BigInt(entry.amount) &&
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.token.currency === 'string' &&
70
- Number.isInteger(entry.token.decimals) &&
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` is omitted. The corrected candidate set
137
- // already supplies the count, so no second indexer query is needed.
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('restricts balances to verified tokens when verified=true', async () => {
239
- const client = app()
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
- { param: { address: holder }, query: { limit: '20', verified: 'true' } },
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 (implies verified)', async () => {
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.token.verified).toBe(true)
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
- formatted: z
96
+ currency: z
99
97
  .string()
100
98
  .check(
101
99
  z.describe(
102
- 'The same balance in human-readable decimal form, using the token’s `decimals`.',
100
+ 'The currency label for this balance, such as `USD` for a USD-denominated token.',
103
101
  ),
104
- z.meta({ examples: ['1.5'] }),
102
+ z.meta({ examples: ['USD'] }),
105
103
  ),
106
- id: z
107
- .string()
104
+ decimals: z
105
+ .number()
108
106
  .check(
107
+ z.int(),
108
+ z.nonnegative(),
109
109
  z.describe(
110
- 'A stable resource ID for this balance, equal to the token contract address.',
110
+ 'The number of decimal places used to convert this balance between base units and human-readable form.',
111
111
  ),
112
- z.meta({ examples: ['0x20c0000000000000000000008f5425160ebe5525'] }),
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(Schema.CountMeta)
139
- .check(z.describe('Page-level resources you requested with `include`.')),
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: 'The upstream indexer could not serve this request.' },
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 { address, chainId, currency, feeEligible, feeTokens, includeTotalCount, limit, verified } =
242
- options
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
- // Resolve token metadata for each balance. The endpoint's contract is a
342
- // balance with its `token` reference inlined per row (and `formatted` needs
343
- // the token `decimals`), so a row whose metadata cannot be resolved is
344
- // dropped rather than returned with placeholders. Because the TIDX filter
345
- // already restricts to TIP-20 (`0x20c0…`) tokens, resolution should succeed
346
- // for nearly every row in practice.
347
- //
348
- // The verified `snapshot` fetched above is reused across the page instead of
349
- // re-resolved per row. `createdAt` is not part of this endpoint's response,
350
- // so it is never fetched here.
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
- Tokens.resolveToken(c, {
355
- address: holding.address,
356
- chainId,
357
- snapshot,
358
- })
359
- .then((metadata) => ({ amount: holding.amount, metadata }) as const)
360
- .catch(() => ({ amount: holding.amount, metadata: undefined }) as const),
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.metadata) continue
440
+ if (!entry) continue
368
441
  data.push({
369
442
  amount: entry.amount,
370
- formatted: core_Value.format(BigInt(entry.amount), entry.metadata.decimals),
371
- id: entry.metadata.address,
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.metadata.address.toLowerCase()) } : {}),
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.metadata.address,
451
+ address: entry.token,
377
452
  currency: entry.metadata.currency,
378
453
  decimals: entry.metadata.decimals,
379
- id: entry.metadata.id,
380
- logoUri: entry.metadata.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.metadata.verified,
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
- ...(includeTotalCount
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: VerifiedTokens.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
  }