tapimo 0.7.2 → 0.8.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 (294) 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 +20434 -19484
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +62 -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 +168 -168
  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 +10305 -12785
  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 +24 -24
  35. package/dist/apps/data/routes/addresses.d.ts +288 -288
  36. package/dist/apps/data/routes/balances.d.ts +6 -6
  37. package/dist/apps/data/routes/blocks.d.ts +39 -39
  38. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  39. package/dist/apps/data/routes/earn.d.ts +436 -6
  40. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  41. package/dist/apps/data/routes/earn.js +631 -4
  42. package/dist/apps/data/routes/earn.js.map +1 -1
  43. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  44. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  45. package/dist/apps/data/routes/indexer.d.ts +6 -6
  46. package/dist/apps/data/routes/indexer.js +4 -1
  47. package/dist/apps/data/routes/indexer.js.map +1 -1
  48. package/dist/apps/data/routes/receipts.d.ts +235 -235
  49. package/dist/apps/data/routes/rpc.d.ts +1 -1
  50. package/dist/apps/data/routes/rpc.js +4 -1
  51. package/dist/apps/data/routes/rpc.js.map +1 -1
  52. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  53. package/dist/apps/data/routes/tokens.d.ts +6 -6
  54. package/dist/apps/data/routes/transactions.d.ts +2 -2
  55. package/dist/apps/data/routes/transfers.d.ts +1 -1
  56. package/dist/apps/data/routes/valuation.d.ts +1 -1
  57. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  58. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  59. package/dist/apps/data/routes/zones.d.ts +14 -14
  60. package/dist/apps/funding/Access.d.ts +16 -0
  61. package/dist/apps/funding/Access.d.ts.map +1 -0
  62. package/dist/apps/funding/Access.js +22 -0
  63. package/dist/apps/funding/Access.js.map +1 -0
  64. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1717 -3035
  65. package/dist/apps/funding/App.d.ts.map +1 -0
  66. package/dist/apps/funding/App.js +46 -0
  67. package/dist/apps/funding/App.js.map +1 -0
  68. package/dist/apps/funding/Inventory.d.ts +5 -0
  69. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  70. package/dist/apps/funding/Inventory.js +25 -0
  71. package/dist/apps/funding/Inventory.js.map +1 -0
  72. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  73. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  74. package/dist/apps/funding/ProviderAttempts.js +24 -0
  75. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  76. package/dist/apps/funding/index.d.ts +2 -0
  77. package/dist/apps/funding/index.d.ts.map +1 -0
  78. package/dist/apps/funding/index.js +2 -0
  79. package/dist/apps/funding/index.js.map +1 -0
  80. package/dist/apps/funding/routes/chains.d.ts +169 -0
  81. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  82. package/dist/apps/funding/routes/chains.js +80 -0
  83. package/dist/apps/funding/routes/chains.js.map +1 -0
  84. package/dist/apps/funding/routes/deposit-addresses.d.ts +2388 -0
  85. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/deposit-addresses.js +860 -0
  87. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  88. package/dist/apps/funding/routes/providers.d.ts +91 -0
  89. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/providers.js +56 -0
  91. package/dist/apps/funding/routes/providers.js.map +1 -0
  92. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  93. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/quotes.js +188 -0
  95. package/dist/apps/funding/routes/quotes.js.map +1 -0
  96. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  97. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/transfers.js +614 -0
  99. package/dist/apps/funding/routes/transfers.js.map +1 -0
  100. package/dist/apps/index.d.ts +1 -0
  101. package/dist/apps/index.d.ts.map +1 -1
  102. package/dist/apps/index.js +1 -0
  103. package/dist/apps/index.js.map +1 -1
  104. package/dist/apps/management/App.d.ts +430 -165
  105. package/dist/apps/management/App.d.ts.map +1 -1
  106. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  107. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  108. package/dist/apps/management/routes/api-keys.js +75 -1
  109. package/dist/apps/management/routes/api-keys.js.map +1 -1
  110. package/dist/apps/management/routes/billing.d.ts +6 -6
  111. package/dist/apps/management/routes/faucet.d.ts +35 -35
  112. package/dist/apps/management/routes/invitations.d.ts +48 -48
  113. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  114. package/dist/apps/management/routes/members.d.ts +69 -69
  115. package/dist/apps/management/routes/orgs.d.ts +37 -37
  116. package/dist/apps/management/routes/projects.d.ts +25 -25
  117. package/dist/apps/management/routes/scopes.d.ts +1 -1
  118. package/dist/apps/management/routes/usage.d.ts +72 -72
  119. package/dist/apps/management/routes/verified-token-requests.d.ts +18 -18
  120. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  121. package/dist/apps/mcp.d.ts +2 -3
  122. package/dist/apps/mcp.d.ts.map +1 -1
  123. package/dist/apps/mcp.js +16 -14
  124. package/dist/apps/mcp.js.map +1 -1
  125. package/dist/apps/metadata.d.ts +4 -0
  126. package/dist/apps/metadata.d.ts.map +1 -1
  127. package/dist/apps/metadata.js +10 -0
  128. package/dist/apps/metadata.js.map +1 -1
  129. package/dist/apps/mpp/App.d.ts +2 -2
  130. package/dist/cloudflare.d.ts +50 -0
  131. package/dist/cloudflare.d.ts.map +1 -1
  132. package/dist/cloudflare.js +178 -0
  133. package/dist/cloudflare.js.map +1 -1
  134. package/dist/db/Db.d.ts +2 -0
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +37 -0
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +12 -0
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +20 -0
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  143. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  144. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  145. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  146. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  147. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  148. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  149. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  150. package/dist/db/tables/fundingDeposits.d.ts +57 -1
  151. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  152. package/dist/db/tables/fundingDeposits.js +82 -0
  153. package/dist/db/tables/fundingDeposits.js.map +1 -1
  154. package/dist/internal/Auth.d.ts +11 -11
  155. package/dist/internal/Auth.d.ts.map +1 -1
  156. package/dist/internal/Auth.js +41 -15
  157. package/dist/internal/Auth.js.map +1 -1
  158. package/dist/internal/Cache.js +1 -1
  159. package/dist/internal/Cache.js.map +1 -1
  160. package/dist/internal/EdgeCache.d.ts +2 -2
  161. package/dist/internal/EdgeCache.d.ts.map +1 -1
  162. package/dist/internal/EdgeCache.js +10 -4
  163. package/dist/internal/EdgeCache.js.map +1 -1
  164. package/dist/internal/IpAllowlist.d.ts +15 -0
  165. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  166. package/dist/internal/IpAllowlist.js +27 -0
  167. package/dist/internal/IpAllowlist.js.map +1 -0
  168. package/dist/internal/OpenApi.d.ts +1 -1
  169. package/dist/internal/OpenApi.js +1 -1
  170. package/dist/internal/OpenApi.js.map +1 -1
  171. package/dist/internal/Tidx.d.ts +6 -0
  172. package/dist/internal/Tidx.d.ts.map +1 -1
  173. package/dist/internal/Tidx.js +13 -3
  174. package/dist/internal/Tidx.js.map +1 -1
  175. package/dist/internal/Viem.d.ts +8 -0
  176. package/dist/internal/Viem.d.ts.map +1 -1
  177. package/dist/internal/Viem.js +12 -5
  178. package/dist/internal/Viem.js.map +1 -1
  179. package/dist/internal/funding/Deposit.d.ts +42 -0
  180. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  181. package/dist/internal/funding/Deposit.js +37 -5
  182. package/dist/internal/funding/Deposit.js.map +1 -1
  183. package/dist/internal/funding/Evidence.d.ts +1 -5
  184. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  185. package/dist/internal/funding/Evidence.js +4 -3
  186. package/dist/internal/funding/Evidence.js.map +1 -1
  187. package/dist/internal/funding/Provider.d.ts +102 -0
  188. package/dist/internal/funding/Provider.d.ts.map +1 -1
  189. package/dist/internal/funding/Provider.js +5 -0
  190. package/dist/internal/funding/Provider.js.map +1 -1
  191. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  192. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  193. package/dist/internal/funding/Reconciliation.js +257 -48
  194. package/dist/internal/funding/Reconciliation.js.map +1 -1
  195. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  196. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  197. package/dist/internal/funding/SourceObservation.js +143 -0
  198. package/dist/internal/funding/SourceObservation.js.map +1 -0
  199. package/dist/internal/funding/Subsidy.d.ts +2 -2
  200. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  201. package/dist/internal/funding/Subsidy.js +1 -1
  202. package/dist/internal/funding/Subsidy.js.map +1 -1
  203. package/dist/internal/funding/index.d.ts +1 -0
  204. package/dist/internal/funding/index.d.ts.map +1 -1
  205. package/dist/internal/funding/index.js +1 -0
  206. package/dist/internal/funding/index.js.map +1 -1
  207. package/dist/internal/funding/providers/relay.d.ts +43 -1
  208. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  209. package/dist/internal/funding/providers/relay.js +434 -27
  210. package/dist/internal/funding/providers/relay.js.map +1 -1
  211. package/package.json +3 -3
  212. package/src/ApiKey.test.ts +35 -0
  213. package/src/ApiKey.ts +27 -0
  214. package/src/ApiKeys.test.ts +29 -0
  215. package/src/ApiKeys.ts +11 -3
  216. package/src/App.test.ts +23 -5
  217. package/src/App.ts +76 -5
  218. package/src/Client.test-d.ts +171 -19
  219. package/src/admin/apps/api-keys.test.ts +25 -1
  220. package/src/admin/apps/api-keys.ts +6 -2
  221. package/src/admin/ui.generated.ts +67 -67
  222. package/src/analytics/Analytics.ts +4 -0
  223. package/src/apps/composition.test.ts +100 -2
  224. package/src/apps/data/App.test.ts +0 -12
  225. package/src/apps/data/App.ts +4 -8
  226. package/src/apps/data/routes/earn.test.ts +283 -0
  227. package/src/apps/data/routes/earn.ts +800 -3
  228. package/src/apps/data/routes/indexer.ts +2 -1
  229. package/src/apps/data/routes/rpc.ts +2 -0
  230. package/src/apps/data/routes/tokens.test.ts +6 -8
  231. package/src/apps/funding/Access.ts +26 -0
  232. package/src/apps/funding/App.test.ts +78 -0
  233. package/src/apps/funding/App.ts +63 -0
  234. package/src/apps/funding/Inventory.ts +30 -0
  235. package/src/apps/funding/ProviderAttempts.ts +26 -0
  236. package/src/apps/funding/index.ts +1 -0
  237. package/src/apps/funding/routes/chains.test.ts +78 -0
  238. package/src/apps/funding/routes/chains.ts +101 -0
  239. package/src/apps/funding/routes/deposit-addresses.test.ts +1628 -0
  240. package/src/apps/funding/routes/deposit-addresses.ts +1015 -0
  241. package/src/apps/funding/routes/providers.test.ts +165 -0
  242. package/src/apps/funding/routes/providers.ts +73 -0
  243. package/src/apps/funding/routes/quotes.test.ts +511 -0
  244. package/src/apps/funding/routes/quotes.ts +205 -0
  245. package/src/apps/funding/routes/transfers.test.ts +1255 -0
  246. package/src/apps/funding/routes/transfers.ts +704 -0
  247. package/src/apps/index.ts +1 -0
  248. package/src/apps/management/routes/api-keys.test.ts +98 -0
  249. package/src/apps/management/routes/api-keys.ts +96 -1
  250. package/src/apps/mcp.test.ts +17 -2
  251. package/src/apps/mcp.ts +18 -13
  252. package/src/apps/metadata.ts +12 -0
  253. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  254. package/src/cloudflare.ts +279 -0
  255. package/src/db/Db.ts +62 -0
  256. package/src/db/Schema.ts +24 -0
  257. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  258. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  259. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  260. package/src/db/tables/fundingDeposits.test.ts +138 -0
  261. package/src/db/tables/fundingDeposits.ts +145 -1
  262. package/src/internal/Auth.test.ts +177 -5
  263. package/src/internal/Auth.ts +48 -18
  264. package/src/internal/Cache.test.ts +37 -3
  265. package/src/internal/Cache.ts +1 -1
  266. package/src/internal/EdgeCache.test.ts +7 -7
  267. package/src/internal/EdgeCache.ts +13 -4
  268. package/src/internal/IpAllowlist.test.ts +52 -0
  269. package/src/internal/IpAllowlist.ts +39 -0
  270. package/src/internal/Log.test.ts +7 -1
  271. package/src/internal/OpenApi.ts +1 -1
  272. package/src/internal/Tidx.test.ts +28 -0
  273. package/src/internal/Tidx.ts +19 -3
  274. package/src/internal/Viem.test.ts +28 -0
  275. package/src/internal/Viem.ts +21 -5
  276. package/src/internal/funding/Deposit.ts +74 -5
  277. package/src/internal/funding/Evidence.test.ts +2 -2
  278. package/src/internal/funding/Evidence.ts +4 -9
  279. package/src/internal/funding/Provider.ts +115 -0
  280. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  281. package/src/internal/funding/Reconciliation.test.ts +1052 -35
  282. package/src/internal/funding/Reconciliation.ts +328 -56
  283. package/src/internal/funding/SourceObservation.test.ts +301 -0
  284. package/src/internal/funding/SourceObservation.ts +267 -0
  285. package/src/internal/funding/Subsidy.ts +2 -2
  286. package/src/internal/funding/index.test.ts +1 -0
  287. package/src/internal/funding/index.ts +1 -0
  288. package/src/internal/funding/providers/relay.test.ts +94 -4
  289. package/src/internal/funding/providers/relay.ts +528 -27
  290. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  291. package/dist/apps/data/routes/funding.js +0 -1557
  292. package/dist/apps/data/routes/funding.js.map +0 -1
  293. package/src/apps/data/routes/funding.test.ts +0 -2994
  294. 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,7 +14,10 @@ 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>
17
+ paths: Record<
18
+ string,
19
+ Partial<Record<'get' | 'post', { operationId: string; tags?: readonly string[] | undefined }>>
20
+ >
18
21
  tags: readonly { description: string; name: string }[]
19
22
  webhooks?: Record<string, unknown> | undefined
20
23
  'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
@@ -146,10 +149,12 @@ describe('data()', () => {
146
149
  expect(keys.some((path) => path.startsWith('/v1/blocks'))).toBe(true)
147
150
  expect(keys.some((path) => path.startsWith('/v1/orgs'))).toBe(false)
148
151
  expect(keys.some((path) => path.startsWith('/v1/me'))).toBe(false)
152
+ expect(keys.some((path) => path.startsWith('/v1/funding'))).toBe(false)
149
153
 
150
154
  // The omitted management route resolves to a 404 (never mounted), not a 401.
151
155
  const orgs = await app.request('/v1/orgs')
152
156
  expect(orgs.status).toBe(404)
157
+ expect((await app.request('/v1/funding/chains')).status).toBe(404)
153
158
  })
154
159
 
155
160
  test('inherits shared chain context', async () => {
@@ -272,6 +277,99 @@ describe('data()', () => {
272
277
  })
273
278
  })
274
279
 
280
+ describe('funding()', () => {
281
+ test('mounts the Funding & Bridge API independently', async () => {
282
+ const group = funding()
283
+ expectTypeOf(App.funding).toEqualTypeOf<typeof funding>()
284
+ expectTypeOf(group).toEqualTypeOf<App.FundingApp>()
285
+ const app = App.create({ auth: false, db: TestApp.database() }).route('/', group)
286
+ const spec = await document(app)
287
+
288
+ expect(
289
+ Object.keys(spec.paths)
290
+ .filter((path) => path.startsWith('/v1/funding'))
291
+ .sort(),
292
+ ).toEqual(
293
+ [
294
+ '/v1/funding/chains',
295
+ '/v1/funding/deposit-addresses',
296
+ '/v1/funding/deposit-addresses/{id}',
297
+ '/v1/funding/deposit-addresses/{id}/deposits',
298
+ '/v1/funding/deposit-addresses/{id}/reconcile',
299
+ '/v1/funding/deposits',
300
+ '/v1/funding/deposits/{id}',
301
+ '/v1/funding/providers',
302
+ '/v1/funding/quotes',
303
+ '/v1/funding/transfers',
304
+ '/v1/funding/transfers/vault',
305
+ '/v1/funding/transfers/zone',
306
+ '/v1/funding/transfers/{id}',
307
+ ].sort(),
308
+ )
309
+ expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/tokens'))).toBe(false)
310
+ expect(spec.paths['/webhooks/funding/{providerId}']).toBeUndefined()
311
+ expect(spec['x-tagGroups']).toEqual([
312
+ {
313
+ name: 'Funding & Bridge API',
314
+ tags: ['Chains', 'Deposit Addresses', 'Providers', 'Quotes', 'Transfers'],
315
+ },
316
+ ])
317
+ expect(
318
+ Object.values(spec.paths).flatMap((path) =>
319
+ [path.get, path.post].flatMap((operation) =>
320
+ operation?.tags?.includes('Deposit Addresses') ? [operation.operationId] : [],
321
+ ),
322
+ ),
323
+ ).toEqual([
324
+ 'createFundingDepositAddress',
325
+ 'reconcileFundingDepositAddress',
326
+ 'getFundingDepositAddress',
327
+ 'getFundingDeposit',
328
+ 'listFundingDepositAddressDeposits',
329
+ 'listFundingDeposits',
330
+ ])
331
+ expect((await app.request('/v1/tokens')).status).toBe(404)
332
+ })
333
+
334
+ test('maps operations to five tags and reuses Transfers across groups', async () => {
335
+ const app = App.create({ auth: false, db: TestApp.database() })
336
+ .route('/', data())
337
+ .route('/', funding())
338
+ const spec = await document(app)
339
+ const operationTag = (path: string, method: 'get' | 'post') =>
340
+ (spec.paths[path] as Record<string, { tags?: readonly string[] | undefined } | undefined>)[
341
+ method
342
+ ]?.tags
343
+
344
+ expect({
345
+ chains: operationTag('/v1/funding/chains', 'get'),
346
+ depositAddress: operationTag('/v1/funding/deposit-addresses', 'post'),
347
+ deposits: operationTag('/v1/funding/deposit-addresses/{id}/deposits', 'get'),
348
+ providers: operationTag('/v1/funding/providers', 'get'),
349
+ quotes: operationTag('/v1/funding/quotes', 'get'),
350
+ transfers: operationTag('/v1/funding/transfers', 'get'),
351
+ vault: operationTag('/v1/funding/transfers/vault', 'post'),
352
+ zone: operationTag('/v1/funding/transfers/zone', 'post'),
353
+ }).toEqual({
354
+ chains: ['Chains'],
355
+ depositAddress: ['Deposit Addresses'],
356
+ deposits: ['Deposit Addresses'],
357
+ providers: ['Providers'],
358
+ quotes: ['Quotes'],
359
+ transfers: ['Transfers'],
360
+ vault: ['Transfers'],
361
+ zone: ['Transfers'],
362
+ })
363
+
364
+ const groups = Object.fromEntries(
365
+ (spec['x-tagGroups'] ?? []).map((group) => [group.name, group.tags]),
366
+ )
367
+ expect(groups['Data API']).toContain('Transfers')
368
+ expect(groups['Funding & Bridge API']).toContain('Transfers')
369
+ expect(spec.tags.filter((tag) => tag.name === 'Transfers')).toHaveLength(1)
370
+ })
371
+ })
372
+
275
373
  describe('relay()', () => {
276
374
  test('mounts the relay after data() without documenting RPC routes', async () => {
277
375
  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',
@@ -87,6 +87,166 @@ describe('schema.getEarnVaults.Query', () => {
87
87
  })
88
88
  })
89
89
 
90
+ describe('schema.getEarnVaultEarnings.Response', () => {
91
+ test('enforces status-specific earnings fields', () => {
92
+ const response = {
93
+ account: TestEarn.earningsAddress,
94
+ assetToken: TestEarn.assetTokenAddress,
95
+ currentValue: '1000000',
96
+ id: TestEarn.earningsAddress,
97
+ }
98
+
99
+ expect({
100
+ active: Earn.schema.getEarnVaultEarnings.Response.safeParse({
101
+ ...response,
102
+ activeEarnings: '0',
103
+ period: 'active',
104
+ status: 'complete',
105
+ }).success,
106
+ activeWithLifetimeEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
107
+ ...response,
108
+ lifetimeEarnings: '0',
109
+ period: 'active',
110
+ status: 'complete',
111
+ }).success,
112
+ lifetime: Earn.schema.getEarnVaultEarnings.Response.safeParse({
113
+ ...response,
114
+ lifetimeEarnings: '0',
115
+ period: 'lifetime',
116
+ status: 'complete',
117
+ }).success,
118
+ completeWithoutEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
119
+ ...response,
120
+ period: 'lifetime',
121
+ status: 'complete',
122
+ }).success,
123
+ incomplete: Earn.schema.getEarnVaultEarnings.Response.safeParse({
124
+ ...response,
125
+ period: 'active',
126
+ status: 'incomplete_cost_basis',
127
+ }).success,
128
+ incompleteWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
129
+ ...response,
130
+ lifetimeEarnings: '0',
131
+ period: 'lifetime',
132
+ status: 'incomplete_cost_basis',
133
+ }).success,
134
+ pending: Earn.schema.getEarnVaultEarnings.Response.safeParse({
135
+ ...response,
136
+ period: 'lifetime',
137
+ status: 'pending_redemption',
138
+ }).success,
139
+ pendingActive: Earn.schema.getEarnVaultEarnings.Response.safeParse({
140
+ ...response,
141
+ period: 'active',
142
+ status: 'pending_redemption',
143
+ }).success,
144
+ pendingWithEarnings: Earn.schema.getEarnVaultEarnings.Response.safeParse({
145
+ ...response,
146
+ lifetimeEarnings: '0',
147
+ period: 'lifetime',
148
+ status: 'pending_redemption',
149
+ }).success,
150
+ }).toMatchInlineSnapshot(`
151
+ {
152
+ "active": true,
153
+ "activeWithLifetimeEarnings": false,
154
+ "completeWithoutEarnings": false,
155
+ "incomplete": true,
156
+ "incompleteWithEarnings": false,
157
+ "lifetime": true,
158
+ "pending": true,
159
+ "pendingActive": false,
160
+ "pendingWithEarnings": false,
161
+ }
162
+ `)
163
+ })
164
+ })
165
+
166
+ describe('schema.getEarnVaultEarnings.Query', () => {
167
+ test('defaults to lifetime and accepts active earnings', () => {
168
+ expect([
169
+ Earn.schema.getEarnVaultEarnings.Query.parse({}).period,
170
+ Earn.schema.getEarnVaultEarnings.Query.parse({ period: 'active' }).period,
171
+ ]).toStrictEqual(['lifetime', 'active'])
172
+ })
173
+ })
174
+
175
+ describe('calculateActiveCostBasis', () => {
176
+ test('allocates weighted-average basis across partial exits', () => {
177
+ expect(
178
+ Earn.calculateActiveCostBasis({
179
+ actions: [
180
+ { assets: 100n, kind: 'deposit', shares: 100n },
181
+ { assets: 300n, kind: 'deposit', shares: 100n },
182
+ { kind: 'redeem', shares: 100n },
183
+ ],
184
+ shareBalance: 100n,
185
+ }),
186
+ ).toBe(200n)
187
+ })
188
+
189
+ test('calculates remaining basis while an async redemption is pending', () => {
190
+ expect(
191
+ Earn.calculateActiveCostBasis({
192
+ actions: [
193
+ { assets: 100n, kind: 'deposit', shares: 100n },
194
+ { kind: 'request', requestId: 'request-1', shares: 40n },
195
+ ],
196
+ shareBalance: 60n,
197
+ }),
198
+ ).toBe(60n)
199
+ })
200
+
201
+ test('restores removed basis when an async redemption is cancelled', () => {
202
+ expect(
203
+ Earn.calculateActiveCostBasis({
204
+ actions: [
205
+ { assets: 100n, kind: 'deposit', shares: 100n },
206
+ { kind: 'request', requestId: 'request-1', shares: 40n },
207
+ { kind: 'cancel', requestId: 'request-1', shares: 40n },
208
+ ],
209
+ shareBalance: 100n,
210
+ }),
211
+ ).toBe(100n)
212
+ })
213
+
214
+ test('recovers after unknown-basis shares fully exit', () => {
215
+ expect(
216
+ Earn.calculateActiveCostBasis({
217
+ actions: [
218
+ { kind: 'unknown', shares: 100n },
219
+ { kind: 'redeem', shares: 100n },
220
+ { assets: 200n, kind: 'deposit', shares: 100n },
221
+ ],
222
+ shareBalance: 100n,
223
+ }),
224
+ ).toBe(200n)
225
+ })
226
+
227
+ test('rejects remaining shares with unknown cost basis', () => {
228
+ expect(
229
+ Earn.calculateActiveCostBasis({
230
+ actions: [
231
+ { assets: 100n, kind: 'deposit', shares: 100n },
232
+ { kind: 'unknown', shares: 100n },
233
+ { kind: 'redeem', shares: 100n },
234
+ ],
235
+ shareBalance: 100n,
236
+ }),
237
+ ).toBeUndefined()
238
+ })
239
+
240
+ test('rejects history that does not reconcile to the indexed share balance', () => {
241
+ expect(
242
+ Earn.calculateActiveCostBasis({
243
+ actions: [{ assets: 100n, kind: 'deposit', shares: 100n }],
244
+ shareBalance: 99n,
245
+ }),
246
+ ).toBeUndefined()
247
+ })
248
+ })
249
+
90
250
  describe('discoveryBatchLimit', () => {
91
251
  test('fetches one extra candidate without live filters', () => {
92
252
  expect(
@@ -864,6 +1024,112 @@ describe('GET /v1/earn/vaults/:vaultId/positions/:address', () => {
864
1024
  )
865
1025
  })
866
1026
 
1027
+ describe('GET /v1/earn/vaults/:vaultId/earnings/:address', () => {
1028
+ test('returns chain selection errors', async () => {
1029
+ const app = TestApp.create({ auth: false, cache })
1030
+ const invalid = await app.request(
1031
+ `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=abc`,
1032
+ )
1033
+ const unsupported = await app.request(
1034
+ `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=999999`,
1035
+ )
1036
+
1037
+ expect([
1038
+ (await TestApp.json(invalid, Schema.ErrorResponse)).error.code,
1039
+ (await TestApp.json(unsupported, Schema.ErrorResponse)).error.code,
1040
+ ]).toStrictEqual(['chain_id_invalid', 'chain_id_unsupported'])
1041
+ })
1042
+
1043
+ test('rejects malformed vault earnings parameters', async () => {
1044
+ const response = await TestApp.create({ auth: false, cache }).request(
1045
+ '/v1/earn/vaults/not-an-address/earnings/not-an-address',
1046
+ )
1047
+
1048
+ expect(response.status).toBe(400)
1049
+ expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({
1050
+ error: { code: 'earnings_invalid' },
1051
+ })
1052
+ })
1053
+
1054
+ test.runIf(Runtime.get().mode === 'testnet')(
1055
+ 'returns lifetime return through indexed vault events',
1056
+ async () => {
1057
+ const app = TestApp.create({ auth: false, cache })
1058
+ const response = await app.request(
1059
+ `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.earningsAddress}?chainId=${TestEarn.chain.id}`,
1060
+ )
1061
+
1062
+ expect(response.status).toBe(200)
1063
+ const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
1064
+ if (earnings.status !== 'complete') throw new Error('Expected complete earnings history.')
1065
+ expect(earnings).toMatchObject({
1066
+ account: TestEarn.earningsAddress,
1067
+ assetToken: TestEarn.assetTokenAddress,
1068
+ currentValue: expect.stringMatching(/^\d+$/),
1069
+ id: TestEarn.earningsAddress,
1070
+ lifetimeEarnings: expect.stringMatching(/^-?\d+$/),
1071
+ period: 'lifetime',
1072
+ status: 'complete',
1073
+ })
1074
+ },
1075
+ 60_000,
1076
+ )
1077
+
1078
+ test.runIf(Runtime.get().mode === 'testnet')(
1079
+ 'returns earnings on shares still held',
1080
+ async () => {
1081
+ const app = TestApp.create({ auth: false, cache })
1082
+ const response = await app.request(
1083
+ `/v1/earn/vaults/${TestEarn.activeEarningsVaultAddress}/earnings/${TestEarn.activeEarningsAddress}?chainId=${TestEarn.chain.id}&period=active`,
1084
+ )
1085
+
1086
+ expect(response.status).toBe(200)
1087
+ const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
1088
+ if (earnings.status !== 'complete' || earnings.period !== 'active')
1089
+ throw new Error('Expected complete active earnings history.')
1090
+ expect(earnings).toMatchObject({
1091
+ account: TestEarn.activeEarningsAddress,
1092
+ activeEarnings: expect.stringMatching(/^-?\d+$/),
1093
+ period: 'active',
1094
+ status: 'complete',
1095
+ })
1096
+ expect(earnings).not.toHaveProperty('lifetimeEarnings')
1097
+ },
1098
+ 60_000,
1099
+ )
1100
+
1101
+ test.runIf(Runtime.get().mode === 'testnet')(
1102
+ 'omits lifetime earnings when venue shares make the cost basis incomplete',
1103
+ async () => {
1104
+ const app = TestApp.create({ auth: false, cache })
1105
+ const response = await app.request(
1106
+ `/v1/earn/vaults/${TestEarn.vaultAddress}/earnings/${TestEarn.incompleteEarningsAddress}?chainId=${TestEarn.chain.id}`,
1107
+ )
1108
+
1109
+ expect(response.status).toBe(200)
1110
+ const earnings = await TestApp.json(response, Earn.schema.getEarnVaultEarnings.Response)
1111
+ expect(earnings.status).toBe('incomplete_cost_basis')
1112
+ expect(earnings).not.toHaveProperty('lifetimeEarnings')
1113
+ },
1114
+ 60_000,
1115
+ )
1116
+
1117
+ test.runIf(Runtime.get().mode === 'testnet')(
1118
+ 'returns not found for an address without an Earn vault',
1119
+ async () => {
1120
+ const response = await TestApp.create({ auth: false, cache }).request(
1121
+ `/v1/earn/vaults/0x1111111111111111111111111111111111111111/earnings/${TestEarn.earningsAddress}?chainId=${TestEarn.chain.id}`,
1122
+ )
1123
+
1124
+ expect(response.status).toBe(404)
1125
+ expect(await TestApp.json(response, Schema.ErrorResponse)).toMatchObject({
1126
+ error: { code: 'earn_vault_not_found' },
1127
+ })
1128
+ },
1129
+ 60_000,
1130
+ )
1131
+ })
1132
+
867
1133
  describe('GET /v1/earn/vaults', () => {
868
1134
  test.runIf(Runtime.get().mode === 'testnet')(
869
1135
  'extracts an indexed deployment and applies onchain filters',
@@ -1039,6 +1305,7 @@ describe('OpenAPI', () => {
1039
1305
  '/v1/earn/vaults',
1040
1306
  '/v1/earn/vaults/verified',
1041
1307
  '/v1/earn/vaults/{vaultId}',
1308
+ '/v1/earn/vaults/{vaultId}/earnings/{address}',
1042
1309
  '/v1/earn/vaults/{vaultId}/positions/{address}',
1043
1310
  ]),
1044
1311
  )
@@ -1105,4 +1372,20 @@ describe('OpenAPI', () => {
1105
1372
  expect(address?.schema?.examples).toStrictEqual([TestEarn.positionAddress])
1106
1373
  expect(vaultId?.schema?.examples).toStrictEqual([TestEarn.mainnetVaultAddress])
1107
1374
  })
1375
+
1376
+ test('documents vault-earnings variants', async () => {
1377
+ const app = TestApp.create({ auth: false, cache })
1378
+ const spec = (await (await app.request('/openapi.json')).json()) as OpenApiDocument
1379
+ const operation = spec.paths['/v1/earn/vaults/{vaultId}/earnings/{address}']?.get
1380
+ const response = JSON.stringify(operation?.responses)
1381
+
1382
+ expect(response).toContain('"earn_vault_not_found"')
1383
+ expect(response).toContain('Stable resource ID for these vault earnings')
1384
+ expect(response).toContain('Current share value plus completed redemptions')
1385
+ expect(response).toContain('weighted-average asset cost basis')
1386
+ expect(response).toContain('cannot reconstruct the remaining asset cost basis')
1387
+ expect(response).toContain('"active"')
1388
+ expect(response).toContain('"incomplete_cost_basis"')
1389
+ expect(response).toContain('"pending_redemption"')
1390
+ })
1108
1391
  })