tapimo 0.6.0 → 0.7.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 (250) hide show
  1. package/dist/App.d.ts +8641 -8263
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +9 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +4 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +5 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +112 -11
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +2 -1
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/early-access.d.ts +2 -2
  14. package/dist/admin/apps/early-access.d.ts.map +1 -1
  15. package/dist/admin/apps/early-access.js +5 -5
  16. package/dist/admin/apps/early-access.js.map +1 -1
  17. package/dist/admin/apps/organizations.d.ts +124 -1
  18. package/dist/admin/apps/organizations.d.ts.map +1 -1
  19. package/dist/admin/apps/organizations.js +97 -6
  20. package/dist/admin/apps/organizations.js.map +1 -1
  21. package/dist/admin/ui.generated.d.ts +1 -1
  22. package/dist/admin/ui.generated.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.js +95 -87
  24. package/dist/admin/ui.generated.js.map +1 -1
  25. package/dist/analytics/Analytics.d.ts.map +1 -1
  26. package/dist/analytics/Analytics.js +20 -0
  27. package/dist/analytics/Analytics.js.map +1 -1
  28. package/dist/analytics/tables/requestEvents.d.ts +18 -0
  29. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  30. package/dist/analytics/tables/requestEvents.js +36 -7
  31. package/dist/analytics/tables/requestEvents.js.map +1 -1
  32. package/dist/apps/data/App.d.ts +5016 -4985
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +2 -15
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +1 -1
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +1 -1
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +27 -27
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +21 -1
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +58 -58
  45. package/dist/apps/data/routes/blocks.d.ts +39 -39
  46. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  47. package/dist/apps/data/routes/exchanges.d.ts +113 -113
  48. package/dist/apps/data/routes/fee-amm.d.ts +105 -91
  49. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  50. package/dist/apps/data/routes/fee-amm.js +76 -91
  51. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  52. package/dist/apps/data/routes/funding.d.ts +72 -34
  53. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  54. package/dist/apps/data/routes/funding.js +34 -26
  55. package/dist/apps/data/routes/funding.js.map +1 -1
  56. package/dist/apps/data/routes/indexer.d.ts +5 -5
  57. package/dist/apps/data/routes/receipts.d.ts +6 -6
  58. package/dist/apps/data/routes/rpc.d.ts +31 -31
  59. package/dist/apps/data/routes/tokens.d.ts +2 -2
  60. package/dist/apps/data/routes/tokens.js +1 -1
  61. package/dist/apps/data/routes/transactions.d.ts +4 -4
  62. package/dist/apps/data/routes/transfers.d.ts +99 -88
  63. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  64. package/dist/apps/data/routes/transfers.js +226 -141
  65. package/dist/apps/data/routes/transfers.js.map +1 -1
  66. package/dist/apps/data/routes/valuation.d.ts +34 -34
  67. package/dist/apps/data/routes/valuation.js +3 -3
  68. package/dist/apps/data/routes/valuation.js.map +1 -1
  69. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  70. package/dist/apps/data/routes/webhooks.d.ts +366 -366
  71. package/dist/apps/index.d.ts +1 -0
  72. package/dist/apps/index.d.ts.map +1 -1
  73. package/dist/apps/index.js +1 -0
  74. package/dist/apps/index.js.map +1 -1
  75. package/dist/apps/management/App.d.ts +460 -523
  76. package/dist/apps/management/App.d.ts.map +1 -1
  77. package/dist/apps/management/routes/api-keys.d.ts +7 -23
  78. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  79. package/dist/apps/management/routes/api-keys.js +20 -8
  80. package/dist/apps/management/routes/api-keys.js.map +1 -1
  81. package/dist/apps/management/routes/billing.d.ts +106 -151
  82. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  83. package/dist/apps/management/routes/billing.js +28 -28
  84. package/dist/apps/management/routes/billing.js.map +1 -1
  85. package/dist/apps/management/routes/faucet.d.ts +40 -26
  86. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  87. package/dist/apps/management/routes/faucet.js +33 -18
  88. package/dist/apps/management/routes/faucet.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +131 -131
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +0 -3
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +6 -8
  94. package/dist/apps/management/routes/me.d.ts.map +1 -1
  95. package/dist/apps/management/routes/me.js +0 -4
  96. package/dist/apps/management/routes/me.js.map +1 -1
  97. package/dist/apps/management/routes/members.d.ts +45 -45
  98. package/dist/apps/management/routes/orgs.d.ts +94 -94
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +5 -0
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/usage.d.ts +32 -32
  103. package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
  104. package/dist/apps/management/routes/webhooks.d.ts +1 -1
  105. package/dist/apps/mpp/App.d.ts +363 -0
  106. package/dist/apps/mpp/App.d.ts.map +1 -0
  107. package/dist/apps/mpp/App.js +72 -0
  108. package/dist/apps/mpp/App.js.map +1 -0
  109. package/dist/apps/mpp/Mpp.d.ts +178 -0
  110. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  111. package/dist/apps/mpp/Mpp.js +183 -0
  112. package/dist/apps/mpp/Mpp.js.map +1 -0
  113. package/dist/apps/mpp/index.d.ts +3 -0
  114. package/dist/apps/mpp/index.d.ts.map +1 -0
  115. package/dist/apps/mpp/index.js +3 -0
  116. package/dist/apps/mpp/index.js.map +1 -0
  117. package/dist/apps/relay/App.d.ts.map +1 -1
  118. package/dist/apps/relay/App.js +56 -9
  119. package/dist/apps/relay/App.js.map +1 -1
  120. package/dist/db/Db.d.ts +2 -0
  121. package/dist/db/Db.d.ts.map +1 -1
  122. package/dist/db/Db.js +27 -0
  123. package/dist/db/Db.js.map +1 -1
  124. package/dist/db/Schema.d.ts +12 -0
  125. package/dist/db/Schema.d.ts.map +1 -1
  126. package/dist/db/Schema.js +11 -0
  127. package/dist/db/Schema.js.map +1 -1
  128. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  129. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  130. package/dist/db/tables/enabledBillingSources.js +69 -0
  131. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  132. package/dist/db/tables/invitations.d.ts +1 -12
  133. package/dist/db/tables/invitations.d.ts.map +1 -1
  134. package/dist/db/tables/invitations.js +3 -23
  135. package/dist/db/tables/invitations.js.map +1 -1
  136. package/dist/db/tables/organizations.d.ts +5 -2
  137. package/dist/db/tables/organizations.d.ts.map +1 -1
  138. package/dist/db/tables/organizations.js +13 -2
  139. package/dist/db/tables/organizations.js.map +1 -1
  140. package/dist/handlers/internal/utils.d.ts +1 -16
  141. package/dist/handlers/internal/utils.d.ts.map +1 -1
  142. package/dist/handlers/internal/utils.js +17 -6
  143. package/dist/handlers/internal/utils.js.map +1 -1
  144. package/dist/handlers/relay.d.ts.map +1 -1
  145. package/dist/handlers/relay.js +37 -5
  146. package/dist/handlers/relay.js.map +1 -1
  147. package/dist/internal/Auth.d.ts +0 -32
  148. package/dist/internal/Auth.d.ts.map +1 -1
  149. package/dist/internal/Auth.js +3 -51
  150. package/dist/internal/Auth.js.map +1 -1
  151. package/dist/internal/FundingQuotes.d.ts +78 -32
  152. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  153. package/dist/internal/FundingQuotes.js +45 -8
  154. package/dist/internal/FundingQuotes.js.map +1 -1
  155. package/dist/internal/Log.d.ts +18 -4
  156. package/dist/internal/Log.d.ts.map +1 -1
  157. package/dist/internal/Log.js +50 -3
  158. package/dist/internal/Log.js.map +1 -1
  159. package/dist/internal/MetricSink.d.ts.map +1 -1
  160. package/dist/internal/MetricSink.js +15 -0
  161. package/dist/internal/MetricSink.js.map +1 -1
  162. package/dist/internal/Response.d.ts +2 -2
  163. package/dist/internal/Response.d.ts.map +1 -1
  164. package/dist/internal/Response.js +12 -4
  165. package/dist/internal/Response.js.map +1 -1
  166. package/dist/internal/Viem.d.ts +1 -1
  167. package/package.json +7 -2
  168. package/src/App.test.ts +9 -4
  169. package/src/App.ts +14 -2
  170. package/src/Client.test-d.ts +109 -28
  171. package/src/Scope.ts +5 -0
  172. package/src/admin/App.test.ts +4 -0
  173. package/src/admin/apps/early-access.ts +6 -5
  174. package/src/admin/apps/organizations.test.ts +86 -2
  175. package/src/admin/apps/organizations.ts +155 -31
  176. package/src/admin/apps/scopes.test.ts +5 -0
  177. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  178. package/src/admin/ui/src/lib/organizations.ts +92 -0
  179. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  180. package/src/admin/ui/src/routes/early-access.tsx +8 -3
  181. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  182. package/src/admin/ui.generated.ts +95 -109
  183. package/src/analytics/Analytics.test.ts +83 -0
  184. package/src/analytics/Analytics.ts +20 -0
  185. package/src/analytics/tables/requestEvents.ts +60 -7
  186. package/src/apps/composition.test.ts +11 -2
  187. package/src/apps/data/App.test.ts +1 -1
  188. package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
  189. package/src/apps/data/FundingQuoteProviders.ts +2 -29
  190. package/src/apps/data/FxOracle.test.ts +2 -2
  191. package/src/apps/data/FxOracle.ts +2 -2
  192. package/src/apps/data/routes/activities.test.ts +6 -2
  193. package/src/apps/data/routes/activities.ts +39 -1
  194. package/src/apps/data/routes/balances.test.ts +6 -2
  195. package/src/apps/data/routes/exchanges.test.ts +7 -3
  196. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  197. package/src/apps/data/routes/fee-amm.ts +98 -119
  198. package/src/apps/data/routes/funding.test.ts +29 -8
  199. package/src/apps/data/routes/funding.ts +42 -48
  200. package/src/apps/data/routes/receipts.test.ts +6 -2
  201. package/src/apps/data/routes/tokens.ts +1 -1
  202. package/src/apps/data/routes/transfers.test.ts +203 -35
  203. package/src/apps/data/routes/transfers.ts +294 -172
  204. package/src/apps/data/routes/valuation.test.ts +18 -3
  205. package/src/apps/data/routes/valuation.ts +4 -4
  206. package/src/apps/index.ts +1 -0
  207. package/src/apps/management/routes/api-keys.test.ts +2 -75
  208. package/src/apps/management/routes/api-keys.ts +30 -7
  209. package/src/apps/management/routes/billing.test.ts +106 -85
  210. package/src/apps/management/routes/billing.ts +32 -21
  211. package/src/apps/management/routes/faucet.test.ts +11 -3
  212. package/src/apps/management/routes/faucet.ts +46 -30
  213. package/src/apps/management/routes/invitations.test.ts +0 -172
  214. package/src/apps/management/routes/invitations.ts +0 -3
  215. package/src/apps/management/routes/me.test.ts +3 -145
  216. package/src/apps/management/routes/me.ts +0 -6
  217. package/src/apps/management/routes/orgs.test.ts +45 -2
  218. package/src/apps/management/routes/orgs.ts +6 -0
  219. package/src/apps/management/routes/scopes.test.ts +1 -0
  220. package/src/apps/mpp/App.ts +86 -0
  221. package/src/apps/mpp/Mpp.test.ts +193 -0
  222. package/src/apps/mpp/Mpp.ts +353 -0
  223. package/src/apps/mpp/index.ts +2 -0
  224. package/src/apps/mpp/internal/Adapter.ts +11 -0
  225. package/src/apps/mpp/internal/Error.ts +9 -0
  226. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  227. package/src/apps/mpp/internal/Screening.ts +10 -0
  228. package/src/apps/relay/App.test.ts +88 -6
  229. package/src/apps/relay/App.ts +61 -9
  230. package/src/db/Db.test.ts +2 -2
  231. package/src/db/Db.ts +30 -0
  232. package/src/db/Schema.ts +15 -0
  233. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  234. package/src/db/tables/enabledBillingSources.ts +118 -0
  235. package/src/db/tables/invitations.ts +4 -26
  236. package/src/db/tables/organizations.test.ts +103 -0
  237. package/src/db/tables/organizations.ts +18 -2
  238. package/src/handlers/internal/utils.ts +22 -6
  239. package/src/handlers/relay.errors.test.ts +153 -0
  240. package/src/handlers/relay.ts +42 -7
  241. package/src/internal/Auth.test.ts +3 -3
  242. package/src/internal/Auth.ts +3 -63
  243. package/src/internal/FundingQuotes.test.ts +18 -5
  244. package/src/internal/FundingQuotes.ts +46 -11
  245. package/src/internal/Log.test.ts +91 -0
  246. package/src/internal/Log.ts +69 -5
  247. package/src/internal/MetricSink.test.ts +55 -0
  248. package/src/internal/MetricSink.ts +14 -0
  249. package/src/internal/Response.test.ts +28 -0
  250. package/src/internal/Response.ts +12 -4
@@ -28,10 +28,16 @@ describe.skip('GET /fee-amm/pools', () => {
28
28
  expect('nextCursor' in body).toBe(true)
29
29
  expect('meta' in body).toBe(false)
30
30
 
31
- // Stable item shape (sorted keys of the first pool).
32
- expect(Object.keys(body.data[0]!).sort()).toMatchInlineSnapshot(`
31
+ // Stable required shape; reserve amounts remain best-effort.
32
+ const {
33
+ userAmount: _userAmount,
34
+ validatorAmount: _validatorAmount,
35
+ ...poolShape
36
+ } = body.data[0]!
37
+ expect(Object.keys(poolShape).sort()).toMatchInlineSnapshot(`
33
38
  [
34
39
  "createdAt",
40
+ "id",
35
41
  "lastMintAt",
36
42
  "mintCount",
37
43
  "poolId",
@@ -40,13 +46,16 @@ describe.skip('GET /fee-amm/pools', () => {
40
46
  ]
41
47
  `)
42
48
 
43
- // Every pool: valid ISO timestamps with first mint at or before the most
44
- // recent one, and each side's reserve riding on its token reference —
45
- // either absent (failed read) or a base-unit decimal `amount` with a
46
- // `formatted` decimal form.
47
- const validReserve = (token: { amount?: string | undefined; formatted?: string | undefined }) =>
48
- (token.amount === undefined && token.formatted === undefined) ||
49
- (/^\d+$/.test(token.amount ?? '') && /^\d+(\.\d+)?$/.test(token.formatted ?? ''))
49
+ const validAmount = (
50
+ amount:
51
+ | { baseUnits: string; currency: string; decimals: number; formatted: string }
52
+ | undefined,
53
+ ) =>
54
+ amount === undefined ||
55
+ (/^\d+$/.test(amount.baseUnits) &&
56
+ amount.currency.length > 0 &&
57
+ Number.isInteger(amount.decimals) &&
58
+ /^\d+(\.\d+)?$/.test(amount.formatted))
50
59
  expect(
51
60
  body.data.every(
52
61
  (pool) =>
@@ -54,8 +63,12 @@ describe.skip('GET /fee-amm/pools', () => {
54
63
  !Number.isNaN(Date.parse(pool.lastMintAt)) &&
55
64
  Date.parse(pool.createdAt) <= Date.parse(pool.lastMintAt) &&
56
65
  pool.mintCount > 0 &&
57
- validReserve(pool.userToken) &&
58
- validReserve(pool.validatorToken),
66
+ pool.userToken.currency.length > 0 &&
67
+ pool.userToken.symbol.length > 0 &&
68
+ pool.validatorToken.currency.length > 0 &&
69
+ pool.validatorToken.symbol.length > 0 &&
70
+ validAmount(pool.userAmount) &&
71
+ validAmount(pool.validatorAmount),
59
72
  ),
60
73
  ).toBe(true)
61
74
  // Presentation order: most active first (by mint count).
@@ -130,7 +143,7 @@ describe.skip('GET /fee-amm/pools', () => {
130
143
  expect(pageTwo.data[0]!.mintCount).toBeLessThanOrEqual(pageOne.data.at(-1)!.mintCount)
131
144
  })
132
145
 
133
- test('omits token metadata by default and embeds it via include=token', async () => {
146
+ test('returns core token metadata and opts into curated fields', async () => {
134
147
  const client = TestApp.client()
135
148
  const baseResponse = await client.v1['fee-amm'].pools.$get(
136
149
  { query: { limit: '5' } },
@@ -140,31 +153,33 @@ describe.skip('GET /fee-amm/pools', () => {
140
153
 
141
154
  expect(baseResponse.status).toBe(200)
142
155
  expect(base.data.length).toBeGreaterThan(0)
143
- // The base page resolves no token metadata: each side carries just its
144
- // `address` plus its (best-effort) reserve, never `symbol`/`decimals`/etc.
145
156
  expect(
146
157
  base.data.every(
147
158
  (pool) =>
148
- pool.userToken.symbol === undefined &&
149
- pool.userToken.decimals === undefined &&
150
- pool.validatorToken.symbol === undefined &&
151
- pool.validatorToken.decimals === undefined,
159
+ pool.userToken.symbol.length > 0 &&
160
+ Number.isInteger(pool.userToken.decimals) &&
161
+ pool.userToken.logoUri === undefined &&
162
+ pool.userToken.verified === undefined &&
163
+ pool.validatorToken.symbol.length > 0 &&
164
+ Number.isInteger(pool.validatorToken.decimals) &&
165
+ pool.validatorToken.logoUri === undefined &&
166
+ pool.validatorToken.verified === undefined,
152
167
  ),
153
168
  ).toBe(true)
154
169
 
155
170
  const includeResponse = await client.v1['fee-amm'].pools.$get(
156
- { query: { include: 'token', limit: '5' } },
171
+ { query: { include: 'token.logoUri,token.verified', limit: '5' } },
157
172
  TestApp.auth,
158
173
  )
159
174
  const included = await TestApp.json(includeResponse, FeeAmm.schema.getFeeAmmPools.Response)
160
175
 
161
176
  expect(includeResponse.status).toBe(200)
162
- // The same pages line up, and `include=token` resolves metadata for at
163
- // least one side (verified tokens always carry `symbol`/`decimals`).
164
177
  expect(included.data.map((pool) => pool.poolId)).toEqual(base.data.map((pool) => pool.poolId))
165
178
  expect(
166
- included.data.some(
167
- (pool) => pool.userToken.symbol !== undefined || pool.validatorToken.symbol !== undefined,
179
+ included.data.every(
180
+ (pool) =>
181
+ typeof pool.userToken.verified === 'boolean' &&
182
+ typeof pool.validatorToken.verified === 'boolean',
168
183
  ),
169
184
  ).toBe(true)
170
185
  })
@@ -187,6 +202,15 @@ describe.skip('GET /fee-amm/pools', () => {
187
202
  })
188
203
  })
189
204
 
205
+ describe('GET /fee-amm/pools query', () => {
206
+ test('rejects the legacy token include', async () => {
207
+ const response = await TestApp.create().request('/v1/fee-amm/pools?include=token', {
208
+ headers: TestApp.auth.headers,
209
+ })
210
+ expect(response.status).toBe(400)
211
+ })
212
+ })
213
+
190
214
  describe('GET /fee-amm/mints', () => {
191
215
  test('returns a decoded head page and walks the cursor to a disjoint page', async () => {
192
216
  const client = TestApp.client()
@@ -1,5 +1,5 @@
1
1
  import { Hono, type Context } from 'hono'
2
- import { AbiEvent, AbiParameters, Hash, type Hex, Value as core_Value } from 'ox'
2
+ import { AbiEvent, AbiParameters, Hash, type Hex } from 'ox'
3
3
  import { Abis, Addresses } from 'viem/tempo'
4
4
  import * as z from 'zod/mini'
5
5
 
@@ -48,11 +48,8 @@ const legacyMintTopic = AbiEvent.getSelector(
48
48
  export namespace schema {
49
49
  /**
50
50
  * A token referenced by a fee pool: its contract address plus best-effort
51
- * metadata. Mirrors the transfer feed's `sourceToken` technique: `address`
52
- * is always present; the metadata fields spread in only when resolution
53
- * succeeds, so a token whose metadata is unavailable keeps just the bare
54
- * reference. The pools listing extends this shape with the side's reserve
55
- * (`getFeeAmmPools.Token`).
51
+ * metadata. Fee AMM mints use this shape; pools require core metadata through
52
+ * `getFeeAmmPools.Token`.
56
53
  */
57
54
  export const PoolToken = Schema.describe(
58
55
  z.object({
@@ -94,21 +91,18 @@ export namespace schema {
94
91
 
95
92
  /** Schemas for the getFeeAmmPools operation. */
96
93
  export namespace getFeeAmmPools {
97
- /** Optional related resources that callers opt into via `include`. */
94
+ /** Optional curated token fields that callers opt into via `include`. */
98
95
  export const Include = z
99
- .enum(['token'])
100
- .check(z.describe('Optional related data you can request with `include`.'))
96
+ .enum(['token.logoUri', 'token.verified'])
97
+ .check(z.describe('Additional token fields to include in the pool response.'))
101
98
 
102
99
  /**
103
- * Parses a comma-separated `include` query value into a list of optional
104
- * resources to embed. Token metadata resolution is one extra (batched)
105
- * `token.getMetadata` round trip, so it runs only when `include=token` is
106
- * requested; the base page returns each side's bare `address` plus its
107
- * reserve and skips the lookup entirely.
100
+ * Parses comma-separated curated token fields. Core token metadata is
101
+ * always present; callers opt into logo and verification fields.
108
102
  */
109
103
  export const includeQuery = Schema.includeQuery(
110
104
  Include,
111
- 'Comma-separated related resources to include, such as `token` metadata.',
105
+ 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.',
112
106
  )
113
107
 
114
108
  /** Query parameters for the fee pool list request. */
@@ -122,31 +116,10 @@ export namespace schema {
122
116
  })
123
117
  .check(...Schema.pageChecks(), z.describe('Query parameters for listing Fee AMM pools.'))
124
118
 
125
- /**
126
- * One side of a fee pool: the token reference carrying the side's current
127
- * on-chain reserve. `amount`/`formatted` come from `FeeManager.getPool`
128
- * and are absent when the read fails (best-effort, like the metadata
129
- * fields).
130
- */
119
+ /** A fee-pool token with required RPC metadata and optional curated fields. */
131
120
  export const Token = Schema.describe(
132
- z.extend(PoolToken, {
133
- amount: z
134
- .optional(Schema.DecimalString)
135
- .check(
136
- z.describe(
137
- 'Current onchain reserve for this token, in base units, from `FeeManager.getPool`. Omitted if the read fails.',
138
- ),
139
- ),
140
- formatted: z
141
- .optional(z.string())
142
- .check(
143
- z.describe(
144
- 'Reserve as a human-readable decimal string using the token’s `decimals`. Omitted if the read fails.',
145
- ),
146
- z.meta({ examples: ['1.5'] }),
147
- ),
148
- }),
149
- 'One token side of a Fee AMM pool, including its current onchain reserve when available.',
121
+ Tokens.schema.TokenReference,
122
+ 'A token referenced by one side of a Fee AMM pool.',
150
123
  )
151
124
 
152
125
  /** A fee AMM pool, aggregated from its `Mint` events. */
@@ -180,16 +153,16 @@ export namespace schema {
180
153
  'Pool id computed as `keccak256(abi.encode(userToken, validatorToken))`, matching `FeeManager.getPoolId`.',
181
154
  ),
182
155
  ),
183
- userToken: Token.check(
184
- z.describe(
185
- 'Token users pay fees with in this pool, including its current reserve when available.',
186
- ),
187
- ),
188
- validatorToken: Token.check(
189
- z.describe(
190
- 'Token validators receive from this pool, including its current reserve when available.',
156
+ userAmount: z
157
+ .optional(Schema.TokenAmount)
158
+ .check(z.describe('Current user-token reserve. Omitted when the onchain read fails.')),
159
+ userToken: Token.check(z.describe('Token users pay fees with in this pool.')),
160
+ validatorAmount: z
161
+ .optional(Schema.TokenAmount)
162
+ .check(
163
+ z.describe('Current validator-token reserve. Omitted when the onchain read fails.'),
191
164
  ),
192
- ),
165
+ validatorToken: Token.check(z.describe('Token validators receive from this pool.')),
193
166
  })
194
167
  .check(z.describe('A Fee AMM pool discovered from its onchain `Mint` events.'))
195
168
 
@@ -319,7 +292,7 @@ export function feeAmm() {
319
292
  'Lists Fee AMM pools, most active first, so you can see which stablecoin fee conversions have the most liquidity activity.',
320
293
  operationId: 'getFeeAmmPools',
321
294
  responses: OpenApi.responses({
322
- errors: { 502: 'The upstream Tempo indexer could not complete the request.' },
295
+ errors: { 502: 'Tempo RPC or the upstream indexer could not complete the request.' },
323
296
  success: {
324
297
  description: 'List of Fee AMM pools.',
325
298
  schema: schema.getFeeAmmPools.Response,
@@ -330,7 +303,7 @@ export function feeAmm() {
330
303
  }),
331
304
  Cache.response<App.Environment>({
332
305
  cacheControl: Cache.policies.state,
333
- name: 'tempo-api:fee-amm:v1',
306
+ name: 'tempo-api:fee-amm-pools:v2',
334
307
  key: (c) => Cache.urlKey(c, schema.getFeeAmmPools.Query),
335
308
  }),
336
309
  async (c) => {
@@ -353,57 +326,63 @@ export function feeAmm() {
353
326
  })
354
327
  const pools = page.data
355
328
 
356
- // Reserves are one `getPool` eth_call per pool; viem auto-batches
357
- // the concurrent calls into a single deployless multicall round
358
- // trip (see `internal/Viem.ts`).
359
- const reserves = await Timing.time(c, 'fee_amm_reserves', () =>
360
- Promise.all(
361
- pools.map((pool) =>
362
- getPoolReserves(c, {
363
- chainId,
364
- userToken: pool.userToken,
365
- validatorToken: pool.validatorToken,
366
- }),
329
+ // Reserves and token metadata are independent batched RPC reads.
330
+ const [reserves, tokensByAddress] = await Promise.all([
331
+ Timing.time(c, 'fee_amm_reserves', () =>
332
+ Promise.all(
333
+ pools.map((pool) =>
334
+ getPoolReserves(c, {
335
+ chainId,
336
+ userToken: pool.userToken,
337
+ validatorToken: pool.validatorToken,
338
+ }),
339
+ ),
367
340
  ),
368
341
  ),
369
- )
342
+ resolveTokenMap(c, {
343
+ addresses: Array.from(
344
+ new Set(pools.flatMap((pool) => [pool.userToken, pool.validatorToken])),
345
+ ),
346
+ chainId,
347
+ include: query.include,
348
+ }),
349
+ ])
370
350
 
371
- // Token metadata is one extra (batched) `token.getMetadata` round
372
- // trip, opt-in via `include=token`. Without it each side carries
373
- // just its `address` and reserve; reserves still format via the
374
- // `reserve` helper's TIP-20 (6-decimal) fallback.
375
- const tokensByAddress = query.include.includes('token')
376
- ? await resolveTokenMap(c, {
377
- addresses: Array.from(
378
- new Set(pools.flatMap((pool) => [pool.userToken, pool.validatorToken])),
379
- ),
380
- chainId,
381
- })
382
- : undefined
351
+ const token = (address: z.output<typeof Schema.Address>) => {
352
+ const metadata = tokensByAddress.get(address)
353
+ if (!metadata) throw new Error(`Token metadata unavailable for ${address}`)
354
+ return {
355
+ address,
356
+ currency: metadata.currency,
357
+ decimals: metadata.decimals,
358
+ ...(query.include.includes('token.logoUri') && metadata.logoUri !== undefined
359
+ ? { logoUri: metadata.logoUri }
360
+ : {}),
361
+ name: metadata.name,
362
+ symbol: metadata.symbol,
363
+ ...(query.include.includes('token.verified') && metadata.verified !== undefined
364
+ ? { verified: metadata.verified }
365
+ : {}),
366
+ }
367
+ }
383
368
 
384
369
  const data = pools.map((pool, index) => {
385
370
  const id = poolId(pool.userToken, pool.validatorToken)
386
- const userToken = { address: pool.userToken, ...tokensByAddress?.get(pool.userToken) }
387
- const validatorToken = {
388
- address: pool.validatorToken,
389
- ...tokensByAddress?.get(pool.validatorToken),
390
- }
391
- // Each side's reserve rides on its token reference (like the
392
- // metadata fields): absent when the `getPool` read failed.
371
+ const userToken = token(pool.userToken)
372
+ const validatorToken = token(pool.validatorToken)
393
373
  return {
394
374
  createdAt: pool.createdAt,
395
375
  id,
396
376
  lastMintAt: pool.lastMintAt,
397
377
  mintCount: pool.mintCount,
398
378
  poolId: id,
399
- userToken: {
400
- ...userToken,
401
- ...reserve(reserves[index]?.userToken, userToken.decimals),
402
- },
403
- validatorToken: {
404
- ...validatorToken,
405
- ...reserve(reserves[index]?.validatorToken, validatorToken.decimals),
406
- },
379
+ userAmount: reserve({ amount: reserves[index]?.userToken, token: userToken }),
380
+ userToken,
381
+ validatorAmount: reserve({
382
+ amount: reserves[index]?.validatorToken,
383
+ token: validatorToken,
384
+ }),
385
+ validatorToken,
407
386
  }
408
387
  })
409
388
 
@@ -1030,38 +1009,31 @@ declare namespace decodeMint {
1030
1009
  }
1031
1010
 
1032
1011
  /**
1033
- * Resolves best-effort metadata for the given token addresses, sharing one
1034
- * verified-token snapshot across the lookups. Mirrors the transfer feed's
1035
- * `sourceToken` enrichment: a token whose metadata is unavailable maps to
1036
- * undefined, so call sites spreading the result keep just the bare `address`
1037
- * (the `PoolToken` shape).
1038
- *
1039
- * Unlike the full token resource, this resolves only the on-chain metadata
1040
- * (one `token.getMetadata` read per token, auto-batched into a single
1041
- * multicall) plus the in-memory verified snapshot. It deliberately skips the
1042
- * curated R2 logo lookup (`getTokenLogo`), which is an un-batched storage read
1043
- * per token and the dominant cost of the pools/mints listings; `logoUri`
1044
- * therefore falls back to the verified entry's curated URL or the precompile's
1045
- * on-chain `logoURI`, both already in hand.
1012
+ * Resolves RPC metadata and selected curated fields. Logo enrichment uses cached verification data or onchain metadata, avoiding one R2 read per token.
1046
1013
  */
1047
1014
  async function resolveTokenMap(c: Context<App.Environment>, options: resolveTokenMap.Options) {
1048
1015
  const { addresses, chainId } = options
1049
- const snapshot = await VerifiedTokens.snapshot(c, chainId)
1016
+ const includeLogoUri = options.include?.includes('token.logoUri') ?? true
1017
+ const includeVerified = options.include?.includes('token.verified') ?? true
1018
+ const snapshot =
1019
+ includeLogoUri || includeVerified
1020
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
1021
+ : undefined
1050
1022
  const resolved = await Timing.time(c, 'tokens', () =>
1051
1023
  Promise.all(
1052
1024
  addresses.map((token) =>
1053
1025
  Tokens.getTokenMetadata(c, { address: token, chainId })
1054
1026
  .then((metadata) => {
1055
- const verified = snapshot.byAddress.get(token)
1027
+ const verified = snapshot?.byAddress.get(token)
1056
1028
  return [
1057
1029
  token,
1058
1030
  {
1059
1031
  currency: metadata.currency,
1060
1032
  decimals: metadata.decimals,
1061
- logoUri: verified?.logoUri ?? metadata.logoUri,
1033
+ logoUri: includeLogoUri ? (verified?.logoUri ?? metadata.logoUri) : undefined,
1062
1034
  name: metadata.name,
1063
1035
  symbol: metadata.symbol,
1064
- verified: snapshot.byAddress.has(token),
1036
+ verified: includeVerified ? snapshot?.byAddress.has(token) : undefined,
1065
1037
  },
1066
1038
  ] as const
1067
1039
  })
@@ -1079,6 +1051,8 @@ declare namespace resolveTokenMap {
1079
1051
  addresses: readonly z.output<typeof Schema.Address>[]
1080
1052
  /** Tempo chain id. */
1081
1053
  chainId: z.output<typeof Schema.ChainId>
1054
+ /** Curated token fields to return; omitted to preserve mint enrichment. */
1055
+ include?: readonly z.output<typeof schema.getFeeAmmPools.Include>[] | undefined
1082
1056
  }
1083
1057
  }
1084
1058
 
@@ -1095,19 +1069,24 @@ function poolId(
1095
1069
  )
1096
1070
  }
1097
1071
 
1098
- /**
1099
- * Shapes one side's on-chain reserve for spreading into its token reference:
1100
- * the base-unit `amount` plus its `formatted` decimal form. `decimals` comes
1101
- * from the side's resolved token metadata; an unresolvable token falls back
1102
- * to 6, the fixed TIP-20 precision, so the reserve still formats. A missing
1103
- * amount (failed `getPool` read) yields undefined, leaving both fields absent.
1104
- */
1105
- function reserve(
1106
- amount: string | undefined,
1107
- decimals: number | undefined,
1108
- ): { amount: string; formatted: string } | undefined {
1109
- if (amount === undefined) return undefined
1110
- return { amount, formatted: core_Value.format(BigInt(amount), decimals ?? 6) }
1072
+ /** Builds a self-contained reserve amount after a successful onchain read. */
1073
+ function reserve(options: reserve.Options) {
1074
+ if (options.amount === undefined) return undefined
1075
+ return Value.tokenAmount({
1076
+ baseUnits: options.amount,
1077
+ currency: options.token.currency,
1078
+ decimals: options.token.decimals,
1079
+ })
1080
+ }
1081
+
1082
+ declare namespace reserve {
1083
+ /** Inputs for formatting one side of a Fee AMM pool reserve. */
1084
+ type Options = {
1085
+ /** Base-unit reserve returned by `FeeManager.getPool`. */
1086
+ amount: string | undefined
1087
+ /** Token metadata used to describe and format the reserve. */
1088
+ token: Pick<z.output<typeof Tokens.schema.TokenReference>, 'currency' | 'decimals'>
1089
+ }
1111
1090
  }
1112
1091
 
1113
1092
  /** Left-pads a 20-byte address to its 32-byte log-topic form. */
@@ -6,6 +6,16 @@ import * as TestApp from '../../../../test/App.js'
6
6
  import * as Funding from './funding.js'
7
7
 
8
8
  describe('GET /funding/quotes', () => {
9
+ test('publishes alphabetically ordered quote example fields', async () => {
10
+ const app = TestApp.create({ auth: false })
11
+ const spec = await (await app.request('/openapi.json')).json()
12
+ const operation = spec.paths['/v1/funding/quotes'].get
13
+ const response = operation.responses['200'].content['application/json']
14
+ const fields = Object.keys(response.example.data[0])
15
+
16
+ expect(fields).toEqual([...fields].sort())
17
+ })
18
+
9
19
  test('returns successful live quotes with output amounts', async () => {
10
20
  const app = TestApp.create({ funding: { adapters: { relay: relayAdapter() } } })
11
21
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
@@ -15,16 +25,26 @@ describe('GET /funding/quotes', () => {
15
25
 
16
26
  expect(body.data).toHaveLength(1)
17
27
  expect(body.data[0]).toMatchObject({
28
+ destinationAmount: {
29
+ baseUnits: '990000',
30
+ currency: 'USD',
31
+ decimals: 6,
32
+ formatted: '0.99',
33
+ },
34
+ destinationAmountMin: {
35
+ baseUnits: '980000',
36
+ currency: 'USD',
37
+ decimals: 6,
38
+ formatted: '0.98',
39
+ },
18
40
  id: 'base-usdc-tempo-usdce-relay',
19
41
  provider: { id: 'relay' },
20
42
  quality: { liquiditySource: 'providerQuote', tier: 'liquid' },
21
- quote: {
22
- inputAmount: '1000000',
23
- outputAmount: '990000',
24
- outputAmountFormatted: '0.99',
25
- },
26
- sourceAmount: { amount: '1000000', formatted: '1' },
43
+ quote: { sampledAt: '2026-01-01T00:00:00Z' },
44
+ sourceAmount: { baseUnits: '1000000', currency: 'USD', decimals: 6, formatted: '1' },
27
45
  })
46
+ expect(body.data[0]?.sourceToken).not.toHaveProperty('amount')
47
+ expect(body.data[0]?.destinationToken).not.toHaveProperty('amount')
28
48
  })
29
49
 
30
50
  test('queries only the requested provider', async () => {
@@ -342,6 +362,9 @@ describe('GET /funding/chains', () => {
342
362
  expect(body.data.flatMap((chain) => chain.tokens.map((token) => token.symbol))).toEqual(
343
363
  expect.arrayContaining(['USDC', 'USDT']),
344
364
  )
365
+ expect(
366
+ body.data.flatMap((chain) => chain.tokens).every((token) => token.currency === 'USD'),
367
+ ).toBe(true)
345
368
  })
346
369
  })
347
370
 
@@ -424,7 +447,6 @@ function relayAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAda
424
447
  return {
425
448
  minimumOutputAmount: '980000',
426
449
  outputAmount: '990000',
427
- outputAmountFormatted: '0.99',
428
450
  quality: {
429
451
  estimatedSeconds: 4,
430
452
  liquiditySource: 'providerQuote',
@@ -448,7 +470,6 @@ function acrossAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAd
448
470
  expiresAt: '2026-01-01T00:01:00Z',
449
471
  minimumOutputAmount: '988000',
450
472
  outputAmount: '990000',
451
- outputAmountFormatted: '0.99',
452
473
  quality: {
453
474
  estimatedSeconds: 2,
454
475
  liquiditySource: 'providerQuote',