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,423 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import { Value as core_Value } from 'ox'
3
+ import { BaseError, ContractFunctionRevertedError, ContractFunctionZeroDataError } from 'viem'
4
+ import { Actions } from 'viem/tempo'
5
+ import * as z from 'zod/mini'
6
+
7
+ import type * as App from '../../../App.js'
8
+ import * as Auth from '../../../internal/Auth.js'
9
+ import * as Cache from '../../../internal/Cache.js'
10
+ import * as OpenApi from '../../../internal/OpenApi.js'
11
+ import * as Response from '../../../internal/Response.js'
12
+ import * as Schema from '../../../internal/Schema.js'
13
+ import * as Store from '../../../internal/Store.js'
14
+ import * as Timing from '../../../internal/Timing.js'
15
+ import * as VerifiedTokens from '../../../internal/VerifiedTokens.js'
16
+ import * as FxOracle from '../FxOracle.js'
17
+
18
+ // How many balance reads to fan out per batch. Concurrent reads in one
19
+ // macrotask collapse into a single deployless multicall; small chunks keep
20
+ // each call under the Tempo RPC's per-call size limit.
21
+ const tokenReadChunkSize = 20
22
+
23
+ /** Zod schemas owned by the valuation handlers. */
24
+ export namespace schema {
25
+ /** A holding's nominal value in the requested denomination. */
26
+ export const Value = z
27
+ .object({
28
+ amount: z
29
+ .string()
30
+ .check(
31
+ z.regex(/^\d+(\.\d+)?$/),
32
+ z.describe('The holding’s nominal value, as a decimal string in `currency`.'),
33
+ z.meta({ examples: ['1.50'] }),
34
+ ),
35
+ currency: z
36
+ .string()
37
+ .check(
38
+ z.describe('The denomination currency of this value.'),
39
+ z.meta({ examples: ['USD'] }),
40
+ ),
41
+ })
42
+ .check(z.describe('The holding’s nominal value in the requested denomination.'))
43
+
44
+ /** Rate provenance for a converted valuation. */
45
+ export const Pricing = z
46
+ .object({
47
+ asOf: z
48
+ .string()
49
+ .check(
50
+ z.describe('Publication date of the rate set used for conversion.'),
51
+ z.meta({ examples: ['2026-07-20'] }),
52
+ ),
53
+ basis: z
54
+ .literal('nominal')
55
+ .check(
56
+ z.describe('Valuation basis: one token unit counts as one unit of its display currency.'),
57
+ z.meta({ examples: ['nominal'] }),
58
+ ),
59
+ source: z
60
+ .string()
61
+ .check(
62
+ z.describe('FX rate oracle that supplied the conversion rates.'),
63
+ z.meta({ examples: ['ecb'] }),
64
+ ),
65
+ })
66
+ .check(z.describe('Rate provenance for a converted valuation.'))
67
+
68
+ /** Schemas for the getAddressValuation operation. */
69
+ export namespace getAddressValuation {
70
+ /** Path parameters for address valuation requests. */
71
+ export const Params = z
72
+ .object({
73
+ address: Schema.Address.check(
74
+ z.describe('The account address whose holdings you want to value.'),
75
+ ),
76
+ })
77
+ .check(z.describe('Path parameters for valuing an account’s verified holdings.'))
78
+
79
+ /** Query parameters for address valuation requests. */
80
+ export const Query = z
81
+ .strictObject({
82
+ chainId: Schema.ChainIdQuery,
83
+ denomination: Schema.Denomination,
84
+ })
85
+ .check(z.describe('Query parameters for valuing an account’s verified holdings.'))
86
+
87
+ /** The nominal valuation of an account's verified holdings. */
88
+ export const Response = z
89
+ .object({
90
+ address: Schema.Address.check(z.describe('The account address this valuation covers.')),
91
+ amount: z
92
+ .string()
93
+ .check(
94
+ z.regex(/^\d+(\.\d+)?$/),
95
+ z.describe(
96
+ 'The total value of the account’s verified holdings, as a decimal string in `currency`.',
97
+ ),
98
+ z.meta({ examples: ['160.50'] }),
99
+ ),
100
+ currency: z
101
+ .string()
102
+ .check(
103
+ z.describe('The denomination currency of `amount`.'),
104
+ z.meta({ examples: ['USD'] }),
105
+ ),
106
+ id: z
107
+ .string()
108
+ .check(
109
+ z.describe('A stable resource ID for this valuation, equal to the account address.'),
110
+ z.meta({ examples: ['0xbe058e1c4df8a4366a387bf595b284246a93039e'] }),
111
+ ),
112
+ pricing: z
113
+ .nullable(Pricing)
114
+ .check(
115
+ z.describe(
116
+ 'Rate provenance, or `null` when every valued holding was already denominated in `currency`.',
117
+ ),
118
+ ),
119
+ unpriced: z
120
+ .array(z.string())
121
+ .check(
122
+ z.describe(
123
+ 'Display currencies excluded from `amount` because the FX oracle has no rate for them.',
124
+ ),
125
+ z.meta({ examples: [['BTC']] }),
126
+ ),
127
+ })
128
+ .check(z.describe('The nominal valuation of an account’s verified token holdings.'))
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Creates address-scoped valuation handlers, mounted under the `/addresses`
134
+ * composer. Exposes `GET /:address/valuation`, the holdings valuation.
135
+ */
136
+ export function addresses(options: addresses.Options = {}) {
137
+ const oracle = options.fx?.oracle ?? FxOracle.ecb()
138
+ return new Hono<App.Environment>().get(
139
+ '/v1/addresses/:address/valuation',
140
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
141
+ OpenApi.validate('param', schema.getAddressValuation.Params, {
142
+ code: 'address_invalid',
143
+ message: 'Invalid account address',
144
+ }),
145
+ OpenApi.validate('query', schema.getAddressValuation.Query, {
146
+ code: 'query_invalid',
147
+ message: 'Invalid query parameters',
148
+ }),
149
+ OpenApi.describeRoute({
150
+ description:
151
+ 'Values every verified TIP-20 token an account holds as one unit of its display currency, converts each currency into the requested denomination using the configured FX rates, and returns the total.',
152
+ operationId: 'getAddressValuation',
153
+ responses: OpenApi.responses({
154
+ errors: { 502: 'Could not read balances or exchange rates from an upstream service.' },
155
+ success: {
156
+ description: 'The valuation of this account’s verified holdings.',
157
+ schema: schema.getAddressValuation.Response,
158
+ },
159
+ }),
160
+ summary: 'Get address valuation',
161
+ tags: ['Balances'],
162
+ }),
163
+ Cache.response<App.Environment>({
164
+ cacheControl: Cache.policies.noStore,
165
+ name: 'tempo-api:addresses:v1',
166
+ key: (c) => Cache.urlKey(c, schema.getAddressValuation.Query),
167
+ }),
168
+ async (c) => {
169
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
170
+ if (OpenApi.narrowValidation)
171
+ return OpenApi.validationError(c, {
172
+ code: 'address_invalid',
173
+ message: 'Invalid request parameters',
174
+ })
175
+ if (OpenApi.narrowValidation)
176
+ return OpenApi.validationError(c, {
177
+ code: 'query_invalid',
178
+ message: 'Invalid request parameters',
179
+ })
180
+
181
+ const { address } = c.req.valid('param')
182
+ const query = c.req.valid('query')
183
+
184
+ try {
185
+ const valuation = await getAddressValuation(c, {
186
+ address,
187
+ chainId: query.chainId ?? c.get('chainId'),
188
+ denomination: query.denomination ?? 'USD',
189
+ oracle,
190
+ })
191
+ return c.json(Response.validated(schema.getAddressValuation.Response, valuation), 200)
192
+ } catch (cause) {
193
+ if (cause instanceof UnsupportedDenominationError)
194
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
195
+ return Response.upstream(c, cause)
196
+ }
197
+ },
198
+ )
199
+ }
200
+
201
+ export declare namespace addresses {
202
+ /** Options for the address-scoped valuation handlers. */
203
+ type Options = {
204
+ /** FX configuration backing currency conversion. */
205
+ fx?: Fx | undefined
206
+ }
207
+
208
+ /** FX configuration for valuation handlers. */
209
+ type Fx = {
210
+ /** FX rate oracle. Defaults to `FxOracle.ecb()`. */
211
+ oracle?: FxOracle.Oracle | undefined
212
+ }
213
+ }
214
+
215
+ async function getAddressValuation(
216
+ c: Context<App.Environment>,
217
+ options: getAddressValuation.Options,
218
+ ) {
219
+ const { address, chainId, denomination, oracle } = options
220
+ const [set, ratesCause] = await rateSet(c, oracle).then(
221
+ (set) => [set, undefined] as const,
222
+ (cause: unknown) => [undefined, cause] as const,
223
+ )
224
+ if (set && denomination !== set.base && set.rates[denomination] === undefined)
225
+ throw new UnsupportedDenominationError(denomination, oracle.name)
226
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
227
+
228
+ type Holding = { amount: bigint; token: VerifiedTokens.Snapshot['list'][number] }
229
+ const holdings = await Timing.time(c, 'valuation_balances', async () => {
230
+ const client = c.get('getClient')(chainId)
231
+ if (snapshot.list.length === 0) return []
232
+ // Pin every chunk to one chain-state snapshot.
233
+ const blockNumber = await client.getBlockNumber({ cacheTime: 0 })
234
+ const rows: Holding[] = []
235
+ for (let index = 0; index < snapshot.list.length; index += tokenReadChunkSize) {
236
+ const group = snapshot.list.slice(index, index + tokenReadChunkSize)
237
+ // Concurrent reads enter viem's shared batch window and become one
238
+ // deployless multicall. A failed read fails the request rather than
239
+ // serving a partial total.
240
+ const results = await Promise.all(
241
+ group.map(async (token) => {
242
+ try {
243
+ const { amount } = await Actions.token.getBalance(client, {
244
+ account: address,
245
+ blockNumber,
246
+ decimals: token.decimals,
247
+ token: token.address,
248
+ })
249
+ return amount === 0n ? undefined : { amount, token }
250
+ } catch (error) {
251
+ // A verified token that is undeployed (zero data) or uninitialized
252
+ // on this chain cannot be held; count it as an empty holding
253
+ // rather than failing the valuation.
254
+ const empty =
255
+ error instanceof BaseError &&
256
+ error.walk(
257
+ (cause) =>
258
+ cause instanceof ContractFunctionZeroDataError ||
259
+ (cause instanceof ContractFunctionRevertedError &&
260
+ cause.data?.errorName === 'Uninitialized'),
261
+ )
262
+ if (empty) return undefined
263
+ throw error
264
+ }
265
+ }),
266
+ )
267
+ rows.push(...results.filter((row) => row !== undefined))
268
+ }
269
+ return rows
270
+ })
271
+
272
+ // Aggregate holdings by display currency at a shared 18-dp scale so tokens
273
+ // with different decimals sum exactly.
274
+ type Group = { currency: string; sum: bigint }
275
+ const groups = new Map<string, Group>()
276
+ for (const { amount, token } of holdings) {
277
+ const group = groups.get(token.currency.toLowerCase()) ?? { currency: token.currency, sum: 0n }
278
+ group.sum +=
279
+ token.decimals <= FxOracle.precision
280
+ ? amount * 10n ** BigInt(FxOracle.precision - token.decimals)
281
+ : amount / 10n ** BigInt(token.decimals - FxOracle.precision)
282
+ groups.set(token.currency.toLowerCase(), group)
283
+ }
284
+
285
+ let total = 0n
286
+ const unpriced: string[] = []
287
+ let pricing: z.output<typeof schema.Pricing> | null = null
288
+
289
+ const pending = [...groups.values()].filter(
290
+ (group) => group.currency.toUpperCase() !== denomination,
291
+ )
292
+ for (const group of groups.values())
293
+ if (group.currency.toUpperCase() === denomination) total += group.sum
294
+
295
+ if (pending.length > 0) {
296
+ if (!set) throw ratesCause
297
+ for (const group of pending) {
298
+ const rate = FxOracle.rate(set, { from: group.currency.toUpperCase(), to: denomination })
299
+ if (rate === undefined) unpriced.push(group.currency)
300
+ else total += (group.sum * rate) / 10n ** BigInt(FxOracle.precision)
301
+ }
302
+ pricing = { asOf: set.asOf, basis: 'nominal', source: oracle.name }
303
+ }
304
+ unpriced.sort()
305
+
306
+ return {
307
+ address,
308
+ // Floor to 6 dp, the TIP-20 native precision; same-currency sums stay exact.
309
+ amount: core_Value.format(total / 10n ** BigInt(FxOracle.precision - 6), 6),
310
+ currency: denomination,
311
+ id: address,
312
+ pricing,
313
+ unpriced,
314
+ }
315
+ }
316
+
317
+ declare namespace getAddressValuation {
318
+ type Options = {
319
+ address: z.output<typeof Schema.Address>
320
+ chainId: z.output<typeof Schema.ChainId>
321
+ /** Currency the valuation is denominated in (uppercase). */
322
+ denomination: string
323
+ /** FX rate oracle backing currency conversion. */
324
+ oracle: FxOracle.Oracle
325
+ }
326
+ }
327
+
328
+ /** Loads the oracle's rate set through the request's store cache. */
329
+ export function rateSet(c: Context<App.Environment>, oracle: FxOracle.Oracle) {
330
+ return Timing.time(c, 'valuation_rates', () =>
331
+ Store.memoize(() => oracle.rates(), {
332
+ key: `fx:v1:${oracle.name}`,
333
+ store: c.get('store'),
334
+ ttl: oracle.ttl,
335
+ }),
336
+ )
337
+ }
338
+
339
+ /**
340
+ * Loads rates best-effort to validate the denomination and value a page.
341
+ * Returns `undefined` when unavailable or unnecessary; a known unsupported denomination still throws.
342
+ */
343
+ export async function ratesFor(
344
+ c: Context<App.Environment>,
345
+ options: ratesFor.Options,
346
+ ): Promise<FxOracle.RateSet | undefined> {
347
+ const { denomination, oracle } = options
348
+ const rates = await rateSet(c, oracle).catch(() => undefined)
349
+ if (!rates) return undefined
350
+ if (denomination !== rates.base && rates.rates[denomination] === undefined)
351
+ throw new UnsupportedDenominationError(denomination, oracle.name)
352
+ const conversion = [...options.currencies].some(
353
+ (currency) => currency.toUpperCase() !== denomination,
354
+ )
355
+ if (!conversion) return undefined
356
+ return rates
357
+ }
358
+
359
+ export declare namespace ratesFor {
360
+ /** Options for loading a page's valuation rates. */
361
+ type Options = {
362
+ /** Display currencies of the curated holdings appearing on the page. */
363
+ currencies: Iterable<string>
364
+ /** Currency values are denominated in (uppercase). */
365
+ denomination: string
366
+ /** FX rate oracle backing currency conversion. */
367
+ oracle: FxOracle.Oracle
368
+ }
369
+ }
370
+
371
+ /** Builds the `pricing` provenance block for a consulted rate set. */
372
+ export function pricing(rates: FxOracle.RateSet, oracle: FxOracle.Oracle) {
373
+ return { asOf: rates.asOf, basis: 'nominal' as const, source: oracle.name }
374
+ }
375
+
376
+ /**
377
+ * Values one holding in the requested denomination via its curated display
378
+ * currency. Returns `null` when the token is unverified, its currency has no
379
+ * rate, or the rate set is unavailable.
380
+ */
381
+ export function valuationFor(options: valuationFor.Options) {
382
+ const { amount, denomination, rates, token } = options
383
+ if (!token) return null
384
+ const rate = (() => {
385
+ if (token.currency.toUpperCase() === denomination) return 10n ** BigInt(FxOracle.precision)
386
+ if (!rates) return undefined
387
+ return FxOracle.rate(rates, { from: token.currency.toUpperCase(), to: denomination })
388
+ })()
389
+ if (rate === undefined) return null
390
+
391
+ // Normalize to the shared 18-dp scale, convert, then floor to 6 dp.
392
+ const scaled =
393
+ token.decimals <= FxOracle.precision
394
+ ? amount * 10n ** BigInt(FxOracle.precision - token.decimals)
395
+ : amount / 10n ** BigInt(token.decimals - FxOracle.precision)
396
+ const value = (scaled * rate) / 10n ** BigInt(FxOracle.precision)
397
+ return {
398
+ amount: core_Value.format(value / 10n ** BigInt(FxOracle.precision - 6), 6),
399
+ currency: denomination,
400
+ }
401
+ }
402
+
403
+ export declare namespace valuationFor {
404
+ /** Options for valuing one holding. */
405
+ type Options = {
406
+ /** Holding amount in the token's base units. */
407
+ amount: bigint
408
+ /** Currency the value is denominated in (uppercase). */
409
+ denomination: string
410
+ /** Oracle rate set, when loaded. */
411
+ rates: FxOracle.RateSet | undefined
412
+ /** Display currency and decimal scale backing the holding, when known. */
413
+ token: Pick<VerifiedTokens.Snapshot['list'][number], 'currency' | 'decimals'> | undefined
414
+ }
415
+ }
416
+
417
+ /** Thrown when the requested denomination is not priced by the FX oracle. */
418
+ export class UnsupportedDenominationError extends Error {
419
+ override name = 'Valuation.UnsupportedDenominationError'
420
+ constructor(denomination: string, oracle: string) {
421
+ super(`Denomination "${denomination}" is not priced by the "${oracle}" FX oracle`)
422
+ }
423
+ }
@@ -21,6 +21,7 @@ export namespace schema {
21
21
  z.describe(
22
22
  'The currency label for this token, such as `USD` for USD-denominated stablecoins.',
23
23
  ),
24
+ z.meta({ examples: ['USD'] }),
24
25
  ),
25
26
  decimals: z
26
27
  .number()
@@ -31,16 +32,27 @@ export namespace schema {
31
32
  z.describe(
32
33
  'The number of decimal places the token uses; Tempo stablecoins typically use 6.',
33
34
  ),
35
+ z.meta({ examples: [6] }),
34
36
  ),
35
37
  logoUri: z
36
38
  .optional(z.url({ protocol: /^https$/ }))
37
- .check(z.describe('The curated HTTPS URL for this token’s logo image, when one is set.')),
38
- name: z.string().check(z.minLength(1), z.describe('The token’s human-readable name.')),
39
+ .check(
40
+ z.describe('The curated HTTPS URL for this token’s logo image, when one is set.'),
41
+ z.meta({ examples: ['https://assets.tempo.xyz/icons/usdc.svg'] }),
42
+ ),
43
+ name: z
44
+ .string()
45
+ .check(
46
+ z.minLength(1),
47
+ z.describe('The token’s human-readable name.'),
48
+ z.meta({ examples: ['USD Coin'] }),
49
+ ),
39
50
  symbol: z
40
51
  .string()
41
52
  .check(
42
53
  z.minLength(1),
43
54
  z.describe('The short ticker symbol wallets and apps show for this token.'),
55
+ z.meta({ examples: ['USDC'] }),
44
56
  ),
45
57
  }
46
58
 
@@ -1,4 +1,5 @@
1
1
  import { type Db, Webhooks as Core } from 'tapimo'
2
+ import * as z from 'zod/mini'
2
3
 
3
4
  import * as Tidx from '../../../internal/Tidx.js'
4
5
  import * as Viem from '../../../internal/Viem.js'
@@ -8,6 +9,9 @@ import * as Runtime from '../../../../test/runtime.js'
8
9
  import * as Tempo from '../../../../test/Tempo.js'
9
10
  import * as Webhooks from './webhooks.js'
10
11
 
12
+ /** Humanized transaction payload delivered by transaction webhooks. */
13
+ type TransactionEvent = z.output<typeof Webhooks.schema.TransactionEvent>
14
+
11
15
  /** API key with both webhook scopes, owned by `org_test`. */
12
16
  const writer = {
13
17
  id: 'key_writer',
@@ -336,23 +340,24 @@ describe('subscription limits', () => {
336
340
  test('rejects creation past the configured per-owner cap with 403 limit_exceeded', async () => {
337
341
  const app = TestApp.client({
338
342
  auth: { keys: [writer, other] },
339
- webhook: { maxPerOwner: 2 },
343
+ webhook: { maxPerOwner: 2, supportedChainIds: [4217, 42431] },
340
344
  })
345
+ const request = { chainId: 4217, ...validBody }
341
346
 
342
347
  // Fill the writer's quota.
343
348
  for (let i = 0; i < 2; i++) {
344
- const ok = await app.v1.webhooks.$post({ json: validBody }, as(writer))
349
+ const ok = await app.v1.webhooks.$post({ json: request }, as(writer))
345
350
  expect(ok.status).toMatchInlineSnapshot(`200`)
346
351
  }
347
352
 
348
353
  // The third create is over the cap.
349
- const over = await app.v1.webhooks.$post({ json: validBody }, as(writer))
354
+ const over = await app.v1.webhooks.$post({ json: request }, as(writer))
350
355
  expect(over.status).toMatchInlineSnapshot(`403`)
351
356
  const body = (await over.json()) as { error?: { code?: string } }
352
357
  expect(body.error?.code).toMatchInlineSnapshot(`"limit_exceeded"`)
353
358
 
354
359
  // The cap is per-owner: a different org is unaffected.
355
- const otherOrg = await app.v1.webhooks.$post({ json: validBody }, as(other))
360
+ const otherOrg = await app.v1.webhooks.$post({ json: request }, as(other))
356
361
  expect(otherOrg.status).toMatchInlineSnapshot(`200`)
357
362
  })
358
363
  })
@@ -671,6 +676,36 @@ describe('slack destination', () => {
671
676
  expect(response.status).toMatchInlineSnapshot(`400`)
672
677
  expect(body.error?.code).toMatchInlineSnapshot(`"url_invalid"`)
673
678
  })
679
+
680
+ test('requires recreation before changing a provider destination to a URL', async () => {
681
+ const app = client([writer])
682
+ const created = await app.v1.webhooks.$post(
683
+ {
684
+ json: {
685
+ destination: {
686
+ type: 'slack',
687
+ url: 'https://hooks.slack.com/services/T000/B000/xxxx',
688
+ },
689
+ eventType: 'token:transfer',
690
+ },
691
+ },
692
+ as(writer),
693
+ )
694
+ const { id } = await TestApp.json(created, Webhooks.schema.createWebhook.Response)
695
+
696
+ const response = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
697
+ {
698
+ json: { destination: { type: 'url', url: 'https://example.com/webhooks' } },
699
+ param: { id },
700
+ },
701
+ as(writer),
702
+ )
703
+
704
+ expect(response.status).toBe(400)
705
+ expect(await response.json()).toMatchObject({
706
+ error: { code: 'destination_transition_invalid' },
707
+ })
708
+ })
674
709
  })
675
710
 
676
711
  describe('betterstack destination', () => {
@@ -1729,11 +1764,18 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1729
1764
  })
1730
1765
 
1731
1766
  expect(result.scanned).toMatchInlineSnapshot(`1`)
1732
- const tx = dispatched[0]?.envelope.data as { hash: string; sender: string } | undefined
1767
+ const tx = dispatched[0]?.envelope.data as TransactionEvent | undefined
1733
1768
  expect(tx?.hash).toMatchInlineSnapshot(
1734
1769
  `"0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3"`,
1735
1770
  )
1736
1771
  expect(tx?.sender).toBe(windowSender)
1772
+ if (tx?.meta.rpc.feeToken) {
1773
+ expect(tx.feeToken?.address).toBe(tx.meta.rpc.feeToken)
1774
+ expect(typeof tx.feeToken?.currency).toBe('string')
1775
+ expect(typeof tx.feeToken?.decimals).toBe('number')
1776
+ expect(typeof tx.feeToken?.name).toBe('string')
1777
+ expect(typeof tx.feeToken?.symbol).toBe('string')
1778
+ } else expect(tx?.feeToken).toBeUndefined()
1737
1779
  })
1738
1780
 
1739
1781
  test('transaction:included matches exact empty calldata through the poller', async () => {
@@ -1821,13 +1863,19 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1821
1863
  })
1822
1864
 
1823
1865
  expect(result.scanned).toMatchInlineSnapshot(`1`)
1824
- const data = dispatched[0]?.envelope.data as
1825
- | { hash: string; meta?: { receipt?: { status: string; transactionHash: string } } }
1826
- | undefined
1866
+ const data = dispatched[0]?.envelope.data as TransactionEvent | undefined
1827
1867
  // The receipt is embedded under `meta.receipt`, matching the read endpoint's
1828
1868
  // `include=receipt` shape, and refers to the same transaction.
1829
1869
  expect(data?.meta?.receipt?.status).toMatchInlineSnapshot(`"success"`)
1830
1870
  expect(data?.meta?.receipt?.transactionHash).toBe(data?.hash)
1871
+ const receipt = data?.meta.receipt
1872
+ if (receipt?.meta.rpc.feeToken) {
1873
+ expect(receipt.feeToken?.address).toBe(receipt.meta.rpc.feeToken)
1874
+ expect(typeof receipt.feeToken?.currency).toBe('string')
1875
+ expect(typeof receipt.feeToken?.decimals).toBe('number')
1876
+ expect(typeof receipt.feeToken?.name).toBe('string')
1877
+ expect(typeof receipt.feeToken?.symbol).toBe('string')
1878
+ } else expect(receipt?.feeToken).toBeUndefined()
1831
1879
  })
1832
1880
 
1833
1881
  // End-to-end through the *default* transport: `poll` with no injected