tapimo 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/dist/App.d.ts +13987 -5755
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/admin/App.d.ts +910 -412
  5. package/dist/admin/App.d.ts.map +1 -1
  6. package/dist/admin/App.js +4 -0
  7. package/dist/admin/App.js.map +1 -1
  8. package/dist/admin/apps/early-access.d.ts +148 -0
  9. package/dist/admin/apps/early-access.d.ts.map +1 -0
  10. package/dist/admin/apps/early-access.js +133 -0
  11. package/dist/admin/apps/early-access.js.map +1 -0
  12. package/dist/admin/apps/verified-tokens.d.ts +643 -118
  13. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  14. package/dist/admin/apps/verified-tokens.js +214 -1
  15. package/dist/admin/apps/verified-tokens.js.map +1 -1
  16. package/dist/admin/ui.generated.d.ts +1 -1
  17. package/dist/admin/ui.generated.d.ts.map +1 -1
  18. package/dist/admin/ui.generated.js +45 -33
  19. package/dist/admin/ui.generated.js.map +1 -1
  20. package/dist/apps/data/App.d.ts +3733 -2428
  21. package/dist/apps/data/App.d.ts.map +1 -1
  22. package/dist/apps/data/App.js +11 -6
  23. package/dist/apps/data/App.js.map +1 -1
  24. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  25. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  26. package/dist/apps/data/FundingQuoteProviders.js +193 -2
  27. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  28. package/dist/apps/data/FxOracle.d.ts +71 -0
  29. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  30. package/dist/apps/data/FxOracle.js +97 -0
  31. package/dist/apps/data/FxOracle.js.map +1 -0
  32. package/dist/apps/data/index.d.ts +1 -0
  33. package/dist/apps/data/index.d.ts.map +1 -1
  34. package/dist/apps/data/index.js +1 -0
  35. package/dist/apps/data/index.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +3575 -1358
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +230 -88
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +963 -342
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/addresses.js +11 -9
  43. package/dist/apps/data/routes/addresses.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +108 -57
  45. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.js +88 -45
  47. package/dist/apps/data/routes/balances.js.map +1 -1
  48. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  49. package/dist/apps/data/routes/exchanges.d.ts +952 -201
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +518 -76
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  54. package/dist/apps/data/routes/funding.d.ts +8 -2
  55. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  56. package/dist/apps/data/routes/funding.js +32 -6
  57. package/dist/apps/data/routes/funding.js.map +1 -1
  58. package/dist/apps/data/routes/indexer.d.ts +5 -5
  59. package/dist/apps/data/routes/receipts.d.ts +392 -315
  60. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  61. package/dist/apps/data/routes/receipts.js +152 -77
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/tokens.d.ts +166 -173
  64. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  65. package/dist/apps/data/routes/tokens.js +14 -12
  66. package/dist/apps/data/routes/tokens.js.map +1 -1
  67. package/dist/apps/data/routes/transactions.d.ts +277 -536
  68. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +96 -77
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts +82 -17
  72. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  73. package/dist/apps/data/routes/transfers.js +70 -13
  74. package/dist/apps/data/routes/transfers.js.map +1 -1
  75. package/dist/apps/data/routes/valuation.d.ts +307 -0
  76. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  77. package/dist/apps/data/routes/valuation.js +292 -0
  78. package/dist/apps/data/routes/valuation.js.map +1 -0
  79. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.js +7 -5
  81. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  82. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +124 -25
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/index.d.ts +1 -1
  87. package/dist/apps/index.d.ts.map +1 -1
  88. package/dist/apps/index.js +1 -1
  89. package/dist/apps/index.js.map +1 -1
  90. package/dist/apps/management/App.d.ts +8606 -1681
  91. package/dist/apps/management/App.d.ts.map +1 -1
  92. package/dist/apps/management/App.js +12 -1
  93. package/dist/apps/management/App.js.map +1 -1
  94. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  95. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  96. package/dist/apps/management/routes/api-keys.js +5 -3
  97. package/dist/apps/management/routes/api-keys.js.map +1 -1
  98. package/dist/apps/management/routes/billing.d.ts +88 -48
  99. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  100. package/dist/apps/management/routes/billing.js +10 -2
  101. package/dist/apps/management/routes/billing.js.map +1 -1
  102. package/dist/apps/management/routes/faucet.d.ts +218 -0
  103. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  104. package/dist/apps/management/routes/faucet.js +139 -0
  105. package/dist/apps/management/routes/faucet.js.map +1 -0
  106. package/dist/apps/management/routes/invitations.d.ts +121 -121
  107. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  108. package/dist/apps/management/routes/invitations.js +3 -0
  109. package/dist/apps/management/routes/invitations.js.map +1 -1
  110. package/dist/apps/management/routes/invite-links.d.ts +197 -168
  111. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  112. package/dist/apps/management/routes/invite-links.js +26 -1
  113. package/dist/apps/management/routes/invite-links.js.map +1 -1
  114. package/dist/apps/management/routes/me.d.ts +2 -0
  115. package/dist/apps/management/routes/me.d.ts.map +1 -1
  116. package/dist/apps/management/routes/me.js +4 -0
  117. package/dist/apps/management/routes/me.js.map +1 -1
  118. package/dist/apps/management/routes/members.d.ts +49 -49
  119. package/dist/apps/management/routes/orgs.d.ts +91 -91
  120. package/dist/apps/management/routes/projects.d.ts +115 -115
  121. package/dist/apps/management/routes/usage.d.ts +35 -32
  122. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  123. package/dist/apps/management/routes/usage.js +4 -0
  124. package/dist/apps/management/routes/usage.js.map +1 -1
  125. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  126. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  127. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  128. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  129. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  130. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  131. package/dist/apps/management/routes/webhooks.js +573 -0
  132. package/dist/apps/management/routes/webhooks.js.map +1 -0
  133. package/dist/apps/mcp.d.ts +9 -2
  134. package/dist/apps/mcp.d.ts.map +1 -1
  135. package/dist/apps/mcp.js +9 -4
  136. package/dist/apps/mcp.js.map +1 -1
  137. package/dist/cli/Cli.d.ts.map +1 -1
  138. package/dist/cli/Cli.js +7 -2
  139. package/dist/cli/Cli.js.map +1 -1
  140. package/dist/db/Db.d.ts +4 -0
  141. package/dist/db/Db.d.ts.map +1 -1
  142. package/dist/db/Db.js +83 -0
  143. package/dist/db/Db.js.map +1 -1
  144. package/dist/db/Schema.d.ts +41 -0
  145. package/dist/db/Schema.d.ts.map +1 -1
  146. package/dist/db/Schema.js +45 -0
  147. package/dist/db/Schema.js.map +1 -1
  148. package/dist/db/tables/earlyAccess.d.ts +50 -0
  149. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  150. package/dist/db/tables/earlyAccess.js +63 -0
  151. package/dist/db/tables/earlyAccess.js.map +1 -0
  152. package/dist/db/tables/invitations.d.ts +12 -1
  153. package/dist/db/tables/invitations.d.ts.map +1 -1
  154. package/dist/db/tables/invitations.js +23 -0
  155. package/dist/db/tables/invitations.js.map +1 -1
  156. package/dist/db/tables/inviteLinks.d.ts +10 -2
  157. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  158. package/dist/db/tables/inviteLinks.js +10 -1
  159. package/dist/db/tables/inviteLinks.js.map +1 -1
  160. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  161. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.js +2 -0
  163. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  164. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  165. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  166. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  167. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  168. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  169. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  170. package/dist/db/tables/webhookSubscriptions.js +16 -0
  171. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  172. package/dist/internal/Auth.d.ts +43 -2
  173. package/dist/internal/Auth.d.ts.map +1 -1
  174. package/dist/internal/Auth.js +60 -45
  175. package/dist/internal/Auth.js.map +1 -1
  176. package/dist/internal/FundingQuotes.d.ts +69 -15
  177. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  178. package/dist/internal/FundingQuotes.js +74 -25
  179. package/dist/internal/FundingQuotes.js.map +1 -1
  180. package/dist/internal/Log.d.ts +19 -1
  181. package/dist/internal/Log.d.ts.map +1 -1
  182. package/dist/internal/Log.js +2 -0
  183. package/dist/internal/Log.js.map +1 -1
  184. package/dist/internal/MetricSink.d.ts.map +1 -1
  185. package/dist/internal/MetricSink.js +9 -0
  186. package/dist/internal/MetricSink.js.map +1 -1
  187. package/dist/internal/OpenApi.d.ts +2 -2
  188. package/dist/internal/OpenApi.d.ts.map +1 -1
  189. package/dist/internal/OpenApi.js +3 -0
  190. package/dist/internal/OpenApi.js.map +1 -1
  191. package/dist/internal/Schema.d.ts +14 -5
  192. package/dist/internal/Schema.d.ts.map +1 -1
  193. package/dist/internal/Schema.js +23 -5
  194. package/dist/internal/Schema.js.map +1 -1
  195. package/dist/internal/Value.d.ts +18 -0
  196. package/dist/internal/Value.d.ts.map +1 -1
  197. package/dist/internal/Value.js +11 -1
  198. package/dist/internal/Value.js.map +1 -1
  199. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  200. package/dist/internal/VerifiedTokens.js +10 -1
  201. package/dist/internal/VerifiedTokens.js.map +1 -1
  202. package/dist/internal/Viem.d.ts +1 -1
  203. package/dist/internal/Webhooks.d.ts +24 -3
  204. package/dist/internal/Webhooks.d.ts.map +1 -1
  205. package/dist/internal/Webhooks.js +34 -8
  206. package/dist/internal/Webhooks.js.map +1 -1
  207. package/package.json +2 -2
  208. package/src/App.ts +9 -1
  209. package/src/Client.test-d.ts +638 -6
  210. package/src/admin/App.ts +4 -0
  211. package/src/admin/apps/early-access.test.ts +125 -0
  212. package/src/admin/apps/early-access.ts +175 -0
  213. package/src/admin/apps/verified-tokens.test.ts +201 -0
  214. package/src/admin/apps/verified-tokens.ts +532 -286
  215. package/src/admin/ui/plugin.ts +2 -1
  216. package/src/admin/ui/src/lib/early-access.ts +63 -0
  217. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  218. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  219. package/src/admin/ui/src/routes/__root.tsx +2 -1
  220. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  221. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  222. package/src/admin/ui.generated.ts +48 -33
  223. package/src/apps/data/App.ts +22 -14
  224. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  225. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  226. package/src/apps/data/FxOracle.test.ts +118 -0
  227. package/src/apps/data/FxOracle.ts +153 -0
  228. package/src/apps/data/index.ts +1 -0
  229. package/src/apps/data/routes/activities.test.ts +173 -3
  230. package/src/apps/data/routes/activities.ts +403 -132
  231. package/src/apps/data/routes/addresses.ts +19 -9
  232. package/src/apps/data/routes/balances.test.ts +144 -11
  233. package/src/apps/data/routes/balances.ts +140 -51
  234. package/src/apps/data/routes/exchanges.test.ts +540 -9
  235. package/src/apps/data/routes/exchanges.ts +690 -103
  236. package/src/apps/data/routes/funding.test.ts +84 -0
  237. package/src/apps/data/routes/funding.ts +38 -6
  238. package/src/apps/data/routes/receipts.test.ts +125 -23
  239. package/src/apps/data/routes/receipts.ts +205 -100
  240. package/src/apps/data/routes/tokens.test.ts +43 -0
  241. package/src/apps/data/routes/tokens.ts +27 -12
  242. package/src/apps/data/routes/transactions.test.ts +72 -19
  243. package/src/apps/data/routes/transactions.ts +162 -92
  244. package/src/apps/data/routes/transfers.test.ts +146 -4
  245. package/src/apps/data/routes/transfers.ts +91 -13
  246. package/src/apps/data/routes/valuation.test.ts +422 -0
  247. package/src/apps/data/routes/valuation.ts +423 -0
  248. package/src/apps/data/routes/verified-tokens.ts +14 -2
  249. package/src/apps/data/routes/webhooks.test.ts +56 -8
  250. package/src/apps/data/routes/webhooks.ts +144 -27
  251. package/src/apps/index.ts +1 -1
  252. package/src/apps/management/App.ts +13 -0
  253. package/src/apps/management/routes/api-keys.test.ts +87 -0
  254. package/src/apps/management/routes/api-keys.ts +4 -3
  255. package/src/apps/management/routes/billing.test.ts +112 -1
  256. package/src/apps/management/routes/billing.ts +7 -2
  257. package/src/apps/management/routes/faucet.test.ts +79 -0
  258. package/src/apps/management/routes/faucet.ts +185 -0
  259. package/src/apps/management/routes/invitations.test.ts +172 -0
  260. package/src/apps/management/routes/invitations.ts +3 -0
  261. package/src/apps/management/routes/invite-links.test.ts +117 -0
  262. package/src/apps/management/routes/invite-links.ts +43 -1
  263. package/src/apps/management/routes/me.test.ts +140 -88
  264. package/src/apps/management/routes/me.ts +6 -0
  265. package/src/apps/management/routes/usage.test.ts +6 -0
  266. package/src/apps/management/routes/usage.ts +7 -0
  267. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  268. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  269. package/src/apps/management/routes/webhooks.test.ts +426 -0
  270. package/src/apps/management/routes/webhooks.ts +672 -0
  271. package/src/apps/mcp.test.ts +13 -0
  272. package/src/apps/mcp.ts +18 -5
  273. package/src/cli/Cli.test.ts +36 -0
  274. package/src/cli/Cli.ts +7 -2
  275. package/src/db/Db.test.ts +19 -0
  276. package/src/db/Db.ts +95 -0
  277. package/src/db/Schema.ts +53 -0
  278. package/src/db/tables/earlyAccess.ts +86 -0
  279. package/src/db/tables/invitations.ts +26 -1
  280. package/src/db/tables/inviteLinks.ts +22 -3
  281. package/src/db/tables/sponsoredTransactions.ts +4 -0
  282. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  283. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  284. package/src/db/tables/webhookSubscriptions.ts +31 -0
  285. package/src/internal/Auth.ts +73 -45
  286. package/src/internal/FundingQuotes.test.ts +143 -1
  287. package/src/internal/FundingQuotes.ts +95 -28
  288. package/src/internal/Log.test-d.ts +21 -0
  289. package/src/internal/Log.test.ts +50 -0
  290. package/src/internal/Log.ts +24 -1
  291. package/src/internal/MetricSink.test.ts +160 -0
  292. package/src/internal/MetricSink.ts +10 -0
  293. package/src/internal/OpenApi.ts +3 -0
  294. package/src/internal/Schema.ts +50 -5
  295. package/src/internal/Value.test.ts +20 -0
  296. package/src/internal/Value.ts +24 -1
  297. package/src/internal/VerifiedTokens.ts +9 -1
  298. package/src/internal/Webhooks.test.ts +140 -0
  299. package/src/internal/Webhooks.ts +53 -9
@@ -1,5 +1,5 @@
1
1
  import { Hono, type Context } from 'hono'
2
- import { AbiEvent, type Hex } from 'ox'
2
+ import { AbiError, AbiEvent, type Hex } from 'ox'
3
3
  import {
4
4
  type Address,
5
5
  BaseError,
@@ -7,7 +7,7 @@ import {
7
7
  decodeFunctionData,
8
8
  parseAbi,
9
9
  } from 'viem'
10
- import { Actions, Addresses, Tick } from 'viem/tempo'
10
+ import { Abis, Actions, Addresses, Tick } from 'viem/tempo'
11
11
  import * as z from 'zod/mini'
12
12
 
13
13
  import type * as App from '../../../App.js'
@@ -22,12 +22,16 @@ import * as Timing from '../../../internal/Timing.js'
22
22
  import * as Ttl from '../../../internal/Ttl.js'
23
23
  import * as Value from '../../../internal/Value.js'
24
24
  import * as VerifiedTokens from '../../../internal/VerifiedTokens.js'
25
+ import * as FxOracle from '../FxOracle.js'
25
26
  import * as Tokens from './tokens.js'
27
+ import * as Valuation from './valuation.js'
26
28
 
27
29
  // Stablecoin DEX precompile. Pair lifecycle is announced by `PairCreated` from
28
30
  // this address; the indexer pre-decodes those into the `dex_pairs` table.
29
31
  const stablecoinDex = Addresses.stablecoinDex.toLowerCase()
30
32
 
33
+ const contractPausedSelector = AbiError.getSelector(AbiError.fromAbi(Abis.tip20, 'ContractPaused'))
34
+
31
35
  // `OrderCancelled(uint128 orderId)` — emitted when a maker cancels a resting
32
36
  // order. Used by the orders endpoint to exclude cancelled orders from the
33
37
  // resting set.
@@ -71,6 +75,18 @@ const swapAbi = parseAbi([
71
75
  // pathUSD on-chain). Uses a fresh `Schema.tokenAddress(...)` so the example
72
76
  // reaches the pipe input that path-param OpenAPI generation reads from.
73
77
  const exampleBase = '0x20c000000000000000000000b9537d11c60e8b50' as const
78
+ const exampleSourceToken = '0x20c0000000000000000000008f5425160ebe5525' as const
79
+ const exampleApprovalCall = Actions.token.approve.call({
80
+ amount: 1_000_000n,
81
+ spender: Addresses.stablecoinDex,
82
+ token: exampleSourceToken,
83
+ })
84
+ const exampleSwapCall = Actions.dex.sell.call({
85
+ amountIn: 1_000_000n,
86
+ minAmountOut: 995_000n,
87
+ tokenIn: exampleSourceToken,
88
+ tokenOut: exampleBase,
89
+ })
74
90
 
75
91
  /**
76
92
  * Tick spacing enforced by the stablecoin DEX precompile: orders must be
@@ -105,6 +121,14 @@ const maxDepthLevels = 200
105
121
  */
106
122
  const maxBuckets = 500
107
123
 
124
+ /** Largest amount accepted by the DEX precompile's uint128 swap arguments. */
125
+ const maxSwapAmount = 2n ** 128n - 1n
126
+
127
+ /** Basis-point denominator used for caller-selected swap slippage. */
128
+ const slippageDenominator = 10_000n
129
+
130
+ const tokenFields = ['token.logoUri', 'token.verified'] as const
131
+
108
132
  /** Converts the OHLC `interval` enum to a millisecond duration. */
109
133
  function intervalMs(interval: '1m' | '5m' | '15m' | '1h' | '4h' | '1d'): number {
110
134
  switch (interval) {
@@ -211,6 +235,195 @@ export namespace schema {
211
235
  'One side of a trading pair (with metadata when requested via `include=tokens`).',
212
236
  )
213
237
 
238
+ /** A token amount carrying its nominal value in the requested denomination. */
239
+ const ValuedAmount = z.extend(Schema.TokenAmount, {
240
+ valuation: z
241
+ .optional(z.nullable(Valuation.schema.Value))
242
+ .check(
243
+ z.describe(
244
+ 'The amount’s nominal value in the requested `denomination`. `null` when the token is ' +
245
+ 'unverified, its display currency has no rate, or rates were unavailable.',
246
+ ),
247
+ ),
248
+ })
249
+
250
+ /** Schemas for creating an executable native DEX exchange quote. */
251
+ export namespace createQuote {
252
+ const DestinationTokenAddress = Schema.tokenAddress(exampleBase)
253
+ const SourceTokenAddress = Schema.tokenAddress(exampleSourceToken)
254
+
255
+ /** Positive token amount accepted by the DEX's uint128 arguments. */
256
+ export const Amount = Schema.DecimalString.check(
257
+ z.refine(
258
+ (value) => {
259
+ const amount = BigInt(value)
260
+ return amount > 0n && amount <= maxSwapAmount
261
+ },
262
+ { error: 'Amount must be between 1 and the maximum uint128 value.' },
263
+ ),
264
+ z.describe(
265
+ 'Positive token amount in the token’s smallest unit, encoded as a decimal string.',
266
+ ),
267
+ z.meta({ examples: ['1000000'] }),
268
+ )
269
+
270
+ /** Optional token fields that require sources beyond RPC metadata. */
271
+ export const Include = z
272
+ .enum(tokenFields)
273
+ .check(z.describe('Additional token fields to include in the quote response.'))
274
+
275
+ /** Parses comma-separated optional token fields. */
276
+ export const includeQuery = Schema.includeQuery(
277
+ Include,
278
+ 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.',
279
+ )
280
+
281
+ /** Query parameters selecting the Tempo chain and optional token fields. */
282
+ export const Query = z
283
+ .strictObject({
284
+ chainId: Schema.ChainIdQuery,
285
+ denomination: Schema.Denomination,
286
+ include: includeQuery,
287
+ })
288
+ .check(z.describe('Query parameters for creating a native DEX exchange quote.'))
289
+
290
+ /** Quote request with one pinned side and caller-selected slippage. */
291
+ export const Request = z
292
+ .strictObject({
293
+ amount: Amount.check(
294
+ z.describe(
295
+ 'Pinned amount in base units: source amount for `exactSource`, destination amount for `exactDestination`.',
296
+ ),
297
+ ),
298
+ destinationToken: DestinationTokenAddress.check(
299
+ z.describe('TIP-20 token the swap receives.'),
300
+ ),
301
+ mode: z
302
+ .enum(['exactSource', 'exactDestination'])
303
+ .check(
304
+ z.describe('Which side of the swap keeps the requested amount exact.'),
305
+ z.meta({ examples: ['exactSource'] }),
306
+ ),
307
+ slippageBps: z
308
+ .number()
309
+ .check(
310
+ z.int(),
311
+ z.gte(0),
312
+ z.lt(10_000),
313
+ z.describe('Allowed execution slippage in basis points, where 100 is 1%.'),
314
+ z.meta({ examples: [50] }),
315
+ ),
316
+ sourceToken: SourceTokenAddress.check(z.describe('TIP-20 token the swap spends.')),
317
+ })
318
+ .check(
319
+ z.refine((request) => request.sourceToken !== request.destinationToken, {
320
+ error: 'Source and destination tokens must differ.',
321
+ }),
322
+ z.describe('Request for an executable quote on Tempo’s native stablecoin DEX.'),
323
+ )
324
+
325
+ /** One call in the unsigned Tempo transaction plan. */
326
+ export const Call = z
327
+ .object({
328
+ data: Schema.Hex.check(z.describe('ABI-encoded call data.')),
329
+ to: Schema.Address.check(z.describe('Contract or precompile that receives the call.')),
330
+ value: Schema.Quantity.check(
331
+ z.describe('Native token value sent with the call; native DEX swap calls use `0x0`.'),
332
+ z.meta({ examples: ['0x0'] }),
333
+ ),
334
+ })
335
+ .check(z.describe('One unsigned call in the Tempo transaction plan.'))
336
+
337
+ const QuoteToken = Schema.describe(
338
+ z.partial(z.omit(Tokens.schema.Token, { id: true }), {
339
+ logoUri: true,
340
+ verified: true,
341
+ }),
342
+ 'A token referenced by an exchange quote, with RPC metadata and optional enrichments.',
343
+ )
344
+
345
+ const DestinationToken = z.extend(QuoteToken, {
346
+ address: DestinationTokenAddress.check(z.describe('TIP-20 token the swap receives.')),
347
+ })
348
+
349
+ const SourceToken = z.extend(QuoteToken, {
350
+ address: SourceTokenAddress.check(z.describe('TIP-20 token the swap spends.')),
351
+ })
352
+
353
+ const Transaction = z
354
+ .object({
355
+ calls: z.array(Call).check(
356
+ z.length(2),
357
+ z.describe('Approval followed by the native DEX swap call.'),
358
+ z.meta({
359
+ examples: [
360
+ [
361
+ {
362
+ data: exampleApprovalCall.data,
363
+ to: exampleApprovalCall.to,
364
+ value: '0x0',
365
+ },
366
+ { data: exampleSwapCall.data, to: exampleSwapCall.to, value: '0x0' },
367
+ ],
368
+ ],
369
+ }),
370
+ ),
371
+ chainId: Schema.ChainId.check(z.describe('Tempo chain where these calls execute.')),
372
+ })
373
+ .check(z.describe('Unsigned Tempo transaction plan.'))
374
+
375
+ /** Response-level valuation rate provenance. */
376
+ export const Meta = z
377
+ .object({
378
+ pricing: z
379
+ .optional(Valuation.schema.Pricing)
380
+ .check(
381
+ z.describe(
382
+ 'Rate provenance for amount valuations. Present when conversion rates were consulted.',
383
+ ),
384
+ ),
385
+ })
386
+ .check(z.describe('Response-level resources attached to this quote.'))
387
+
388
+ /** Native DEX quote plus the approval and swap calls needed to execute it. */
389
+ export const Response = z
390
+ .discriminatedUnion('mode', [
391
+ z.object({
392
+ destinationAmount: ValuedAmount.check(z.describe('Quoted destination amount.')),
393
+ destinationAmountMin: ValuedAmount.check(
394
+ z.describe('Minimum destination amount after slippage.'),
395
+ ),
396
+ destinationToken: DestinationToken,
397
+ meta: z.optional(Meta).check(z.describe('Valuation rate provenance for this quote.')),
398
+ mode: z
399
+ .literal('exactSource')
400
+ .check(
401
+ z.describe('The source amount is fixed and the destination has a minimum.'),
402
+ z.meta({ examples: ['exactSource'] }),
403
+ ),
404
+ sourceAmount: ValuedAmount.check(z.describe('Exact source amount.')),
405
+ sourceToken: SourceToken.check(z.describe('Token spent by the swap.')),
406
+ transaction: Transaction,
407
+ }),
408
+ z.object({
409
+ destinationAmount: ValuedAmount.check(z.describe('Exact destination amount.')),
410
+ destinationToken: DestinationToken.check(z.describe('Token received by the swap.')),
411
+ meta: z.optional(Meta).check(z.describe('Valuation rate provenance for this quote.')),
412
+ mode: z
413
+ .literal('exactDestination')
414
+ .check(
415
+ z.describe('The destination amount is fixed and the source has a maximum.'),
416
+ z.meta({ examples: ['exactDestination'] }),
417
+ ),
418
+ sourceAmount: ValuedAmount.check(z.describe('Quoted source amount.')),
419
+ sourceAmountMax: ValuedAmount.check(z.describe('Maximum source amount after slippage.')),
420
+ sourceToken: SourceToken,
421
+ transaction: Transaction,
422
+ }),
423
+ ])
424
+ .check(z.describe('Executable native DEX quote with slippage-protected calls.'))
425
+ }
426
+
214
427
  /** Schemas for the getPairs operation. */
215
428
  export namespace getPairs {
216
429
  /**
@@ -368,23 +581,15 @@ export namespace schema {
368
581
 
369
582
  /** Schemas for the getSwaps operation (the swap feed, optionally pair-scoped). */
370
583
  export namespace getSwaps {
371
- /**
372
- * Optional related resource that callers opt into via `include`. Currently
373
- * only `tokens` (embeds metadata for both pair tokens on each row's
374
- * `sourceToken`/`destinationToken`).
375
- */
584
+ /** Optional token fields that require sources beyond RPC metadata. */
376
585
  export const Include = z
377
- .enum(['tokens'])
378
- .check(z.describe('Related resource you can opt into with `include`.'))
586
+ .enum(tokenFields)
587
+ .check(z.describe('Additional token fields to include in swap responses.'))
379
588
 
380
- /**
381
- * Parses a comma-separated `include` query value into a list of optional
382
- * resources to embed. Extra lookups (e.g. token metadata) only run when
383
- * explicitly requested, keeping the base swap feed fast.
384
- */
589
+ /** Parses comma-separated optional token fields. */
385
590
  export const includeQuery = Schema.includeQuery(
386
591
  Include,
387
- 'Comma-separated optional resources to embed, e.g. `tokens`.',
592
+ 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.',
388
593
  )
389
594
 
390
595
  /** Query parameters for swap feed requests. */
@@ -394,6 +599,7 @@ export namespace schema {
394
599
  'blockNumber.to': Schema.blockNumberBound('swaps', 'to'),
395
600
  chainId: Schema.ChainIdQuery,
396
601
  cursor: Schema.Cursor,
602
+ denomination: Schema.Denomination,
397
603
  include: includeQuery,
398
604
  limit: Schema.Limit,
399
605
  maker: z
@@ -428,30 +634,19 @@ export namespace schema {
428
634
  ),
429
635
  )
430
636
 
431
- /**
432
- * One side of a swap: a `PairToken` (address plus optional metadata when
433
- * requested via `include=tokens`) annotated with the `amount` that moved
434
- * on this side, as a decimal integer string in base units.
435
- */
436
- export const SwapSide = z
437
- .extend(PairToken, {
438
- amount: Schema.DecimalString.check(
439
- z.describe(
440
- 'Amount moved on this side, returned as a decimal integer string in the token’s smallest units.',
441
- ),
442
- z.meta({ examples: ['1000000'] }),
443
- ),
444
- })
445
- .check(z.describe('One side of a swap: the token and amount that moved.'))
637
+ /** One swap-side token with RPC metadata and optional curated fields. */
638
+ export const SwapSide = Schema.describe(
639
+ Tokens.schema.TokenReference,
640
+ 'A token referenced by one side of a swap.',
641
+ )
446
642
 
447
643
  /** A single maker-order fill within a swap, framed from the taker's perspective. */
448
644
  export const Fill = z
449
645
  .object({
450
- destinationToken: SwapSide.check(
451
- z.describe(
452
- 'Token and amount the taker received in this fill, with token metadata when requested via `include=tokens`.',
453
- ),
646
+ destinationAmount: ValuedAmount.check(
647
+ z.describe('Amount the taker received in this fill.'),
454
648
  ),
649
+ destinationToken: SwapSide.check(z.describe('Token the taker received in this fill.')),
455
650
  logIndex: z
456
651
  .number()
457
652
  .check(
@@ -479,11 +674,8 @@ export namespace schema {
479
674
  ),
480
675
  z.meta({ examples: ['1.00000'] }),
481
676
  ),
482
- sourceToken: SwapSide.check(
483
- z.describe(
484
- 'Token and amount the taker sent in this fill, with token metadata when requested via `include=tokens`.',
485
- ),
486
- ),
677
+ sourceAmount: ValuedAmount.check(z.describe('Amount the taker sent in this fill.')),
678
+ sourceToken: SwapSide.check(z.describe('Token the taker sent in this fill.')),
487
679
  })
488
680
  .check(
489
681
  z.describe('One maker-order fill within a swap, described from the taker’s perspective.'),
@@ -506,11 +698,10 @@ export namespace schema {
506
698
  z.describe('Block number where the swap was included.'),
507
699
  z.meta({ examples: [23456789] }),
508
700
  ),
509
- destinationToken: SwapSide.check(
510
- z.describe(
511
- 'Destination side of the swap: what the taker received, summed across the final hop.',
512
- ),
701
+ destinationAmount: ValuedAmount.check(
702
+ z.describe('Total amount the taker received in this swap.'),
513
703
  ),
704
+ destinationToken: SwapSide.check(z.describe('Destination token for the final hop.')),
514
705
  filledAt: z.iso
515
706
  .datetime()
516
707
  .check(
@@ -564,9 +755,8 @@ export namespace schema {
564
755
  'Token path for the swap, from source through any intermediate tokens to destination. `route.length - 1` is the hop count; two entries means a direct swap.',
565
756
  ),
566
757
  ),
567
- sourceToken: SwapSide.check(
568
- z.describe('Source side of the swap: what the taker sent, summed across the first hop.'),
569
- ),
758
+ sourceAmount: ValuedAmount.check(z.describe('Total amount the taker sent in this swap.')),
759
+ sourceToken: SwapSide.check(z.describe('Source token for the first hop.')),
570
760
  taker: Schema.Address.check(z.describe('Taker address that initiated the swap.')),
571
761
  transactionHash: Schema.Hash.check(
572
762
  z.describe('Transaction hash for the transaction containing the swap.'),
@@ -581,9 +771,26 @@ export namespace schema {
581
771
  * rows per page; a transaction batching several unrelated swaps by one
582
772
  * taker may have its rows split across consecutive pages.
583
773
  */
774
+ /** Page-level resources: valuation rate provenance. */
775
+ export const Meta = z
776
+ .object({
777
+ pricing: z
778
+ .optional(Valuation.schema.Pricing)
779
+ .check(
780
+ z.describe(
781
+ 'Rate provenance for amount valuations. Present when conversion rates were ' +
782
+ 'consulted; absent when every value was identity-valued or rates were unavailable.',
783
+ ),
784
+ ),
785
+ })
786
+ .check(z.describe('Page-level resources attached to this response.'))
787
+
584
788
  export const Response = z
585
789
  .object({
586
790
  data: z.array(Swap).check(z.describe('Swaps returned on this page.')),
791
+ meta: z
792
+ .optional(Meta)
793
+ .check(z.describe('Page-level resources, such as valuation rate provenance.')),
587
794
  nextCursor: Schema.NextCursor,
588
795
  })
589
796
  .check(z.describe('A paginated list of swaps on Tempo’s built-in stablecoin exchange.'))
@@ -1309,8 +1516,238 @@ export namespace schema {
1309
1516
  * `token_balances_snapshot` row attribute cleanly to one pair. Pairs whose
1310
1517
  * DEX-escrow balance is zero are excluded from the liquidity ranking.
1311
1518
  */
1312
- export function exchanges() {
1519
+ export function exchanges(options: exchanges.Options = {}) {
1520
+ const oracle = options.fx?.oracle ?? FxOracle.ecb()
1313
1521
  return new Hono<App.Environment>()
1522
+ .post(
1523
+ '/v1/exchange/quotes',
1524
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
1525
+ OpenApi.validate('query', schema.createQuote.Query, {
1526
+ code: 'query_invalid',
1527
+ message: 'Invalid query parameters',
1528
+ }),
1529
+ OpenApi.validate('json', schema.createQuote.Request, {
1530
+ code: 'body_invalid',
1531
+ message: 'Invalid request body',
1532
+ }),
1533
+ OpenApi.describeRoute({
1534
+ description:
1535
+ 'Creates a priced exchange preview on Tempo’s native stablecoin DEX with unsigned approval and swap calls. This endpoint does not submit a transaction.',
1536
+ operationId: 'createExchangeQuote',
1537
+ responses: OpenApi.responses({
1538
+ errors: {
1539
+ 400: { codes: ['body_invalid', 'query_invalid', 'quote_amount_out_of_range'] },
1540
+ 404: {
1541
+ codes: ['quote_not_available'],
1542
+ description: 'The native DEX cannot price an executable exchange for these terms.',
1543
+ },
1544
+ 502: 'The Tempo RPC could not quote the swap or resolve token metadata.',
1545
+ },
1546
+ success: {
1547
+ description: 'A native DEX quote and the unsigned calls needed to execute it.',
1548
+ schema: schema.createQuote.Response,
1549
+ },
1550
+ }),
1551
+ summary: 'Create quote',
1552
+ tags: ['Exchange'],
1553
+ }),
1554
+ async (c) => {
1555
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
1556
+ if (OpenApi.narrowValidation)
1557
+ return OpenApi.validationError(c, {
1558
+ code: 'query_invalid',
1559
+ message: 'Invalid query parameters',
1560
+ })
1561
+ if (OpenApi.narrowValidation)
1562
+ return OpenApi.validationError(c, {
1563
+ code: 'body_invalid',
1564
+ message: 'Invalid request body',
1565
+ })
1566
+
1567
+ const request = c.req.valid('json')
1568
+ const options = c.req.valid('query')
1569
+ const chainId = options.chainId ?? c.get('chainId')
1570
+ const client = c.get('getClient')(chainId)
1571
+ const amount = BigInt(request.amount)
1572
+ const slippageBps = BigInt(request.slippageBps)
1573
+
1574
+ c.header('Cache-Control', 'no-store')
1575
+ try {
1576
+ // Value quoted amounts via their curated display currency; snapshot
1577
+ // or rate failures degrade the values to `null`, never the quote.
1578
+ const denomination = options.denomination ?? 'USD'
1579
+ const snapshot = await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
1580
+ const rates = await Valuation.ratesFor(c, {
1581
+ currencies: snapshot
1582
+ ? [request.destinationToken, request.sourceToken].flatMap((address) => {
1583
+ const held = snapshot.byAddress.get(address.toLowerCase())?.currency
1584
+ return held === undefined ? [] : [held]
1585
+ })
1586
+ : [],
1587
+ denomination,
1588
+ oracle,
1589
+ })
1590
+ const valued = (
1591
+ baseUnits: bigint | string,
1592
+ address: string,
1593
+ token: { currency: string; decimals: number },
1594
+ ) => ({
1595
+ ...Value.tokenAmount({
1596
+ baseUnits,
1597
+ currency: token.currency,
1598
+ decimals: token.decimals,
1599
+ }),
1600
+ valuation: Valuation.valuationFor({
1601
+ amount: BigInt(baseUnits),
1602
+ denomination,
1603
+ rates,
1604
+ token: snapshot?.byAddress.get(address.toLowerCase()),
1605
+ }),
1606
+ })
1607
+
1608
+ if (request.mode === 'exactSource') {
1609
+ const destinationAmount = await Actions.dex.getSellQuote(client, {
1610
+ amountIn: amount,
1611
+ tokenIn: request.sourceToken,
1612
+ tokenOut: request.destinationToken,
1613
+ })
1614
+ if (destinationAmount === 0n) return quoteNotAvailable(c)
1615
+
1616
+ const minimumAmount =
1617
+ (destinationAmount * (slippageDenominator - slippageBps) + slippageDenominator - 1n) /
1618
+ slippageDenominator
1619
+ const approve = Actions.token.approve.call(client, {
1620
+ amount,
1621
+ spender: Addresses.stablecoinDex,
1622
+ token: request.sourceToken,
1623
+ })
1624
+ const swap = Actions.dex.sell.call({
1625
+ amountIn: amount,
1626
+ minAmountOut: minimumAmount,
1627
+ tokenIn: request.sourceToken,
1628
+ tokenOut: request.destinationToken,
1629
+ })
1630
+ const tokensByAddress = await resolveTokensByAddress(c, {
1631
+ addresses: [request.destinationToken, request.sourceToken],
1632
+ chainId,
1633
+ include: options.include,
1634
+ })
1635
+ const destinationToken = tokensByAddress.get(request.destinationToken)
1636
+ const sourceToken = tokensByAddress.get(request.sourceToken)
1637
+ if (!destinationToken || !sourceToken)
1638
+ throw new Error('Unable to resolve exchange quote token metadata')
1639
+
1640
+ return c.json(
1641
+ Response.validated(schema.createQuote.Response, {
1642
+ destinationAmount: valued(
1643
+ destinationAmount,
1644
+ request.destinationToken,
1645
+ destinationToken,
1646
+ ),
1647
+ destinationAmountMin: valued(
1648
+ minimumAmount,
1649
+ request.destinationToken,
1650
+ destinationToken,
1651
+ ),
1652
+ destinationToken: {
1653
+ ...destinationToken,
1654
+ address: request.destinationToken,
1655
+ },
1656
+ ...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
1657
+ mode: request.mode,
1658
+ sourceAmount: valued(request.amount, request.sourceToken, sourceToken),
1659
+ sourceToken: {
1660
+ ...sourceToken,
1661
+ address: request.sourceToken,
1662
+ },
1663
+ transaction: {
1664
+ calls: [
1665
+ { data: approve.data, to: approve.to, value: '0x0' },
1666
+ { data: swap.data, to: swap.to, value: '0x0' },
1667
+ ],
1668
+ chainId,
1669
+ },
1670
+ }),
1671
+ 200,
1672
+ )
1673
+ }
1674
+
1675
+ const sourceAmount = await Actions.dex.getBuyQuote(client, {
1676
+ amountOut: amount,
1677
+ tokenIn: request.sourceToken,
1678
+ tokenOut: request.destinationToken,
1679
+ })
1680
+ if (sourceAmount === 0n) return quoteNotAvailable(c)
1681
+
1682
+ const maximumAmount =
1683
+ (sourceAmount * (slippageDenominator + slippageBps)) / slippageDenominator
1684
+ if (maximumAmount > maxSwapAmount)
1685
+ return Response.error(c, {
1686
+ code: 'quote_amount_out_of_range',
1687
+ message: 'Slippage-adjusted source amount exceeds the native DEX limit',
1688
+ status: 400,
1689
+ })
1690
+
1691
+ const approve = Actions.token.approve.call(client, {
1692
+ amount: maximumAmount,
1693
+ spender: Addresses.stablecoinDex,
1694
+ token: request.sourceToken,
1695
+ })
1696
+ const swap = Actions.dex.buy.call({
1697
+ amountOut: amount,
1698
+ maxAmountIn: maximumAmount,
1699
+ tokenIn: request.sourceToken,
1700
+ tokenOut: request.destinationToken,
1701
+ })
1702
+ const tokensByAddress = await resolveTokensByAddress(c, {
1703
+ addresses: [request.destinationToken, request.sourceToken],
1704
+ chainId,
1705
+ include: options.include,
1706
+ })
1707
+ const destinationToken = tokensByAddress.get(request.destinationToken)
1708
+ const sourceToken = tokensByAddress.get(request.sourceToken)
1709
+ if (!destinationToken || !sourceToken)
1710
+ throw new Error('Unable to resolve exchange quote token metadata')
1711
+
1712
+ return c.json(
1713
+ Response.validated(schema.createQuote.Response, {
1714
+ destinationAmount: valued(request.amount, request.destinationToken, destinationToken),
1715
+ destinationToken: {
1716
+ ...destinationToken,
1717
+ address: request.destinationToken,
1718
+ },
1719
+ ...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
1720
+ mode: request.mode,
1721
+ sourceAmount: valued(sourceAmount, request.sourceToken, sourceToken),
1722
+ sourceAmountMax: valued(maximumAmount, request.sourceToken, sourceToken),
1723
+ sourceToken: {
1724
+ ...sourceToken,
1725
+ address: request.sourceToken,
1726
+ },
1727
+ transaction: {
1728
+ calls: [
1729
+ { data: approve.data, to: approve.to, value: '0x0' },
1730
+ { data: swap.data, to: swap.to, value: '0x0' },
1731
+ ],
1732
+ chainId,
1733
+ },
1734
+ }),
1735
+ 200,
1736
+ )
1737
+ } catch (cause) {
1738
+ if (request.mode === 'exactSource' && isQuoteAmountOutOfRange(cause))
1739
+ return Response.error(c, {
1740
+ code: 'quote_amount_out_of_range',
1741
+ message: 'Quote amount exceeds the native DEX limit',
1742
+ status: 400,
1743
+ })
1744
+ if (isQuoteUnavailable(cause)) return quoteNotAvailable(c)
1745
+ if (cause instanceof Valuation.UnsupportedDenominationError)
1746
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
1747
+ return Response.upstream(c, cause)
1748
+ }
1749
+ },
1750
+ )
1314
1751
  .get(
1315
1752
  '/v1/exchange/swaps',
1316
1753
  Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
@@ -1324,7 +1761,7 @@ export function exchanges() {
1324
1761
  operationId: 'getSwaps',
1325
1762
  responses: OpenApi.responses({
1326
1763
  errors: {
1327
- 502: 'The indexer could not serve the exchange data right now.',
1764
+ 502: 'The indexer or Tempo RPC could not serve the exchange data right now.',
1328
1765
  },
1329
1766
  success: {
1330
1767
  description: 'A page of swaps from Tempo’s built-in stablecoin exchange.',
@@ -1335,11 +1772,10 @@ export function exchanges() {
1335
1772
  tags: ['Exchange'],
1336
1773
  }),
1337
1774
  Cache.response<App.Environment>({
1338
- // Exchanges are a newest-first feed; refresh quickly. The `v2` cache
1339
- // segment isolates the swap-level row shape from previously cached
1340
- // per-fill pages (cursor pages cache as `immutable`).
1775
+ // Exchanges are a newest-first feed; refresh quickly. The `v3` cache
1776
+ // segment isolates this response shape from earlier swap pages.
1341
1777
  cacheControl: Cache.policies.feed,
1342
- name: 'tempo-api:exchange:v2:swaps',
1778
+ name: 'tempo-api:exchange:v3:swaps',
1343
1779
  key: (c) => Cache.urlKey(c, schema.getSwaps.Query),
1344
1780
  }),
1345
1781
  async (c) => {
@@ -1353,10 +1789,8 @@ export function exchanges() {
1353
1789
  const options = c.req.valid('query')
1354
1790
  const chainId = options.chainId ?? c.get('chainId')
1355
1791
 
1356
- // Cursor pages anchor below the head and are effectively immutable, so
1357
- // upgrade their cache policy; the head page keeps the route's `feed`
1358
- // default.
1359
- if (options.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
1792
+ // Cursor pages carry current valuations and curated verification data.
1793
+ if (options.cursor !== undefined) Cache.setPolicy(c, Cache.policies.metadata)
1360
1794
 
1361
1795
  try {
1362
1796
  // The pair index (base address → pair) resolves each fill's book
@@ -1380,43 +1814,94 @@ export function exchanges() {
1380
1814
  transactionHash: options.transactionHash,
1381
1815
  })
1382
1816
 
1383
- // Token metadata is opt-in. Collect every token address across the
1384
- // page (`route` spans each swap's source, intermediates, and
1385
- // destination a superset of every fill side) and resolve each
1386
- // once, then spread it onto the swap-level sides and every fill
1387
- // side. Resolution is best-effort: a token whose metadata is
1388
- // unavailable keeps just `address`/`amount`.
1389
- let data = page.data
1390
- if (options.include.includes('tokens') && data.length > 0) {
1391
- const tokensByAddress = await resolveTokensByAddress(c, {
1392
- addresses: data.flatMap((swap) => swap.route),
1393
- chainId,
1394
- })
1395
- type Side = (typeof data)[number]['sourceToken']
1396
- const spread = <side extends Side>(side: side): side => {
1397
- const token = tokensByAddress.get(side.address)
1398
- return token ? { ...side, ...token } : side
1817
+ // `route` spans every swap and fill side, so one lookup resolves each
1818
+ // token once for required RPC metadata and requested curated fields.
1819
+ const tokensByAddress = await resolveTokensByAddress(c, {
1820
+ addresses: page.data.flatMap((swap) => swap.route),
1821
+ chainId,
1822
+ include: options.include,
1823
+ })
1824
+ // Value amounts via their curated display currency; snapshot or rate
1825
+ // failures degrade the values to `null`, never the feed.
1826
+ const denomination = options.denomination ?? 'USD'
1827
+ const snapshot =
1828
+ page.data.length > 0
1829
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
1830
+ : undefined
1831
+ const rates = await Valuation.ratesFor(c, {
1832
+ currencies: snapshot
1833
+ ? page.data.flatMap((swap) =>
1834
+ swap.route.flatMap((address) => {
1835
+ const held = snapshot.byAddress.get(address.toLowerCase())?.currency
1836
+ return held === undefined ? [] : [held]
1837
+ }),
1838
+ )
1839
+ : [],
1840
+ denomination,
1841
+ oracle,
1842
+ })
1843
+ const enrichSide = (side: getSwaps.Side) => {
1844
+ const token = tokensByAddress.get(side.address)
1845
+ if (!token) throw new Error(`Token metadata unavailable for ${side.address}`)
1846
+ return {
1847
+ amount: {
1848
+ ...Value.tokenAmount({
1849
+ baseUnits: side.amount,
1850
+ currency: token.currency,
1851
+ decimals: token.decimals,
1852
+ }),
1853
+ valuation: Valuation.valuationFor({
1854
+ amount: BigInt(side.amount),
1855
+ denomination,
1856
+ rates,
1857
+ token: snapshot?.byAddress.get(side.address.toLowerCase()),
1858
+ }),
1859
+ },
1860
+ token: {
1861
+ address: side.address,
1862
+ currency: token.currency,
1863
+ decimals: token.decimals,
1864
+ ...(token.logoUri !== undefined ? { logoUri: token.logoUri } : {}),
1865
+ name: token.name,
1866
+ symbol: token.symbol,
1867
+ ...(token.verified !== undefined ? { verified: token.verified } : {}),
1868
+ },
1399
1869
  }
1400
- data = data.map((swap) => ({
1401
- ...swap,
1402
- destinationToken: spread(swap.destinationToken),
1403
- fills: swap.fills.map((fill) => ({
1404
- ...fill,
1405
- destinationToken: spread(fill.destinationToken),
1406
- sourceToken: spread(fill.sourceToken),
1407
- })),
1408
- sourceToken: spread(swap.sourceToken),
1409
- }))
1410
1870
  }
1871
+ const data = page.data.map((swap) => {
1872
+ const destination = enrichSide(swap.destinationToken)
1873
+ const source = enrichSide(swap.sourceToken)
1874
+ return {
1875
+ ...swap,
1876
+ destinationAmount: destination.amount,
1877
+ destinationToken: destination.token,
1878
+ fills: swap.fills.map((fill) => {
1879
+ const destination_fill = enrichSide(fill.destinationToken)
1880
+ const source_fill = enrichSide(fill.sourceToken)
1881
+ return {
1882
+ ...fill,
1883
+ destinationAmount: destination_fill.amount,
1884
+ destinationToken: destination_fill.token,
1885
+ sourceAmount: source_fill.amount,
1886
+ sourceToken: source_fill.token,
1887
+ }
1888
+ }),
1889
+ sourceAmount: source.amount,
1890
+ sourceToken: source.token,
1891
+ }
1892
+ })
1411
1893
 
1412
1894
  return c.json(
1413
1895
  Response.validated(schema.getSwaps.Response, {
1414
1896
  data,
1897
+ ...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
1415
1898
  nextCursor: page.nextCursor,
1416
1899
  }),
1417
1900
  200,
1418
1901
  )
1419
1902
  } catch (cause) {
1903
+ if (cause instanceof Valuation.UnsupportedDenominationError)
1904
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 })
1420
1905
  return Response.upstream(c, cause)
1421
1906
  }
1422
1907
  },
@@ -1500,6 +1985,7 @@ export function exchanges() {
1500
1985
  const tokensByAddress = await resolveTokensByAddress(c, {
1501
1986
  addresses: data.flatMap((pair) => [pair.base.address, pair.quote.address]),
1502
1987
  chainId,
1988
+ include: tokenFields,
1503
1989
  })
1504
1990
  data = data.map((pair) => {
1505
1991
  const base = tokensByAddress.get(pair.base.address)
@@ -1596,6 +2082,7 @@ export function exchanges() {
1596
2082
  const tokensByAddress = await resolveTokensByAddress(c, {
1597
2083
  addresses: [target.base, target.quote],
1598
2084
  chainId,
2085
+ include: tokenFields,
1599
2086
  })
1600
2087
  const baseMeta = tokensByAddress.get(target.base)
1601
2088
  const quoteMeta = tokensByAddress.get(target.quote)
@@ -1704,6 +2191,7 @@ export function exchanges() {
1704
2191
  const tokensByAddress = await resolveTokensByAddress(c, {
1705
2192
  addresses: data.flatMap((o) => [o.pair.base.address, o.pair.quote.address]),
1706
2193
  chainId,
2194
+ include: tokenFields,
1707
2195
  })
1708
2196
  data = data.map((o) => {
1709
2197
  const base = tokensByAddress.get(o.pair.base.address)
@@ -2027,6 +2515,7 @@ export function exchanges() {
2027
2515
  const tokensByAddress = await resolveTokensByAddress(c, {
2028
2516
  addresses: [target.base, target.quote],
2029
2517
  chainId,
2518
+ include: tokenFields,
2030
2519
  })
2031
2520
  const baseMeta = tokensByAddress.get(target.base)
2032
2521
  const quoteMeta = tokensByAddress.get(target.quote)
@@ -2128,6 +2617,7 @@ export function exchanges() {
2128
2617
  const tokensByAddress = await resolveTokensByAddress(c, {
2129
2618
  addresses: [target.base, target.quote],
2130
2619
  chainId,
2620
+ include: tokenFields,
2131
2621
  })
2132
2622
  const baseMeta = tokensByAddress.get(target.base)
2133
2623
  const quoteMeta = tokensByAddress.get(target.quote)
@@ -2146,6 +2636,14 @@ export function exchanges() {
2146
2636
  )
2147
2637
  }
2148
2638
 
2639
+ export declare namespace exchanges {
2640
+ /** Options for the exchange handlers. */
2641
+ type Options = {
2642
+ /** FX configuration backing amount valuation. */
2643
+ fx?: Valuation.addresses.Fx | undefined
2644
+ }
2645
+ }
2646
+
2149
2647
  // Hard cap on the curated pair index. Pair creation is rare (one event per
2150
2648
  // pair lifetime), so a few thousand bounds today's set comfortably; raise this
2151
2649
  // if the index ever exhausts.
@@ -2169,34 +2667,62 @@ function pairNotFoundError(
2169
2667
  return Response.error(c, { code: 'pair_not_found', message, status: 404 })
2170
2668
  }
2171
2669
 
2172
- /**
2173
- * Batch-resolves token metadata for a set of addresses, sharing one
2174
- * verified-token snapshot and one created-at lookup across the whole set.
2175
- * Best-effort: addresses whose metadata cannot be resolved are absent from
2176
- * the returned map, so callers keep their bare `{ address }` shapes. Shared
2177
- * by every `include=tokens` embed in this module.
2178
- */
2670
+ /** Resolves RPC token metadata and explicitly requested enrichments. Unresolvable addresses are omitted. */
2179
2671
  async function resolveTokensByAddress(
2180
2672
  c: Context<App.Environment>,
2181
2673
  options: {
2182
2674
  addresses: readonly z.output<typeof Schema.Address>[]
2183
2675
  chainId: z.output<typeof Schema.ChainId>
2676
+ include: readonly (typeof tokenFields)[number][]
2184
2677
  },
2185
2678
  ) {
2186
- const { addresses, chainId } = options
2187
- type Token = Awaited<ReturnType<typeof Tokens.resolveToken>>
2679
+ const { addresses, chainId, include } = options
2680
+ const includeLogoUri = include.includes('token.logoUri')
2681
+ const includeVerified = include.includes('token.verified')
2682
+ type Token = Omit<z.output<typeof Tokens.schema.Token>, 'verified'> & {
2683
+ verified?: boolean | undefined
2684
+ }
2188
2685
  const tokensByAddress = new Map<string, Token>()
2189
2686
  const uniqueTokens = [...new Set(addresses)]
2190
2687
  if (uniqueTokens.length === 0) return tokensByAddress
2688
+ const snapshot =
2689
+ includeLogoUri || includeVerified
2690
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
2691
+ : undefined
2191
2692
 
2192
- const snapshot = await VerifiedTokens.snapshot(c, chainId)
2193
2693
  const resolved = await Timing.time(c, 'tokens', () =>
2194
2694
  Promise.all(
2195
- uniqueTokens.map((token) =>
2196
- Tokens.resolveToken(c, { address: token, chainId, snapshot })
2197
- .then((metadata) => [token, metadata] as const)
2198
- .catch(() => [token, undefined] as const),
2199
- ),
2695
+ uniqueTokens.map(async (token) => {
2696
+ try {
2697
+ const [logoUri, metadata] = await Promise.all([
2698
+ includeLogoUri
2699
+ ? Timing.time(c, 'token_logo', () =>
2700
+ Tokens.getTokenLogo(c, { address: token, chainId }),
2701
+ ).catch(() => undefined)
2702
+ : undefined,
2703
+ Timing.time(c, 'token_metadata', () =>
2704
+ Tokens.getTokenMetadata(c, { address: token, chainId }),
2705
+ ),
2706
+ ])
2707
+ return [
2708
+ token,
2709
+ {
2710
+ address: token,
2711
+ currency: metadata.currency,
2712
+ decimals: metadata.decimals,
2713
+ id: token,
2714
+ logoUri: includeLogoUri
2715
+ ? (logoUri ?? snapshot?.byAddress.get(token)?.logoUri ?? metadata.logoUri)
2716
+ : undefined,
2717
+ name: metadata.name,
2718
+ symbol: metadata.symbol,
2719
+ verified: includeVerified ? snapshot?.byAddress.has(token) : undefined,
2720
+ },
2721
+ ] as const
2722
+ } catch {
2723
+ return [token, undefined] as const
2724
+ }
2725
+ }),
2200
2726
  ),
2201
2727
  )
2202
2728
  for (const [token, metadata] of resolved) if (metadata) tokensByAddress.set(token, metadata)
@@ -2646,7 +3172,7 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
2646
3172
  })
2647
3173
  }
2648
3174
 
2649
- type Candidate = { group: parseGroupRow.Group; swap: z.output<typeof schema.getSwaps.Swap> }
3175
+ type Candidate = { group: parseGroupRow.Group; swap: getSwaps.Swap }
2650
3176
 
2651
3177
  // A row's keyset position is `(block, group first_idx, its first fill
2652
3178
  // logIndex)`; rows at or before the active cursor's row anchor were already
@@ -2949,6 +3475,36 @@ async function getSwaps(c: Context<App.Environment>, options: getSwaps.Options)
2949
3475
  }
2950
3476
 
2951
3477
  declare namespace getSwaps {
3478
+ /** Swap side before RPC metadata and structured amount enrichment. */
3479
+ type Side = {
3480
+ address: z.output<typeof Schema.TokenAddress>
3481
+ amount: z.output<typeof Schema.DecimalString>
3482
+ }
3483
+
3484
+ /** Maker fill before RPC metadata and structured amount enrichment. */
3485
+ type Fill = Omit<
3486
+ z.output<typeof schema.getSwaps.Fill>,
3487
+ 'destinationAmount' | 'destinationToken' | 'sourceAmount' | 'sourceToken'
3488
+ > & {
3489
+ /** Destination token and raw base-unit amount. */
3490
+ destinationToken: Side
3491
+ /** Source token and raw base-unit amount. */
3492
+ sourceToken: Side
3493
+ }
3494
+
3495
+ /** Logical swap before RPC metadata and structured amount enrichment. */
3496
+ type Swap = Omit<
3497
+ z.output<typeof schema.getSwaps.Swap>,
3498
+ 'destinationAmount' | 'destinationToken' | 'fills' | 'sourceAmount' | 'sourceToken'
3499
+ > & {
3500
+ /** Destination token and raw base-unit amount. */
3501
+ destinationToken: Side
3502
+ /** Maker fills that compose this swap. */
3503
+ fills: Fill[]
3504
+ /** Source token and raw base-unit amount. */
3505
+ sourceToken: Side
3506
+ }
3507
+
2952
3508
  /** Options for the swap page query. */
2953
3509
  type Options = {
2954
3510
  chainId: z.output<typeof Schema.ChainId>
@@ -3317,7 +3873,7 @@ declare namespace resolveSwapModes {
3317
3873
  type Options = {
3318
3874
  chainId: z.output<typeof Schema.ChainId>
3319
3875
  /** The final page's swap rows; `mode` is mutated in place. */
3320
- swaps: z.output<typeof schema.getSwaps.Swap>[]
3876
+ swaps: getSwaps.Swap[]
3321
3877
  }
3322
3878
  }
3323
3879
 
@@ -3392,13 +3948,13 @@ declare namespace resolveBookQuotes {
3392
3948
  function buildSwapRows(
3393
3949
  group: parseGroupRow.Group,
3394
3950
  options: buildSwapRows.Options,
3395
- ): z.output<typeof schema.getSwaps.Swap>[] {
3951
+ ): getSwaps.Swap[] {
3396
3952
  const { order, quoteByBase, states } = options
3397
3953
  const priceScale = BigInt(Tick.priceScale)
3398
3954
 
3399
3955
  type Oriented = {
3400
3956
  destination: { address: z.output<typeof Schema.TokenAddress>; amount: bigint }
3401
- fill: z.output<typeof schema.getSwaps.Fill>
3957
+ fill: getSwaps.Fill
3402
3958
  source: { address: z.output<typeof Schema.TokenAddress>; amount: bigint }
3403
3959
  }
3404
3960
  const oriented: Oriented[] = []
@@ -3533,8 +4089,8 @@ declare namespace buildSwapRows {
3533
4089
  destination: z.output<typeof Schema.TokenAddress>
3534
4090
  /** Total received on this leg, in base units. */
3535
4091
  destinationAmount: bigint
3536
- /** The leg's fills (public shape). */
3537
- fills: z.output<typeof schema.getSwaps.Fill>[]
4092
+ /** The leg's fills before metadata enrichment. */
4093
+ fills: getSwaps.Fill[]
3538
4094
  /** Token the taker sent on this leg. */
3539
4095
  source: z.output<typeof Schema.TokenAddress>
3540
4096
  /** Total sent on this leg, in base units. */
@@ -4595,6 +5151,37 @@ function isOrderDoesNotExist(error: unknown): boolean {
4595
5151
  )
4596
5152
  }
4597
5153
 
5154
+ /** Returns the native DEX 404 used when no executable quote is available. */
5155
+ function quoteNotAvailable(c: Context<App.Environment>) {
5156
+ return Response.error(c, {
5157
+ code: 'quote_not_available',
5158
+ message: 'No executable quote is available on Tempo’s native DEX',
5159
+ status: 404,
5160
+ })
5161
+ }
5162
+
5163
+ /** Distinguishes native DEX route failures from RPC transport failures. */
5164
+ function isQuoteUnavailable(error: unknown): boolean {
5165
+ if (!(error instanceof BaseError)) return false
5166
+ const revert = error.walk((e) => e instanceof ContractFunctionRevertedError)
5167
+ if (!(revert instanceof ContractFunctionRevertedError)) return false
5168
+ return (
5169
+ revert.raw === contractPausedSelector ||
5170
+ ['InsufficientLiquidity', 'InvalidBaseToken', 'InvalidToken', 'PairDoesNotExist'].includes(
5171
+ revert.data?.errorName ?? '',
5172
+ )
5173
+ )
5174
+ }
5175
+
5176
+ /** Detects uint128 arithmetic overflow from exact-source DEX quote calls. */
5177
+ function isQuoteAmountOutOfRange(error: unknown): boolean {
5178
+ if (!(error instanceof BaseError)) return false
5179
+ const revert = error.walk((e) => e instanceof ContractFunctionRevertedError)
5180
+ if (!(revert instanceof ContractFunctionRevertedError)) return false
5181
+ const data = revert.data
5182
+ return data?.errorName === 'Panic' && data.args?.[0] === 0x11n
5183
+ }
5184
+
4598
5185
  /**
4599
5186
  * Resolves a single page of `OrderFilled` events for one order. Pagination is
4600
5187
  * keyset on `(block_num, log_idx)`, which is unique and append-only for the