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
@@ -1,11 +1,62 @@
1
1
  import type * as z from 'zod/mini'
2
- import { Schema } from 'tapimo'
2
+ import { Value as core_Value } from 'ox'
3
+ import { Db, Schema } from 'tapimo'
4
+ import { FxOracle } from 'tapimo/apps'
3
5
 
4
6
  import * as TestApp from '../../../../test/App.js'
5
7
  import * as Runtime from '../../../../test/runtime.js'
6
8
  import * as Transfers from './transfers.js'
7
9
 
10
+ // Deterministic EUR-based rate set so conversion math is exact in assertions.
11
+ const fixed = FxOracle.from({
12
+ name: 'fixed',
13
+ rates: async () => ({ asOf: '2026-01-01', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' } }),
14
+ })
15
+
8
16
  describe('GET /transfers', () => {
17
+ test('degrades unavailable verification data to null valuations', async () => {
18
+ const db = () =>
19
+ Db.postgres({
20
+ connectionString: 'postgresql://postgres:postgres@127.0.0.1:1/none',
21
+ })
22
+ const client = TestApp.client({ db, fx: { oracle: fixed } })
23
+
24
+ const response = await client.v1.transfers.$get(
25
+ { query: { limit: '5', token: TestApp.tokenWithHolders } },
26
+ TestApp.auth,
27
+ )
28
+ const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
29
+
30
+ expect(response.status).toBe(200)
31
+ expect(body.data.length).toBeGreaterThan(0)
32
+ for (const entry of body.data) expect(entry.sourceToken.valuation).toBeNull()
33
+
34
+ const emptyResponse = await client.v1.transfers.$get(
35
+ { query: { limit: '5', token: `0x${'11'.repeat(20)}` } },
36
+ TestApp.auth,
37
+ )
38
+ const empty = await TestApp.json(emptyResponse, Transfers.schema.getTransfers.Response)
39
+ expect(emptyResponse.status).toBe(200)
40
+ expect(empty.data).toMatchInlineSnapshot(`[]`)
41
+ expect(emptyResponse.headers.get('server-timing')?.includes('verified_tokens;dur=')).not.toBe(
42
+ true,
43
+ )
44
+
45
+ const unsupportedResponse = await client.v1.transfers.$get(
46
+ {
47
+ query: {
48
+ denomination: 'JPY',
49
+ limit: '5',
50
+ token: TestApp.tokenWithHolders,
51
+ },
52
+ },
53
+ TestApp.auth,
54
+ )
55
+ const unsupported = await TestApp.json(unsupportedResponse, Schema.ErrorResponse)
56
+ expect(unsupportedResponse.status).toBe(400)
57
+ expect(unsupported.error.code).toBe('query_invalid')
58
+ })
59
+
9
60
  test('returns an unscoped transfer page, embedding tokens on demand', async () => {
10
61
  const client = TestApp.client()
11
62
  const baseResponse = await client.v1.transfers.$get({ query: { limit: '5' } }, TestApp.auth)
@@ -147,6 +198,9 @@ describe('GET /transfers', () => {
147
198
  const second = await TestApp.json(secondResponse, Transfers.schema.getTransfers.Response)
148
199
 
149
200
  expect(secondResponse.status).toBe(200)
201
+ expect(secondResponse.headers.get('cache-control')).toMatchInlineSnapshot(
202
+ `"private, max-age=60, stale-while-revalidate=300"`,
203
+ )
150
204
  expect(second.data.length).toBeGreaterThan(0)
151
205
  // Each row's identity on the unscoped feed is `(blockNumber, txHash,
152
206
  // sourceToken, sender, recipient, amount)` — a single transaction can
@@ -285,7 +339,7 @@ describe('GET /transfers', () => {
285
339
  expect(Number.isInteger(body.meta?.totalCount)).toBe(true)
286
340
  expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
287
341
  expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
288
- expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount >= Schema.countCap)
342
+ expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount! >= Schema.countCap)
289
343
  })
290
344
 
291
345
  test('embeds a capped token-scoped total count on demand', async () => {
@@ -309,7 +363,7 @@ describe('GET /transfers', () => {
309
363
  expect(typeof body.meta?.totalCountCapped).toBe('boolean')
310
364
  // The count never exceeds the cap; `totalCountCapped` is set iff the cap was hit.
311
365
  expect(body.meta!.totalCount).toBeLessThanOrEqual(Schema.countCap)
312
- expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount >= Schema.countCap)
366
+ expect(body.meta!.totalCountCapped).toBe(body.meta!.totalCount! >= Schema.countCap)
313
367
 
314
368
  // Without `include=totalCount`, the same page omits `meta` entirely and
315
369
  // does not run the count query.
@@ -319,7 +373,10 @@ describe('GET /transfers', () => {
319
373
  )
320
374
  const bare = await TestApp.json(bareResponse, Transfers.schema.getTransfers.Response)
321
375
  expect(bareResponse.status).toBe(200)
322
- expect(bare.meta).toBeUndefined()
376
+ // Without the include, counts are omitted (`meta` carries at most rate
377
+ // provenance).
378
+ expect(bare.meta?.totalCount).toBeUndefined()
379
+ expect(bare.meta?.totalCountCapped).toBeUndefined()
323
380
  expect(bareResponse.headers.get('server-timing')?.includes('transfers_count;dur=')).toBe(false)
324
381
  })
325
382
 
@@ -422,6 +479,91 @@ describe('GET /transfers', () => {
422
479
  },
423
480
  )
424
481
 
482
+ test('values verified transfer legs in the default denomination', async () => {
483
+ const db = TestApp.database()
484
+ await TestApp.verifiedSeed(db, Runtime.get().chainId)
485
+ const client = TestApp.client({ db })
486
+
487
+ // pathUSD is curated USD, so its legs value at identity with no rates.
488
+ const response = await client.v1.transfers.$get(
489
+ { query: { limit: '5', token: TestApp.tokenWithHolders } },
490
+ TestApp.auth,
491
+ )
492
+ const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
493
+
494
+ expect(response.status).toBe(200)
495
+ expect(body.data.length).toBeGreaterThan(0)
496
+ for (const entry of body.data)
497
+ expect(entry.sourceToken.valuation).toEqual({
498
+ amount: core_Value.format(BigInt(entry.sourceToken.amount), 6),
499
+ currency: 'USD',
500
+ })
501
+ expect(body.meta?.pricing).toBeUndefined()
502
+ })
503
+
504
+ test('values transfer legs in a requested denomination', async () => {
505
+ const db = TestApp.database()
506
+ await TestApp.verifiedSeed(db, Runtime.get().chainId)
507
+ const client = TestApp.client({ db, fx: { oracle: fixed } })
508
+
509
+ // Denominations are case-insensitive.
510
+ const response = await client.v1.transfers.$get(
511
+ { query: { denomination: 'aud', limit: '5', token: TestApp.tokenWithHolders } },
512
+ TestApp.auth,
513
+ )
514
+ const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
515
+
516
+ expect(response.status).toBe(200)
517
+ expect(body.data.length).toBeGreaterThan(0)
518
+ // USD -> AUD at the fixed 1.6 cross rate, floored to 6 dp.
519
+ for (const entry of body.data)
520
+ expect(entry.sourceToken.valuation).toEqual({
521
+ amount: core_Value.format((BigInt(entry.sourceToken.amount) * 16n) / 10n, 6),
522
+ currency: 'AUD',
523
+ })
524
+ expect(body.meta?.pricing).toMatchInlineSnapshot(`
525
+ {
526
+ "asOf": "2026-01-01",
527
+ "basis": "nominal",
528
+ "source": "fixed",
529
+ }
530
+ `)
531
+ })
532
+
533
+ test.skipIf(Runtime.get().mode !== 'localnet')(
534
+ 'nulls leg values for unverified tokens',
535
+ async () => {
536
+ // Curate only pathUSD so the faucet token queried below is unverified
537
+ // for this app, and its legs never value.
538
+ const db = TestApp.database()
539
+ await TestApp.verifiedSeed(db, Runtime.get().chainId, {
540
+ tokens: [
541
+ {
542
+ address: TestApp.tokenWithHolders,
543
+ currency: 'USD',
544
+ decimals: 6,
545
+ name: 'PathUSD',
546
+ symbol: 'pathUSD',
547
+ },
548
+ ],
549
+ })
550
+ const client = TestApp.client({ db })
551
+
552
+ const token = Runtime.get().fixtures!.transferTokenAddresses.find(
553
+ (address) => address.toLowerCase() !== TestApp.tokenWithHolders,
554
+ )!
555
+ const response = await client.v1.transfers.$get(
556
+ { query: { limit: '5', token } },
557
+ TestApp.auth,
558
+ )
559
+ const body = await TestApp.json(response, Transfers.schema.getTransfers.Response)
560
+
561
+ expect(response.status).toBe(200)
562
+ expect(body.data.length).toBeGreaterThan(0)
563
+ for (const entry of body.data) expect(entry.sourceToken.valuation).toBeNull()
564
+ },
565
+ )
566
+
425
567
  test('surfaces a 502 when transfer data is unavailable', async () => {
426
568
  // Point at an unreachable indexer so the upstream query fails deterministically.
427
569
  const client = TestApp.client({ tidx: { baseUrl: 'http://127.0.0.1:1' } })
@@ -17,7 +17,9 @@ import * as Timing from '../../../internal/Timing.js'
17
17
  import * as Ttl from '../../../internal/Ttl.js'
18
18
  import * as Value from '../../../internal/Value.js'
19
19
  import * as VerifiedTokens from '../../../internal/VerifiedTokens.js'
20
+ import * as FxOracle from '../FxOracle.js'
20
21
  import * as Tokens from './tokens.js'
22
+ import * as Valuation from './valuation.js'
21
23
 
22
24
  // TIP-20 tokens emit the standard ERC-20 Transfer event. Supplying the
23
25
  // signature exposes a decoded `Transfer` CTE (with `from`/`to`/`value`) to
@@ -93,6 +95,14 @@ export namespace schema {
93
95
  z.describe('The short ticker symbol wallets and apps show for this token.'),
94
96
  z.meta({ examples: ['USDC'] }),
95
97
  ),
98
+ valuation: z
99
+ .optional(z.nullable(Valuation.schema.Value))
100
+ .check(
101
+ z.describe(
102
+ 'The leg’s nominal value in the requested `denomination`. `null` when the token is ' +
103
+ 'unverified, its display currency has no rate, or rates were unavailable.',
104
+ ),
105
+ ),
96
106
  verified: z
97
107
  .optional(z.boolean())
98
108
  .check(
@@ -174,6 +184,7 @@ export namespace schema {
174
184
  'blockNumber.to': Schema.blockNumberBound('transfers', 'to'),
175
185
  chainId: Schema.ChainIdQuery,
176
186
  cursor: Schema.Cursor,
187
+ denomination: Schema.Denomination,
177
188
  include: includeQuery,
178
189
  limit: Schema.Limit,
179
190
  order: Schema.Order,
@@ -266,16 +277,32 @@ export namespace schema {
266
277
  ),
267
278
  )
268
279
 
280
+ /** Page-level resources: opt-in counts plus valuation rate provenance. */
281
+ export const Meta = z
282
+ .object({
283
+ pricing: z
284
+ .optional(Valuation.schema.Pricing)
285
+ .check(
286
+ z.describe(
287
+ 'Rate provenance for leg valuations. Present when conversion rates were ' +
288
+ 'consulted; absent when every value was identity-valued or rates were unavailable.',
289
+ ),
290
+ ),
291
+ totalCount: z.optional(Schema.TotalCount),
292
+ totalCountCapped: z.optional(Schema.TotalCountCapped),
293
+ })
294
+ .check(z.describe('Page-level resources attached to this response.'))
295
+
269
296
  /** Page of TIP-20 token transfers across the chain, ordered by block then log index. */
270
297
  export const Response = z
271
298
  .object({
272
299
  data: z.array(Transfer).check(z.describe('The transfers in this page.')),
273
300
  // Response-wide `meta` carries the opt-in capped total count
274
- // (`include=totalCount`); per-row embeds (`memo`, `token`) attach to
275
- // each `data` item, not here.
301
+ // (`include=totalCount`) and valuation rate provenance; per-row embeds
302
+ // (`memo`, `token`) attach to each `data` item, not here.
276
303
  meta: z
277
- .optional(Schema.CountMeta)
278
- .check(z.describe('Page-level resources you requested with `include`.')),
304
+ .optional(Meta)
305
+ .check(z.describe('Page-level resources: opt-in counts and valuation rate provenance.')),
279
306
  nextCursor: Schema.NextCursor,
280
307
  })
281
308
  .check(z.describe('A page of TIP-20 token transfer events across the chain.'))
@@ -283,7 +310,8 @@ export namespace schema {
283
310
  }
284
311
 
285
312
  /** Creates transfer handlers. */
286
- export function transfers() {
313
+ export function transfers(options: transfers.Options = {}) {
314
+ const oracle = options.fx?.oracle ?? FxOracle.ecb()
287
315
  return new Hono<App.Environment>().get(
288
316
  '/v1/transfers',
289
317
  Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
@@ -319,11 +347,10 @@ export function transfers() {
319
347
 
320
348
  const options = c.req.valid('query')
321
349
  const chainId = options.chainId ?? c.get('chainId')
350
+ const denomination = options.denomination ?? 'USD'
322
351
 
323
- // Cursor pages are anchored below the head and effectively immutable,
324
- // so cache them aggressively; the head page keeps the route's short
325
- // `feed` default.
326
- if (options.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
352
+ // Cursor pages carry current valuations and curated verification data.
353
+ if (options.cursor !== undefined) Cache.setPolicy(c, Cache.policies.metadata)
327
354
 
328
355
  // Filters shared by the page query and the opt-in total count, so the
329
356
  // count always matches the page it annotates.
@@ -427,7 +454,13 @@ export function transfers() {
427
454
  return suppress.has(key) ? [] : [transfer]
428
455
  })
429
456
  }
430
- if (options.include.includes('token') && data.length > 0) {
457
+ // Verification is ancillary to the feed. Skip empty pages and degrade
458
+ // cold snapshot failures to null valuations.
459
+ const snapshot =
460
+ data.length > 0
461
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
462
+ : undefined
463
+ if (options.include.includes('token') && data.length > 0 && snapshot) {
431
464
  const uniqueTokens = Array.from(
432
465
  new Set(
433
466
  data.flatMap((transfer) => [
@@ -436,7 +469,6 @@ export function transfers() {
436
469
  ]),
437
470
  ),
438
471
  )
439
- const snapshot = await VerifiedTokens.snapshot(c, chainId)
440
472
  const resolved = await Timing.time(c, 'tokens', () =>
441
473
  Promise.all(
442
474
  uniqueTokens.map((token) =>
@@ -466,23 +498,69 @@ export function transfers() {
466
498
  }))
467
499
  }
468
500
 
469
- const meta = countPromise ? await countPromise : undefined
501
+ // Value each leg via its curated display currency, in the requested
502
+ // denomination.
503
+ const rates = await Valuation.ratesFor(c, {
504
+ currencies: data.flatMap((transfer) =>
505
+ [
506
+ transfer.sourceToken.address,
507
+ ...(transfer.destinationToken ? [transfer.destinationToken.address] : []),
508
+ ].flatMap((address) => {
509
+ const held = snapshot?.byAddress.get(address.toLowerCase())?.currency
510
+ return held === undefined ? [] : [held]
511
+ }),
512
+ ),
513
+ denomination,
514
+ oracle,
515
+ })
516
+ const valued = <leg extends { address: string; amount: string }>(leg: leg) => ({
517
+ ...leg,
518
+ valuation: Valuation.valuationFor({
519
+ amount: BigInt(leg.amount),
520
+ denomination,
521
+ rates,
522
+ token: snapshot?.byAddress.get(leg.address.toLowerCase()),
523
+ }),
524
+ })
525
+ data = data.map((transfer) => ({
526
+ ...transfer,
527
+ sourceToken: valued(transfer.sourceToken),
528
+ ...(transfer.destinationToken
529
+ ? { destinationToken: valued(transfer.destinationToken) }
530
+ : {}),
531
+ }))
532
+
533
+ const counts = countPromise ? await countPromise : undefined
534
+ const meta = {
535
+ ...(rates ? { pricing: Valuation.pricing(rates, oracle) } : {}),
536
+ ...counts,
537
+ }
470
538
 
471
539
  return c.json(
472
540
  Response.validated(schema.getTransfers.Response, {
473
541
  data,
474
- ...(meta ? { meta } : {}),
542
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
475
543
  nextCursor: page.nextCursor,
476
544
  }),
477
545
  200,
478
546
  )
479
547
  } catch (cause) {
548
+ if (cause instanceof Valuation.UnsupportedDenominationError)
549
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
480
550
  return Response.upstream(c, cause)
481
551
  }
482
552
  },
483
553
  )
484
554
  }
485
555
 
556
+ export declare namespace transfers {
557
+ /** Options for the transfer handlers. */
558
+ type Options = {
559
+ /** FX configuration backing leg valuation. */
560
+ fx?: Valuation.addresses.Fx | undefined
561
+ }
562
+ }
563
+
486
564
  /**
487
565
  * Shared TIP-20 `Transfer` page query. Used by `GET /transfers` and the
488
566
  * webhook poller. Builds the signature-decoded `Transfer` CTE query, applies