tapimo 0.7.2 → 0.9.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 (300) hide show
  1. package/README.md +10 -7
  2. package/dist/ApiKey.d.ts +3 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -0
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +7 -3
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +9 -2
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +20117 -19183
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +61 -4
  13. package/dist/App.js.map +1 -1
  14. package/dist/admin/App.d.ts +84 -74
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +14 -1
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -2
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +2 -0
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/apps/verified-tokens.d.ts +11 -11
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +67 -67
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts +2 -0
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +3 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/data/App.d.ts +11696 -13937
  31. package/dist/apps/data/App.d.ts.map +1 -1
  32. package/dist/apps/data/App.js +4 -8
  33. package/dist/apps/data/App.js.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +271 -271
  35. package/dist/apps/data/routes/addresses.d.ts +398 -167
  36. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  37. package/dist/apps/data/routes/balances.d.ts +278 -8
  38. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  39. package/dist/apps/data/routes/balances.js +114 -2
  40. package/dist/apps/data/routes/balances.js.map +1 -1
  41. package/dist/apps/data/routes/blocks.d.ts +123 -123
  42. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  43. package/dist/apps/data/routes/earn.d.ts +498 -6
  44. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  45. package/dist/apps/data/routes/earn.js +876 -4
  46. package/dist/apps/data/routes/earn.js.map +1 -1
  47. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  48. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  49. package/dist/apps/data/routes/indexer.d.ts +6 -6
  50. package/dist/apps/data/routes/indexer.js +4 -1
  51. package/dist/apps/data/routes/indexer.js.map +1 -1
  52. package/dist/apps/data/routes/receipts.d.ts +235 -235
  53. package/dist/apps/data/routes/rpc.d.ts +31 -31
  54. package/dist/apps/data/routes/rpc.js +4 -1
  55. package/dist/apps/data/routes/rpc.js.map +1 -1
  56. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  57. package/dist/apps/data/routes/tokens.d.ts +449 -449
  58. package/dist/apps/data/routes/transactions.d.ts +2 -2
  59. package/dist/apps/data/routes/transfers.d.ts +81 -81
  60. package/dist/apps/data/routes/valuation.d.ts +1 -1
  61. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  62. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  63. package/dist/apps/data/routes/zones.d.ts +14 -14
  64. package/dist/apps/funding/Access.d.ts +16 -0
  65. package/dist/apps/funding/Access.d.ts.map +1 -0
  66. package/dist/apps/funding/Access.js +22 -0
  67. package/dist/apps/funding/Access.js.map +1 -0
  68. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
  69. package/dist/apps/funding/App.d.ts.map +1 -0
  70. package/dist/apps/funding/App.js +61 -0
  71. package/dist/apps/funding/App.js.map +1 -0
  72. package/dist/apps/funding/Inventory.d.ts +5 -0
  73. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  74. package/dist/apps/funding/Inventory.js +25 -0
  75. package/dist/apps/funding/Inventory.js.map +1 -0
  76. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  77. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  78. package/dist/apps/funding/ProviderAttempts.js +24 -0
  79. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  80. package/dist/apps/funding/index.d.ts +2 -0
  81. package/dist/apps/funding/index.d.ts.map +1 -0
  82. package/dist/apps/funding/index.js +2 -0
  83. package/dist/apps/funding/index.js.map +1 -0
  84. package/dist/apps/funding/routes/chains.d.ts +169 -0
  85. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/chains.js +80 -0
  87. package/dist/apps/funding/routes/chains.js.map +1 -0
  88. package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/deposit-addresses.js +778 -0
  91. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  92. package/dist/apps/funding/routes/providers.d.ts +91 -0
  93. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/providers.js +56 -0
  95. package/dist/apps/funding/routes/providers.js.map +1 -0
  96. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  97. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/quotes.js +188 -0
  99. package/dist/apps/funding/routes/quotes.js.map +1 -0
  100. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  101. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  102. package/dist/apps/funding/routes/transfers.js +614 -0
  103. package/dist/apps/funding/routes/transfers.js.map +1 -0
  104. package/dist/apps/index.d.ts +1 -0
  105. package/dist/apps/index.d.ts.map +1 -1
  106. package/dist/apps/index.js +1 -0
  107. package/dist/apps/index.js.map +1 -1
  108. package/dist/apps/management/App.d.ts +430 -165
  109. package/dist/apps/management/App.d.ts.map +1 -1
  110. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  111. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  112. package/dist/apps/management/routes/api-keys.js +75 -1
  113. package/dist/apps/management/routes/api-keys.js.map +1 -1
  114. package/dist/apps/management/routes/billing.d.ts +6 -6
  115. package/dist/apps/management/routes/faucet.d.ts +35 -35
  116. package/dist/apps/management/routes/invitations.d.ts +122 -122
  117. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  118. package/dist/apps/management/routes/members.d.ts +69 -69
  119. package/dist/apps/management/routes/orgs.d.ts +99 -99
  120. package/dist/apps/management/routes/projects.d.ts +25 -25
  121. package/dist/apps/management/routes/scopes.d.ts +1 -1
  122. package/dist/apps/management/routes/usage.d.ts +72 -72
  123. package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
  124. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  125. package/dist/apps/mcp.d.ts +2 -3
  126. package/dist/apps/mcp.d.ts.map +1 -1
  127. package/dist/apps/mcp.js +16 -14
  128. package/dist/apps/mcp.js.map +1 -1
  129. package/dist/apps/metadata.d.ts +4 -0
  130. package/dist/apps/metadata.d.ts.map +1 -1
  131. package/dist/apps/metadata.js +10 -0
  132. package/dist/apps/metadata.js.map +1 -1
  133. package/dist/apps/mpp/App.d.ts +44 -44
  134. package/dist/cloudflare.d.ts +50 -0
  135. package/dist/cloudflare.d.ts.map +1 -1
  136. package/dist/cloudflare.js +178 -0
  137. package/dist/cloudflare.js.map +1 -1
  138. package/dist/db/Db.d.ts +2 -0
  139. package/dist/db/Db.d.ts.map +1 -1
  140. package/dist/db/Db.js +45 -0
  141. package/dist/db/Db.js.map +1 -1
  142. package/dist/db/Schema.d.ts +12 -0
  143. package/dist/db/Schema.d.ts.map +1 -1
  144. package/dist/db/Schema.js +20 -0
  145. package/dist/db/Schema.js.map +1 -1
  146. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  147. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  148. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  149. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  150. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  151. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  152. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  153. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  154. package/dist/db/tables/fundingDeposits.d.ts +74 -1
  155. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  156. package/dist/db/tables/fundingDeposits.js +126 -0
  157. package/dist/db/tables/fundingDeposits.js.map +1 -1
  158. package/dist/internal/Auth.d.ts +15 -11
  159. package/dist/internal/Auth.d.ts.map +1 -1
  160. package/dist/internal/Auth.js +52 -17
  161. package/dist/internal/Auth.js.map +1 -1
  162. package/dist/internal/Cache.js +1 -1
  163. package/dist/internal/Cache.js.map +1 -1
  164. package/dist/internal/EdgeCache.d.ts +2 -2
  165. package/dist/internal/EdgeCache.d.ts.map +1 -1
  166. package/dist/internal/EdgeCache.js +10 -4
  167. package/dist/internal/EdgeCache.js.map +1 -1
  168. package/dist/internal/IpAllowlist.d.ts +15 -0
  169. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  170. package/dist/internal/IpAllowlist.js +27 -0
  171. package/dist/internal/IpAllowlist.js.map +1 -0
  172. package/dist/internal/OpenApi.d.ts +1 -1
  173. package/dist/internal/OpenApi.js +1 -1
  174. package/dist/internal/OpenApi.js.map +1 -1
  175. package/dist/internal/Tidx.d.ts +6 -0
  176. package/dist/internal/Tidx.d.ts.map +1 -1
  177. package/dist/internal/Tidx.js +13 -3
  178. package/dist/internal/Tidx.js.map +1 -1
  179. package/dist/internal/Viem.d.ts +8 -0
  180. package/dist/internal/Viem.d.ts.map +1 -1
  181. package/dist/internal/Viem.js +12 -5
  182. package/dist/internal/Viem.js.map +1 -1
  183. package/dist/internal/funding/Deposit.d.ts +42 -0
  184. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  185. package/dist/internal/funding/Deposit.js +37 -5
  186. package/dist/internal/funding/Deposit.js.map +1 -1
  187. package/dist/internal/funding/Evidence.d.ts +1 -5
  188. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  189. package/dist/internal/funding/Evidence.js +4 -3
  190. package/dist/internal/funding/Evidence.js.map +1 -1
  191. package/dist/internal/funding/Provider.d.ts +102 -0
  192. package/dist/internal/funding/Provider.d.ts.map +1 -1
  193. package/dist/internal/funding/Provider.js +5 -0
  194. package/dist/internal/funding/Provider.js.map +1 -1
  195. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  196. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  197. package/dist/internal/funding/Reconciliation.js +348 -54
  198. package/dist/internal/funding/Reconciliation.js.map +1 -1
  199. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  200. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  201. package/dist/internal/funding/SourceObservation.js +143 -0
  202. package/dist/internal/funding/SourceObservation.js.map +1 -0
  203. package/dist/internal/funding/Subsidy.d.ts +2 -2
  204. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  205. package/dist/internal/funding/Subsidy.js +1 -1
  206. package/dist/internal/funding/Subsidy.js.map +1 -1
  207. package/dist/internal/funding/index.d.ts +1 -0
  208. package/dist/internal/funding/index.d.ts.map +1 -1
  209. package/dist/internal/funding/index.js +1 -0
  210. package/dist/internal/funding/index.js.map +1 -1
  211. package/dist/internal/funding/providers/relay.d.ts +43 -1
  212. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  213. package/dist/internal/funding/providers/relay.js +438 -27
  214. package/dist/internal/funding/providers/relay.js.map +1 -1
  215. package/package.json +3 -3
  216. package/src/ApiKey.test.ts +35 -0
  217. package/src/ApiKey.ts +27 -0
  218. package/src/ApiKeys.test.ts +29 -0
  219. package/src/ApiKeys.ts +11 -3
  220. package/src/App.test.ts +55 -5
  221. package/src/App.ts +79 -5
  222. package/src/Client.test-d.ts +203 -28
  223. package/src/admin/apps/api-keys.test.ts +25 -1
  224. package/src/admin/apps/api-keys.ts +6 -2
  225. package/src/admin/ui.generated.ts +67 -67
  226. package/src/analytics/Analytics.ts +4 -0
  227. package/src/apps/composition.test.ts +130 -3
  228. package/src/apps/data/App.test.ts +0 -12
  229. package/src/apps/data/App.ts +4 -8
  230. package/src/apps/data/routes/balances.test.ts +42 -0
  231. package/src/apps/data/routes/balances.ts +218 -85
  232. package/src/apps/data/routes/earn.test.ts +363 -0
  233. package/src/apps/data/routes/earn.ts +1141 -3
  234. package/src/apps/data/routes/indexer.ts +2 -1
  235. package/src/apps/data/routes/rpc.ts +2 -0
  236. package/src/apps/data/routes/tokens.test.ts +6 -8
  237. package/src/apps/funding/Access.ts +26 -0
  238. package/src/apps/funding/App.test.ts +78 -0
  239. package/src/apps/funding/App.ts +78 -0
  240. package/src/apps/funding/Inventory.ts +30 -0
  241. package/src/apps/funding/ProviderAttempts.ts +26 -0
  242. package/src/apps/funding/index.ts +1 -0
  243. package/src/apps/funding/routes/chains.test.ts +78 -0
  244. package/src/apps/funding/routes/chains.ts +101 -0
  245. package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
  246. package/src/apps/funding/routes/deposit-addresses.ts +932 -0
  247. package/src/apps/funding/routes/providers.test.ts +165 -0
  248. package/src/apps/funding/routes/providers.ts +73 -0
  249. package/src/apps/funding/routes/quotes.test.ts +511 -0
  250. package/src/apps/funding/routes/quotes.ts +205 -0
  251. package/src/apps/funding/routes/transfers.test.ts +1236 -0
  252. package/src/apps/funding/routes/transfers.ts +704 -0
  253. package/src/apps/index.ts +1 -0
  254. package/src/apps/management/routes/api-keys.test.ts +98 -0
  255. package/src/apps/management/routes/api-keys.ts +96 -1
  256. package/src/apps/mcp.test.ts +17 -2
  257. package/src/apps/mcp.ts +18 -13
  258. package/src/apps/metadata.ts +12 -0
  259. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  260. package/src/cloudflare.ts +279 -0
  261. package/src/db/Db.ts +71 -0
  262. package/src/db/Schema.ts +24 -0
  263. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  264. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  265. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  266. package/src/db/tables/fundingDeposits.test.ts +239 -0
  267. package/src/db/tables/fundingDeposits.ts +214 -1
  268. package/src/internal/Auth.test.ts +205 -5
  269. package/src/internal/Auth.ts +63 -19
  270. package/src/internal/Cache.test.ts +37 -3
  271. package/src/internal/Cache.ts +1 -1
  272. package/src/internal/EdgeCache.test.ts +7 -7
  273. package/src/internal/EdgeCache.ts +13 -4
  274. package/src/internal/IpAllowlist.test.ts +52 -0
  275. package/src/internal/IpAllowlist.ts +39 -0
  276. package/src/internal/Log.test.ts +7 -1
  277. package/src/internal/OpenApi.ts +1 -1
  278. package/src/internal/Tidx.test.ts +28 -0
  279. package/src/internal/Tidx.ts +19 -3
  280. package/src/internal/Viem.test.ts +28 -0
  281. package/src/internal/Viem.ts +21 -5
  282. package/src/internal/funding/Deposit.ts +74 -5
  283. package/src/internal/funding/Evidence.test.ts +2 -2
  284. package/src/internal/funding/Evidence.ts +4 -9
  285. package/src/internal/funding/Provider.ts +115 -0
  286. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  287. package/src/internal/funding/Reconciliation.test.ts +1364 -109
  288. package/src/internal/funding/Reconciliation.ts +467 -88
  289. package/src/internal/funding/SourceObservation.test.ts +301 -0
  290. package/src/internal/funding/SourceObservation.ts +267 -0
  291. package/src/internal/funding/Subsidy.ts +2 -2
  292. package/src/internal/funding/index.test.ts +1 -0
  293. package/src/internal/funding/index.ts +1 -0
  294. package/src/internal/funding/providers/relay.test.ts +94 -4
  295. package/src/internal/funding/providers/relay.ts +533 -27
  296. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  297. package/dist/apps/data/routes/funding.js +0 -1557
  298. package/dist/apps/data/routes/funding.js.map +0 -1
  299. package/src/apps/data/routes/funding.test.ts +0 -2994
  300. package/src/apps/data/routes/funding.ts +0 -1821
@@ -359,9 +359,13 @@ export class ConfigError extends Error {
359
359
 
360
360
  /** Failed ClickHouse insert (non-2xx HTTP response). */
361
361
  export class InsertError extends Error {
362
+ /** HTTP response status returned by ClickHouse. */
363
+ status: number
364
+
362
365
  constructor(status: number, body: string) {
363
366
  super(`ClickHouse insert failed with status ${status}: ${body.slice(0, 500)}`)
364
367
  this.name = 'Analytics.InsertError'
368
+ this.status = status
365
369
  }
366
370
  }
367
371
 
@@ -1,5 +1,5 @@
1
1
  import { App, Store } from 'tapimo'
2
- import { data, management, mpp, relay } from 'tapimo/apps'
2
+ import { data, funding, management, mpp, relay } from 'tapimo/apps'
3
3
  import { Hono } from 'hono'
4
4
  import { describeRoute } from 'hono-openapi'
5
5
  import * as TestApp from '../../test/App.js'
@@ -14,8 +14,16 @@ async function document(app: { request: (path: string) => Promise<Response> | Re
14
14
  const res = await app.request('/openapi.json')
15
15
  return (await res.json()) as {
16
16
  components: { securitySchemes: Record<string, unknown> }
17
- paths: Record<string, unknown>
18
- tags: readonly { description: string; name: string }[]
17
+ paths: Record<
18
+ string,
19
+ Partial<Record<'get' | 'post', { operationId: string; tags?: readonly string[] | undefined }>>
20
+ >
21
+ tags: readonly {
22
+ description: string
23
+ name: string
24
+ 'x-displayName'?: string | undefined
25
+ 'x-pagePath'?: string | undefined
26
+ }[]
19
27
  webhooks?: Record<string, unknown> | undefined
20
28
  'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
21
29
  }
@@ -146,10 +154,12 @@ describe('data()', () => {
146
154
  expect(keys.some((path) => path.startsWith('/v1/blocks'))).toBe(true)
147
155
  expect(keys.some((path) => path.startsWith('/v1/orgs'))).toBe(false)
148
156
  expect(keys.some((path) => path.startsWith('/v1/me'))).toBe(false)
157
+ expect(keys.some((path) => path.startsWith('/v1/funding'))).toBe(false)
149
158
 
150
159
  // The omitted management route resolves to a 404 (never mounted), not a 401.
151
160
  const orgs = await app.request('/v1/orgs')
152
161
  expect(orgs.status).toBe(404)
162
+ expect((await app.request('/v1/funding/chains')).status).toBe(404)
153
163
  })
154
164
 
155
165
  test('inherits shared chain context', async () => {
@@ -272,6 +282,123 @@ describe('data()', () => {
272
282
  })
273
283
  })
274
284
 
285
+ describe('funding()', () => {
286
+ test('mounts the Funding & Bridge API independently', async () => {
287
+ const group = funding()
288
+ expectTypeOf(App.funding).toEqualTypeOf<typeof funding>()
289
+ expectTypeOf(group).toEqualTypeOf<App.FundingApp>()
290
+ const app = App.create({ auth: false, db: TestApp.database() }).route('/', group)
291
+ const spec = await document(app)
292
+
293
+ expect(
294
+ Object.keys(spec.paths)
295
+ .filter((path) => path.startsWith('/v1/funding'))
296
+ .sort(),
297
+ ).toEqual(
298
+ [
299
+ '/v1/funding/chains',
300
+ '/v1/funding/deposit-addresses',
301
+ '/v1/funding/deposit-addresses/{id}',
302
+ '/v1/funding/deposit-addresses/{id}/reconcile',
303
+ '/v1/funding/deposits',
304
+ '/v1/funding/deposits/{id}',
305
+ '/v1/funding/providers',
306
+ '/v1/funding/quotes',
307
+ '/v1/funding/transfers',
308
+ '/v1/funding/transfers/vault',
309
+ '/v1/funding/transfers/zone',
310
+ '/v1/funding/transfers/{id}',
311
+ ].sort(),
312
+ )
313
+ expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/tokens'))).toBe(false)
314
+ expect(spec.paths['/webhooks/funding/{providerId}']).toBeUndefined()
315
+ expect(spec['x-tagGroups']).toEqual([
316
+ {
317
+ name: 'Funding & Bridge API',
318
+ tags: ['Chains', 'Deposit Addresses', 'Providers', 'Quotes', 'Funding Transfers'],
319
+ },
320
+ ])
321
+ expect(
322
+ Object.values(spec.paths).flatMap((path) =>
323
+ [path.get, path.post].flatMap((operation) =>
324
+ operation?.tags?.includes('Deposit Addresses') ? [operation.operationId] : [],
325
+ ),
326
+ ),
327
+ ).toEqual([
328
+ 'createFundingDepositAddress',
329
+ 'reconcileFundingDepositAddress',
330
+ 'getFundingDepositAddress',
331
+ 'getFundingDeposit',
332
+ 'listFundingDeposits',
333
+ ])
334
+ expect((await app.request('/v1/tokens')).status).toBe(404)
335
+ })
336
+
337
+ test('keeps data and funding transfers separate with the same display name', async () => {
338
+ const app = App.create({ auth: false, db: TestApp.database() })
339
+ .route('/', data())
340
+ .route('/', funding())
341
+ const spec = await document(app)
342
+ const operationTag = (path: string, method: 'get' | 'post') =>
343
+ (spec.paths[path] as Record<string, { tags?: readonly string[] | undefined } | undefined>)[
344
+ method
345
+ ]?.tags
346
+
347
+ expect({
348
+ chains: operationTag('/v1/funding/chains', 'get'),
349
+ depositAddress: operationTag('/v1/funding/deposit-addresses', 'post'),
350
+ deposits: operationTag('/v1/funding/deposits', 'get'),
351
+ providers: operationTag('/v1/funding/providers', 'get'),
352
+ quotes: operationTag('/v1/funding/quotes', 'get'),
353
+ tokenTransfers: operationTag('/v1/transfers', 'get'),
354
+ transfers: operationTag('/v1/funding/transfers', 'get'),
355
+ vault: operationTag('/v1/funding/transfers/vault', 'post'),
356
+ zone: operationTag('/v1/funding/transfers/zone', 'post'),
357
+ }).toEqual({
358
+ chains: ['Chains'],
359
+ depositAddress: ['Deposit Addresses'],
360
+ deposits: ['Deposit Addresses'],
361
+ providers: ['Providers'],
362
+ quotes: ['Quotes'],
363
+ tokenTransfers: ['Transfers'],
364
+ transfers: ['Funding Transfers'],
365
+ vault: ['Funding Transfers'],
366
+ zone: ['Funding Transfers'],
367
+ })
368
+
369
+ const groups = Object.fromEntries(
370
+ (spec['x-tagGroups'] ?? []).map((group) => [group.name, group.tags]),
371
+ )
372
+ expect(groups['Data API']).toContain('Transfers')
373
+ expect(groups['Data API']).not.toContain('Funding Transfers')
374
+ expect(groups['Funding & Bridge API']).toContain('Funding Transfers')
375
+ expect(groups['Funding & Bridge API']).not.toContain('Transfers')
376
+ expect(spec.tags.filter((tag) => tag.name === 'Transfers')).toHaveLength(1)
377
+ expect(spec.tags.filter((tag) => tag.name === 'Funding Transfers')).toEqual([
378
+ {
379
+ description: 'Inbound funding transfers.',
380
+ name: 'Funding Transfers',
381
+ 'x-displayName': 'Transfers',
382
+ 'x-pagePath': 'funding/transfers',
383
+ },
384
+ ])
385
+ expect(
386
+ Object.fromEntries(
387
+ (groups['Funding & Bridge API'] ?? []).map((name) => {
388
+ const tag = spec.tags.find((tag) => tag.name === name)
389
+ return [tag?.['x-displayName'] ?? tag?.name, tag?.['x-pagePath']]
390
+ }),
391
+ ),
392
+ ).toEqual({
393
+ Chains: 'funding/chains',
394
+ 'Deposit Addresses': 'funding/deposit-addresses',
395
+ Providers: 'funding/providers',
396
+ Quotes: 'funding/quotes',
397
+ Transfers: 'funding/transfers',
398
+ })
399
+ })
400
+ })
401
+
275
402
  describe('relay()', () => {
276
403
  test('mounts the relay after data() without documenting RPC routes', async () => {
277
404
  const composed = App.create({ db: TestApp.database() }).route('/', data()).route('/', relay())
@@ -386,18 +386,6 @@ describe('zone chain gate', () => {
386
386
  expect((await privateApp.request(path)).status).toBe(403)
387
387
  })
388
388
 
389
- test('public chains remain edge-cacheable on a Zone deployment', async () => {
390
- const store = Store.memory()
391
- const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] })
392
- const path = '/v1/funding/chains'
393
-
394
- const first = await app.request(path)
395
- const second = await app.request(path)
396
- expect(first.status).toBe(200)
397
- expect(second.status).toBe(200)
398
- expect(second.headers.get('cache-control')).toContain('public')
399
- })
400
-
401
389
  test('serves anonymous token lists from the edge cache on public chains', async () => {
402
390
  const store = Store.memory()
403
391
  const app = TestApp.create({ auth: false, cache: { store }, zones: [zoneOptions(zone)] })
@@ -25,7 +25,6 @@ import { coingecko } from './routes/coingecko.js'
25
25
  import { earn } from './routes/earn.js'
26
26
  import { exchanges } from './routes/exchanges.js'
27
27
  import { feeAmm } from './routes/fee-amm.js'
28
- import { funding } from './routes/funding.js'
29
28
  import { indexer } from './routes/indexer.js'
30
29
  import { receipts } from './routes/receipts.js'
31
30
  import { rpc } from './routes/rpc.js'
@@ -217,7 +216,6 @@ export function data(options: data.Options = {}) {
217
216
  .route('/', earn_routes)
218
217
  .route('/', exchanges({ fx: options.fx }))
219
218
  .route('/', feeAmm())
220
- .route('/', funding())
221
219
  .route('/', indexer_routes)
222
220
  .route('/', receipts({ fx: options.fx }))
223
221
  .route('/', tokenlist())
@@ -375,13 +373,12 @@ const openapi = {
375
373
  { name: 'Earn', description: 'Vaults that earn yield from assets deposited on Tempo.' },
376
374
  { name: 'Exchange', description: "Tempo's built-in stablecoin exchange: pairs, swaps, orders, prices." }, // prettier-ignore
377
375
  { name: 'Fee AMM', description: 'Pools that convert stablecoins to pay fees.' },
378
- {
379
- name: 'Funding',
380
- description: 'Quote, transfer, and deposit management for funding Tempo from other chains.',
381
- },
382
376
  { name: 'Tokens', description: 'TIP-20 token details, supply, and holders.' },
383
377
  { name: 'Transactions', description: 'Transactions submitted to Tempo, and their receipts.' },
384
- { name: 'Transfers', description: 'Token movements from executing transactions.' },
378
+ {
379
+ name: 'Transfers',
380
+ description: 'Token movements from executing transactions.',
381
+ },
385
382
  { name: 'Verified Tokens', description: 'A curated, trusted list of TIP-20 tokens.' },
386
383
  { name: 'Webhooks', description: 'Get signed callbacks when onchain events happen.' },
387
384
  { name: 'Zones', description: 'Private chain operations anchored to Tempo.' },
@@ -399,7 +396,6 @@ const openapi = {
399
396
  'Earn',
400
397
  'Exchange',
401
398
  'Fee AMM',
402
- 'Funding',
403
399
  'Tokens',
404
400
  'Transactions',
405
401
  'Transfers',
@@ -3,6 +3,7 @@
3
3
  import { Schema } from 'tapimo'
4
4
  import { FxOracle } from 'tapimo/apps'
5
5
  import { Value as core_Value } from 'ox'
6
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
6
7
  import { Actions } from 'viem/tempo'
7
8
 
8
9
  import * as TestApp from '../../../../test/App.js'
@@ -469,3 +470,44 @@ describe('GET /addresses/:address/balances', () => {
469
470
  expect(body.meta!.totalCount).toBeGreaterThanOrEqual(body.data.length)
470
471
  })
471
472
  })
473
+
474
+ describe('GET /v1/addresses/:address/balances/:token', () => {
475
+ test('returns one live balance without indexed holder discovery', async () => {
476
+ const client = app()
477
+ const holder = await holderWithBalance
478
+ const response = await client.v1.addresses[':address'].balances[':token'].$get(
479
+ {
480
+ param: { address: holder, token: TestApp.tokenWithHolders },
481
+ query: {},
482
+ },
483
+ TestApp.auth,
484
+ )
485
+ const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response)
486
+
487
+ expect(response.status).toBe(200)
488
+ expect(body.id).toBe(TestApp.tokenWithHolders)
489
+ expect(body.token.address).toBe(TestApp.tokenWithHolders)
490
+ expect(body.amount).toMatch(/^\d+$/)
491
+ expect(core_Value.from(body.formatted, body.decimals)).toBe(BigInt(body.amount))
492
+ expect(body.valuation).toEqual({ amount: body.formatted, currency: 'USD' })
493
+ })
494
+
495
+ test('returns zero for a token the account does not hold', async () => {
496
+ const client = app()
497
+ const response = await client.v1.addresses[':address'].balances[':token'].$get(
498
+ {
499
+ param: {
500
+ address: privateKeyToAccount(generatePrivateKey()).address,
501
+ token: TestApp.tokenWithHolders,
502
+ },
503
+ query: {},
504
+ },
505
+ TestApp.auth,
506
+ )
507
+ const body = await TestApp.json(response, Balances.schema.getAddressBalance.Response)
508
+
509
+ expect(response.status).toBe(200)
510
+ expect(body.amount).toBe('0')
511
+ expect(body.formatted).toBe('0')
512
+ })
513
+ })
@@ -181,102 +181,235 @@ export namespace schema {
181
181
  })
182
182
  .check(z.describe('A page of TIP-20 token balances held by this account.'))
183
183
  }
184
+
185
+ /** Schemas for the getAddressBalance operation. */
186
+ export namespace getAddressBalance {
187
+ /** Path parameters for a single address balance request. */
188
+ export const Params = z
189
+ .object({
190
+ address: Schema.Address.check(
191
+ z.describe('The account address whose token balance you want to read.'),
192
+ ),
193
+ token: Schema.TokenAddress.check(z.describe('The TIP-20 token contract address.')),
194
+ })
195
+ .check(z.describe('Path parameters for reading one account token balance.'))
196
+
197
+ /** Query parameters for a single address balance request. */
198
+ export const Query = z
199
+ .strictObject({
200
+ chainId: Schema.ChainIdQuery,
201
+ 'valuation.currency': Schema.Denomination,
202
+ })
203
+ .check(z.describe('Query parameters for reading one account token balance.'))
204
+
205
+ /** A single TIP-20 token balance held by an address. */
206
+ export const Response = getAddressBalances.Balance.check(
207
+ z.describe('One TIP-20 token balance held by this account.'),
208
+ )
209
+ }
184
210
  }
185
211
 
186
212
  /**
187
213
  * Creates address-scoped balance handlers, mounted under the `/addresses`
188
- * composer. Exposes `GET /:address/balances`, the held-balance page.
214
+ * composer. Exposes the held-balance page and individual token balances.
189
215
  */
190
216
  export function addresses(options: addresses.Options = {}) {
191
217
  const oracle = options.fx?.oracle ?? FxOracle.ecb()
192
- return new Hono<App.Environment>().get(
193
- '/v1/addresses/:address/balances',
194
- Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
195
- OpenApi.validate('param', schema.getAddressBalances.Params, {
196
- code: 'address_invalid',
197
- message: 'Invalid account address',
198
- }),
199
- OpenApi.validate('query', schema.getAddressBalances.Query, {
200
- code: 'query_invalid',
201
- message: 'Invalid query parameters',
202
- }),
203
- OpenApi.describeRoute({
204
- description:
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.',
206
- operationId: 'getAddressBalances',
207
- responses: OpenApi.responses({
208
- errors: { 502: 'Could not read balance or token data from an upstream service.' },
209
- success: {
210
- description: 'A page of token balances for this account.',
211
- schema: schema.getAddressBalances.Response,
212
- },
218
+ return new Hono<App.Environment>()
219
+ .get(
220
+ '/v1/addresses/:address/balances',
221
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
222
+ OpenApi.validate('param', schema.getAddressBalances.Params, {
223
+ code: 'address_invalid',
224
+ message: 'Invalid account address',
213
225
  }),
214
- summary: 'List address balances',
215
- tags: ['Balances'],
216
- }),
217
- Cache.response<App.Environment>({
218
- cacheControl: Cache.policies.noStore,
219
- name: 'tempo-api:addresses:v1',
220
- key: (c) => Cache.urlKey(c, schema.getAddressBalances.Query),
221
- }),
222
- async (c) => {
223
- if (Auth.narrowAccess) return Auth.paidAccessError(c)
224
- if (OpenApi.narrowValidation)
225
- return OpenApi.validationError(c, {
226
- code: 'address_invalid',
227
- message: 'Invalid request parameters',
228
- })
229
- if (OpenApi.narrowValidation)
230
- return OpenApi.validationError(c, {
231
- code: 'query_invalid',
232
- message: 'Invalid request parameters',
233
- })
234
-
235
- const { address } = c.req.valid('param')
236
- const query = c.req.valid('query')
226
+ OpenApi.validate('query', schema.getAddressBalances.Query, {
227
+ code: 'query_invalid',
228
+ message: 'Invalid query parameters',
229
+ }),
230
+ OpenApi.describeRoute({
231
+ description:
232
+ '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.',
233
+ operationId: 'getAddressBalances',
234
+ responses: OpenApi.responses({
235
+ errors: { 502: 'Could not read balance or token data from an upstream service.' },
236
+ success: {
237
+ description: 'A page of token balances for this account.',
238
+ schema: schema.getAddressBalances.Response,
239
+ },
240
+ }),
241
+ summary: 'List address balances',
242
+ tags: ['Balances'],
243
+ }),
244
+ Cache.response<App.Environment>({
245
+ cacheControl: Cache.policies.noStore,
246
+ name: 'tempo-api:addresses:v1',
247
+ key: (c) => Cache.urlKey(c, schema.getAddressBalances.Query),
248
+ }),
249
+ async (c) => {
250
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
251
+ if (OpenApi.narrowValidation)
252
+ return OpenApi.validationError(c, {
253
+ code: 'address_invalid',
254
+ message: 'Invalid request parameters',
255
+ })
256
+ if (OpenApi.narrowValidation)
257
+ return OpenApi.validationError(c, {
258
+ code: 'query_invalid',
259
+ message: 'Invalid request parameters',
260
+ })
261
+
262
+ const { address } = c.req.valid('param')
263
+ const query = c.req.valid('query')
237
264
 
238
- try {
265
+ try {
266
+ const chainId = query.chainId ?? c.get('chainId')
267
+ // `currency` only makes sense against verified tokens, so it implies
268
+ // `verified=true` from the caller's perspective.
269
+ const verified = query.verified || query.currency !== undefined
270
+ const feeEligible = query.feeEligible ?? false
271
+
272
+ // The fee-token set powers both the per-row `feeEligible` flag (always)
273
+ // and the optional `feeEligible=true` page restriction. It is memoized
274
+ // per chain (a near-constant), so one read feeds the page, the count,
275
+ // and the row enrichment. It is a best-effort enrichment: a lookup
276
+ // failure omits `feeEligible` rather than failing the page — unless the
277
+ // caller explicitly asked to filter by `feeEligible`, where the filter
278
+ // can't be honored, so the failure surfaces as the declared `502`.
279
+ const feeTokens = await FeeAmm.feeTokenSet(c, chainId).catch((cause) => {
280
+ if (feeEligible) throw cause
281
+ return undefined
282
+ })
283
+
284
+ const balances = await getAddressBalances(c, {
285
+ address,
286
+ chainId,
287
+ currency: query.currency,
288
+ cursor: query.cursor,
289
+ denomination: query['valuation.currency'] ?? 'USD',
290
+ feeEligible,
291
+ feeTokens,
292
+ includeTotalCount: query.include.includes('totalCount'),
293
+ limit: query.limit,
294
+ oracle,
295
+ page: query.page,
296
+ verified,
297
+ })
298
+
299
+ return c.json(Response.validated(schema.getAddressBalances.Response, balances), 200)
300
+ } catch (cause) {
301
+ if (cause instanceof Valuation.UnsupportedDenominationError)
302
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
303
+ return Response.upstream(c, cause)
304
+ }
305
+ },
306
+ )
307
+ .get(
308
+ '/v1/addresses/:address/balances/:token',
309
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
310
+ OpenApi.validate('param', schema.getAddressBalance.Params, {
311
+ code: 'address_invalid',
312
+ message: 'Invalid address or token address',
313
+ }),
314
+ OpenApi.validate('query', schema.getAddressBalance.Query, {
315
+ code: 'query_invalid',
316
+ message: 'Invalid query parameters',
317
+ }),
318
+ OpenApi.describeRoute({
319
+ description:
320
+ 'Returns the live balance of one TIP-20 token held by an account, including token metadata and a human-readable amount.',
321
+ operationId: 'getAddressBalance',
322
+ responses: OpenApi.responses({
323
+ errors: {
324
+ 404: {
325
+ description: 'The token is not a registered TIP-20 token.',
326
+ codes: ['token_not_found'],
327
+ },
328
+ 502: 'Could not read balance or token data from an upstream service.',
329
+ },
330
+ success: {
331
+ description: 'The requested token balance for this account.',
332
+ schema: schema.getAddressBalance.Response,
333
+ },
334
+ }),
335
+ summary: 'Get address balance',
336
+ tags: ['Balances'],
337
+ }),
338
+ Cache.response<App.Environment>({
339
+ cacheControl: Cache.policies.noStore,
340
+ name: 'tempo-api:addresses:v1',
341
+ key: (c) => Cache.urlKey(c, schema.getAddressBalance.Query),
342
+ }),
343
+ async (c) => {
344
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
345
+ if (OpenApi.narrowValidation)
346
+ return OpenApi.validationError(c, {
347
+ code: 'address_invalid',
348
+ message: 'Invalid request parameters',
349
+ })
350
+ if (OpenApi.narrowValidation)
351
+ return OpenApi.validationError(c, {
352
+ code: 'query_invalid',
353
+ message: 'Invalid request parameters',
354
+ })
355
+
356
+ const { address, token } = c.req.valid('param')
357
+ const query = c.req.valid('query')
239
358
  const chainId = query.chainId ?? c.get('chainId')
240
- // `currency` only makes sense against verified tokens, so it implies
241
- // `verified=true` from the caller's perspective.
242
- const verified = query.verified || query.currency !== undefined
243
- const feeEligible = query.feeEligible ?? false
244
-
245
- // The fee-token set powers both the per-row `feeEligible` flag (always)
246
- // and the optional `feeEligible=true` page restriction. It is memoized
247
- // per chain (a near-constant), so one read feeds the page, the count,
248
- // and the row enrichment. It is a best-effort enrichment: a lookup
249
- // failure omits `feeEligible` rather than failing the page — unless the
250
- // caller explicitly asked to filter by `feeEligible`, where the filter
251
- // can't be honored, so the failure surfaces as the declared `502`.
252
- const feeTokens = await FeeAmm.feeTokenSet(c, chainId).catch((cause) => {
253
- if (feeEligible) throw cause
254
- return undefined
255
- })
256
-
257
- const balances = await getAddressBalances(c, {
258
- address,
259
- chainId,
260
- currency: query.currency,
261
- cursor: query.cursor,
262
- denomination: query['valuation.currency'] ?? 'USD',
263
- feeEligible,
264
- feeTokens,
265
- includeTotalCount: query.include.includes('totalCount'),
266
- limit: query.limit,
267
- oracle,
268
- page: query.page,
269
- verified,
270
- })
271
359
 
272
- return c.json(Response.validated(schema.getAddressBalances.Response, balances), 200)
273
- } catch (cause) {
274
- if (cause instanceof Valuation.UnsupportedDenominationError)
275
- return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
276
- return Response.upstream(c, cause)
277
- }
278
- },
279
- )
360
+ try {
361
+ const snapshot = await VerifiedTokens.snapshot(c, chainId)
362
+ const [balance, resolvedToken] = await Promise.all([
363
+ Timing.time(c, 'balance_rpc', async () => {
364
+ const result = await Actions.token.getBalance(c.get('getClient')(chainId), {
365
+ account: address,
366
+ token,
367
+ })
368
+ return result.amount
369
+ }),
370
+ Timing.time(c, 'token', () =>
371
+ Tokens.resolveToken(c, { address: token, chainId, include: [], snapshot }),
372
+ ),
373
+ ])
374
+ const denomination = query['valuation.currency'] ?? 'USD'
375
+ const rates = await Valuation.ratesFor(c, {
376
+ currencies: snapshot.byAddress.has(token) ? [resolvedToken.currency] : [],
377
+ denomination,
378
+ oracle,
379
+ })
380
+ const feeTokens = await FeeAmm.feeTokenSet(c, chainId).catch(() => undefined)
381
+
382
+ return c.json(
383
+ Response.validated(schema.getAddressBalance.Response, {
384
+ amount: balance.toString(),
385
+ currency: resolvedToken.currency,
386
+ decimals: resolvedToken.decimals,
387
+ ...(feeTokens ? { feeEligible: feeTokens.has(token) } : {}),
388
+ formatted: core_Value.format(balance, resolvedToken.decimals),
389
+ id: token,
390
+ token: resolvedToken,
391
+ valuation: Valuation.valuationFor({
392
+ amount: balance,
393
+ denomination,
394
+ rates,
395
+ token: snapshot.byAddress.get(token),
396
+ }),
397
+ }),
398
+ 200,
399
+ )
400
+ } catch (cause) {
401
+ if (Tokens.isTokenNotFound(cause))
402
+ return Response.error(c, {
403
+ code: 'token_not_found',
404
+ message: 'Token not found',
405
+ status: 404,
406
+ })
407
+ if (cause instanceof Valuation.UnsupportedDenominationError)
408
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
409
+ return Response.upstream(c, cause)
410
+ }
411
+ },
412
+ )
280
413
  }
281
414
 
282
415
  export declare namespace addresses {