tapimo 0.5.3 → 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 (289) 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 +526 -1
  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 +872 -251
  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/rpc.d.ts +31 -31
  64. package/dist/apps/data/routes/tokens.d.ts +166 -173
  65. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  66. package/dist/apps/data/routes/tokens.js +14 -12
  67. package/dist/apps/data/routes/tokens.js.map +1 -1
  68. package/dist/apps/data/routes/transactions.d.ts +277 -536
  69. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  70. package/dist/apps/data/routes/transactions.js +96 -77
  71. package/dist/apps/data/routes/transactions.js.map +1 -1
  72. package/dist/apps/data/routes/transfers.d.ts +82 -17
  73. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  74. package/dist/apps/data/routes/transfers.js +70 -13
  75. package/dist/apps/data/routes/transfers.js.map +1 -1
  76. package/dist/apps/data/routes/valuation.d.ts +307 -0
  77. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  78. package/dist/apps/data/routes/valuation.js +292 -0
  79. package/dist/apps/data/routes/valuation.js.map +1 -0
  80. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  81. package/dist/apps/data/routes/verified-tokens.js +7 -5
  82. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  83. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  84. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  85. package/dist/apps/data/routes/webhooks.js +124 -25
  86. package/dist/apps/data/routes/webhooks.js.map +1 -1
  87. package/dist/apps/index.d.ts +1 -1
  88. package/dist/apps/index.d.ts.map +1 -1
  89. package/dist/apps/index.js +1 -1
  90. package/dist/apps/index.js.map +1 -1
  91. package/dist/apps/management/App.d.ts +8606 -1681
  92. package/dist/apps/management/App.d.ts.map +1 -1
  93. package/dist/apps/management/App.js +12 -1
  94. package/dist/apps/management/App.js.map +1 -1
  95. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  96. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  97. package/dist/apps/management/routes/api-keys.js +5 -3
  98. package/dist/apps/management/routes/api-keys.js.map +1 -1
  99. package/dist/apps/management/routes/billing.d.ts +88 -48
  100. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  101. package/dist/apps/management/routes/billing.js +10 -2
  102. package/dist/apps/management/routes/billing.js.map +1 -1
  103. package/dist/apps/management/routes/faucet.d.ts +218 -0
  104. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  105. package/dist/apps/management/routes/faucet.js +139 -0
  106. package/dist/apps/management/routes/faucet.js.map +1 -0
  107. package/dist/apps/management/routes/invitations.d.ts +121 -121
  108. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  109. package/dist/apps/management/routes/invitations.js +3 -0
  110. package/dist/apps/management/routes/invitations.js.map +1 -1
  111. package/dist/apps/management/routes/invite-links.d.ts +49 -20
  112. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  113. package/dist/apps/management/routes/invite-links.js +26 -1
  114. package/dist/apps/management/routes/invite-links.js.map +1 -1
  115. package/dist/apps/management/routes/me.d.ts +2 -0
  116. package/dist/apps/management/routes/me.d.ts.map +1 -1
  117. package/dist/apps/management/routes/me.js +4 -0
  118. package/dist/apps/management/routes/me.js.map +1 -1
  119. package/dist/apps/management/routes/members.d.ts +49 -49
  120. package/dist/apps/management/routes/orgs.d.ts +91 -91
  121. package/dist/apps/management/routes/projects.d.ts +44 -44
  122. package/dist/apps/management/routes/usage.d.ts +35 -32
  123. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  124. package/dist/apps/management/routes/usage.js +4 -0
  125. package/dist/apps/management/routes/usage.js.map +1 -1
  126. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  127. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  128. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  129. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  130. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  131. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  132. package/dist/apps/management/routes/webhooks.js +573 -0
  133. package/dist/apps/management/routes/webhooks.js.map +1 -0
  134. package/dist/db/Db.d.ts +4 -0
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +83 -0
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +41 -0
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +45 -0
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/earlyAccess.d.ts +50 -0
  143. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  144. package/dist/db/tables/earlyAccess.js +63 -0
  145. package/dist/db/tables/earlyAccess.js.map +1 -0
  146. package/dist/db/tables/invitations.d.ts +12 -1
  147. package/dist/db/tables/invitations.d.ts.map +1 -1
  148. package/dist/db/tables/invitations.js +23 -0
  149. package/dist/db/tables/invitations.js.map +1 -1
  150. package/dist/db/tables/inviteLinks.d.ts +10 -2
  151. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  152. package/dist/db/tables/inviteLinks.js +10 -1
  153. package/dist/db/tables/inviteLinks.js.map +1 -1
  154. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  155. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  156. package/dist/db/tables/sponsoredTransactions.js +2 -0
  157. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  158. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  159. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  160. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  161. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  162. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  163. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  164. package/dist/db/tables/webhookSubscriptions.js +16 -0
  165. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  166. package/dist/internal/Auth.d.ts +43 -2
  167. package/dist/internal/Auth.d.ts.map +1 -1
  168. package/dist/internal/Auth.js +60 -45
  169. package/dist/internal/Auth.js.map +1 -1
  170. package/dist/internal/FundingQuotes.d.ts +69 -15
  171. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  172. package/dist/internal/FundingQuotes.js +74 -25
  173. package/dist/internal/FundingQuotes.js.map +1 -1
  174. package/dist/internal/Log.d.ts +19 -1
  175. package/dist/internal/Log.d.ts.map +1 -1
  176. package/dist/internal/Log.js +2 -0
  177. package/dist/internal/Log.js.map +1 -1
  178. package/dist/internal/MetricSink.d.ts.map +1 -1
  179. package/dist/internal/MetricSink.js +9 -0
  180. package/dist/internal/MetricSink.js.map +1 -1
  181. package/dist/internal/OpenApi.d.ts +2 -2
  182. package/dist/internal/OpenApi.d.ts.map +1 -1
  183. package/dist/internal/OpenApi.js +3 -0
  184. package/dist/internal/OpenApi.js.map +1 -1
  185. package/dist/internal/Schema.d.ts +14 -5
  186. package/dist/internal/Schema.d.ts.map +1 -1
  187. package/dist/internal/Schema.js +23 -5
  188. package/dist/internal/Schema.js.map +1 -1
  189. package/dist/internal/Value.d.ts +18 -0
  190. package/dist/internal/Value.d.ts.map +1 -1
  191. package/dist/internal/Value.js +11 -1
  192. package/dist/internal/Value.js.map +1 -1
  193. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  194. package/dist/internal/VerifiedTokens.js +10 -1
  195. package/dist/internal/VerifiedTokens.js.map +1 -1
  196. package/dist/internal/Viem.d.ts +1 -1
  197. package/dist/internal/Webhooks.d.ts +24 -3
  198. package/dist/internal/Webhooks.d.ts.map +1 -1
  199. package/dist/internal/Webhooks.js +34 -8
  200. package/dist/internal/Webhooks.js.map +1 -1
  201. package/package.json +1 -1
  202. package/src/App.ts +9 -1
  203. package/src/Client.test-d.ts +638 -6
  204. package/src/admin/App.ts +4 -0
  205. package/src/admin/apps/early-access.test.ts +125 -0
  206. package/src/admin/apps/early-access.ts +175 -0
  207. package/src/admin/apps/verified-tokens.test.ts +201 -0
  208. package/src/admin/apps/verified-tokens.ts +532 -286
  209. package/src/admin/ui/plugin.ts +2 -1
  210. package/src/admin/ui/src/lib/early-access.ts +63 -0
  211. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  212. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  213. package/src/admin/ui/src/routes/__root.tsx +2 -1
  214. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  215. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  216. package/src/admin/ui.generated.ts +48 -33
  217. package/src/apps/data/App.ts +22 -14
  218. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  219. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  220. package/src/apps/data/FxOracle.test.ts +118 -0
  221. package/src/apps/data/FxOracle.ts +153 -0
  222. package/src/apps/data/index.ts +1 -0
  223. package/src/apps/data/routes/activities.test.ts +173 -3
  224. package/src/apps/data/routes/activities.ts +403 -132
  225. package/src/apps/data/routes/addresses.ts +19 -9
  226. package/src/apps/data/routes/balances.test.ts +144 -11
  227. package/src/apps/data/routes/balances.ts +140 -51
  228. package/src/apps/data/routes/exchanges.test.ts +540 -9
  229. package/src/apps/data/routes/exchanges.ts +690 -103
  230. package/src/apps/data/routes/funding.test.ts +84 -0
  231. package/src/apps/data/routes/funding.ts +38 -6
  232. package/src/apps/data/routes/receipts.test.ts +125 -23
  233. package/src/apps/data/routes/receipts.ts +205 -100
  234. package/src/apps/data/routes/tokens.test.ts +43 -0
  235. package/src/apps/data/routes/tokens.ts +27 -12
  236. package/src/apps/data/routes/transactions.test.ts +72 -19
  237. package/src/apps/data/routes/transactions.ts +162 -92
  238. package/src/apps/data/routes/transfers.test.ts +146 -4
  239. package/src/apps/data/routes/transfers.ts +91 -13
  240. package/src/apps/data/routes/valuation.test.ts +422 -0
  241. package/src/apps/data/routes/valuation.ts +423 -0
  242. package/src/apps/data/routes/verified-tokens.ts +14 -2
  243. package/src/apps/data/routes/webhooks.test.ts +56 -8
  244. package/src/apps/data/routes/webhooks.ts +144 -27
  245. package/src/apps/index.ts +1 -1
  246. package/src/apps/management/App.ts +13 -0
  247. package/src/apps/management/routes/api-keys.test.ts +87 -0
  248. package/src/apps/management/routes/api-keys.ts +4 -3
  249. package/src/apps/management/routes/billing.test.ts +112 -1
  250. package/src/apps/management/routes/billing.ts +7 -2
  251. package/src/apps/management/routes/faucet.test.ts +79 -0
  252. package/src/apps/management/routes/faucet.ts +185 -0
  253. package/src/apps/management/routes/invitations.test.ts +172 -0
  254. package/src/apps/management/routes/invitations.ts +3 -0
  255. package/src/apps/management/routes/invite-links.test.ts +117 -0
  256. package/src/apps/management/routes/invite-links.ts +43 -1
  257. package/src/apps/management/routes/me.test.ts +140 -88
  258. package/src/apps/management/routes/me.ts +6 -0
  259. package/src/apps/management/routes/usage.test.ts +6 -0
  260. package/src/apps/management/routes/usage.ts +7 -0
  261. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  262. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  263. package/src/apps/management/routes/webhooks.test.ts +426 -0
  264. package/src/apps/management/routes/webhooks.ts +672 -0
  265. package/src/db/Db.test.ts +19 -0
  266. package/src/db/Db.ts +95 -0
  267. package/src/db/Schema.ts +53 -0
  268. package/src/db/tables/earlyAccess.ts +86 -0
  269. package/src/db/tables/invitations.ts +26 -1
  270. package/src/db/tables/inviteLinks.ts +22 -3
  271. package/src/db/tables/sponsoredTransactions.ts +4 -0
  272. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  273. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  274. package/src/db/tables/webhookSubscriptions.ts +31 -0
  275. package/src/internal/Auth.ts +73 -45
  276. package/src/internal/FundingQuotes.test.ts +143 -1
  277. package/src/internal/FundingQuotes.ts +95 -28
  278. package/src/internal/Log.test-d.ts +21 -0
  279. package/src/internal/Log.test.ts +50 -0
  280. package/src/internal/Log.ts +24 -1
  281. package/src/internal/MetricSink.test.ts +160 -0
  282. package/src/internal/MetricSink.ts +10 -0
  283. package/src/internal/OpenApi.ts +3 -0
  284. package/src/internal/Schema.ts +50 -5
  285. package/src/internal/Value.test.ts +20 -0
  286. package/src/internal/Value.ts +24 -1
  287. package/src/internal/VerifiedTokens.ts +9 -1
  288. package/src/internal/Webhooks.test.ts +140 -0
  289. package/src/internal/Webhooks.ts +53 -9
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono';
2
- import { AbiEvent } from 'ox';
2
+ import { AbiError, AbiEvent } from 'ox';
3
3
  import { BaseError, ContractFunctionRevertedError, decodeFunctionData, parseAbi, } from 'viem';
4
- import { Actions, Addresses, Tick } from 'viem/tempo';
4
+ import { Abis, Actions, Addresses, Tick } from 'viem/tempo';
5
5
  import * as z from 'zod/mini';
6
6
  import * as Auth from '../../../internal/Auth.js';
7
7
  import * as Cache from '../../../internal/Cache.js';
@@ -14,10 +14,13 @@ import * as Timing from '../../../internal/Timing.js';
14
14
  import * as Ttl from '../../../internal/Ttl.js';
15
15
  import * as Value from '../../../internal/Value.js';
16
16
  import * as VerifiedTokens from '../../../internal/VerifiedTokens.js';
17
+ import * as FxOracle from '../FxOracle.js';
17
18
  import * as Tokens from './tokens.js';
19
+ import * as Valuation from './valuation.js';
18
20
  // Stablecoin DEX precompile. Pair lifecycle is announced by `PairCreated` from
19
21
  // this address; the indexer pre-decodes those into the `dex_pairs` table.
20
22
  const stablecoinDex = Addresses.stablecoinDex.toLowerCase();
23
+ const contractPausedSelector = AbiError.getSelector(AbiError.fromAbi(Abis.tip20, 'ContractPaused'));
21
24
  // `OrderCancelled(uint128 orderId)` — emitted when a maker cancels a resting
22
25
  // order. Used by the orders endpoint to exclude cancelled orders from the
23
26
  // resting set.
@@ -53,6 +56,18 @@ const swapAbi = parseAbi([
53
56
  // pathUSD on-chain). Uses a fresh `Schema.tokenAddress(...)` so the example
54
57
  // reaches the pipe input that path-param OpenAPI generation reads from.
55
58
  const exampleBase = '0x20c000000000000000000000b9537d11c60e8b50';
59
+ const exampleSourceToken = '0x20c0000000000000000000008f5425160ebe5525';
60
+ const exampleApprovalCall = Actions.token.approve.call({
61
+ amount: 1000000n,
62
+ spender: Addresses.stablecoinDex,
63
+ token: exampleSourceToken,
64
+ });
65
+ const exampleSwapCall = Actions.dex.sell.call({
66
+ amountIn: 1000000n,
67
+ minAmountOut: 995000n,
68
+ tokenIn: exampleSourceToken,
69
+ tokenOut: exampleBase,
70
+ });
56
71
  /**
57
72
  * Tick spacing enforced by the stablecoin DEX precompile: orders must be
58
73
  * placed at ticks divisible by 10 (a 1 bp price grid). Walking the depth at
@@ -82,6 +97,11 @@ const maxDepthLevels = 200;
82
97
  * namespace because the OHLC `Query` refine references it at validation time.
83
98
  */
84
99
  const maxBuckets = 500;
100
+ /** Largest amount accepted by the DEX precompile's uint128 swap arguments. */
101
+ const maxSwapAmount = 2n ** 128n - 1n;
102
+ /** Basis-point denominator used for caller-selected swap slippage. */
103
+ const slippageDenominator = 10000n;
104
+ const tokenFields = ['token.logoUri', 'token.verified'];
85
105
  /** Converts the OHLC `interval` enum to a millisecond duration. */
86
106
  function intervalMs(interval) {
87
107
  switch (interval) {
@@ -157,6 +177,126 @@ export var schema;
157
177
  .optional(z.boolean())
158
178
  .check(z.describe('Whether Tempo has verified this token metadata.'), z.meta({ examples: [true] })),
159
179
  }), 'One side of a trading pair (with metadata when requested via `include=tokens`).');
180
+ /** A token amount carrying its nominal value in the requested denomination. */
181
+ const ValuedAmount = z.extend(Schema.TokenAmount, {
182
+ valuation: z
183
+ .optional(z.nullable(Valuation.schema.Value))
184
+ .check(z.describe('The amount’s nominal value in the requested `denomination`. `null` when the token is ' +
185
+ 'unverified, its display currency has no rate, or rates were unavailable.')),
186
+ });
187
+ /** Schemas for creating an executable native DEX exchange quote. */
188
+ let createQuote;
189
+ (function (createQuote) {
190
+ const DestinationTokenAddress = Schema.tokenAddress(exampleBase);
191
+ const SourceTokenAddress = Schema.tokenAddress(exampleSourceToken);
192
+ /** Positive token amount accepted by the DEX's uint128 arguments. */
193
+ createQuote.Amount = Schema.DecimalString.check(z.refine((value) => {
194
+ const amount = BigInt(value);
195
+ return amount > 0n && amount <= maxSwapAmount;
196
+ }, { error: 'Amount must be between 1 and the maximum uint128 value.' }), z.describe('Positive token amount in the token’s smallest unit, encoded as a decimal string.'), z.meta({ examples: ['1000000'] }));
197
+ /** Optional token fields that require sources beyond RPC metadata. */
198
+ createQuote.Include = z
199
+ .enum(tokenFields)
200
+ .check(z.describe('Additional token fields to include in the quote response.'));
201
+ /** Parses comma-separated optional token fields. */
202
+ createQuote.includeQuery = Schema.includeQuery(createQuote.Include, 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.');
203
+ /** Query parameters selecting the Tempo chain and optional token fields. */
204
+ createQuote.Query = z
205
+ .strictObject({
206
+ chainId: Schema.ChainIdQuery,
207
+ denomination: Schema.Denomination,
208
+ include: createQuote.includeQuery,
209
+ })
210
+ .check(z.describe('Query parameters for creating a native DEX exchange quote.'));
211
+ /** Quote request with one pinned side and caller-selected slippage. */
212
+ createQuote.Request = z
213
+ .strictObject({
214
+ amount: createQuote.Amount.check(z.describe('Pinned amount in base units: source amount for `exactSource`, destination amount for `exactDestination`.')),
215
+ destinationToken: DestinationTokenAddress.check(z.describe('TIP-20 token the swap receives.')),
216
+ mode: z
217
+ .enum(['exactSource', 'exactDestination'])
218
+ .check(z.describe('Which side of the swap keeps the requested amount exact.'), z.meta({ examples: ['exactSource'] })),
219
+ slippageBps: z
220
+ .number()
221
+ .check(z.int(), z.gte(0), z.lt(10_000), z.describe('Allowed execution slippage in basis points, where 100 is 1%.'), z.meta({ examples: [50] })),
222
+ sourceToken: SourceTokenAddress.check(z.describe('TIP-20 token the swap spends.')),
223
+ })
224
+ .check(z.refine((request) => request.sourceToken !== request.destinationToken, {
225
+ error: 'Source and destination tokens must differ.',
226
+ }), z.describe('Request for an executable quote on Tempo’s native stablecoin DEX.'));
227
+ /** One call in the unsigned Tempo transaction plan. */
228
+ createQuote.Call = z
229
+ .object({
230
+ data: Schema.Hex.check(z.describe('ABI-encoded call data.')),
231
+ to: Schema.Address.check(z.describe('Contract or precompile that receives the call.')),
232
+ value: Schema.Quantity.check(z.describe('Native token value sent with the call; native DEX swap calls use `0x0`.'), z.meta({ examples: ['0x0'] })),
233
+ })
234
+ .check(z.describe('One unsigned call in the Tempo transaction plan.'));
235
+ const QuoteToken = Schema.describe(z.partial(z.omit(Tokens.schema.Token, { id: true }), {
236
+ logoUri: true,
237
+ verified: true,
238
+ }), 'A token referenced by an exchange quote, with RPC metadata and optional enrichments.');
239
+ const DestinationToken = z.extend(QuoteToken, {
240
+ address: DestinationTokenAddress.check(z.describe('TIP-20 token the swap receives.')),
241
+ });
242
+ const SourceToken = z.extend(QuoteToken, {
243
+ address: SourceTokenAddress.check(z.describe('TIP-20 token the swap spends.')),
244
+ });
245
+ const Transaction = z
246
+ .object({
247
+ calls: z.array(createQuote.Call).check(z.length(2), z.describe('Approval followed by the native DEX swap call.'), z.meta({
248
+ examples: [
249
+ [
250
+ {
251
+ data: exampleApprovalCall.data,
252
+ to: exampleApprovalCall.to,
253
+ value: '0x0',
254
+ },
255
+ { data: exampleSwapCall.data, to: exampleSwapCall.to, value: '0x0' },
256
+ ],
257
+ ],
258
+ })),
259
+ chainId: Schema.ChainId.check(z.describe('Tempo chain where these calls execute.')),
260
+ })
261
+ .check(z.describe('Unsigned Tempo transaction plan.'));
262
+ /** Response-level valuation rate provenance. */
263
+ createQuote.Meta = z
264
+ .object({
265
+ pricing: z
266
+ .optional(Valuation.schema.Pricing)
267
+ .check(z.describe('Rate provenance for amount valuations. Present when conversion rates were consulted.')),
268
+ })
269
+ .check(z.describe('Response-level resources attached to this quote.'));
270
+ /** Native DEX quote plus the approval and swap calls needed to execute it. */
271
+ createQuote.Response = z
272
+ .discriminatedUnion('mode', [
273
+ z.object({
274
+ destinationAmount: ValuedAmount.check(z.describe('Quoted destination amount.')),
275
+ destinationAmountMin: ValuedAmount.check(z.describe('Minimum destination amount after slippage.')),
276
+ destinationToken: DestinationToken,
277
+ meta: z.optional(createQuote.Meta).check(z.describe('Valuation rate provenance for this quote.')),
278
+ mode: z
279
+ .literal('exactSource')
280
+ .check(z.describe('The source amount is fixed and the destination has a minimum.'), z.meta({ examples: ['exactSource'] })),
281
+ sourceAmount: ValuedAmount.check(z.describe('Exact source amount.')),
282
+ sourceToken: SourceToken.check(z.describe('Token spent by the swap.')),
283
+ transaction: Transaction,
284
+ }),
285
+ z.object({
286
+ destinationAmount: ValuedAmount.check(z.describe('Exact destination amount.')),
287
+ destinationToken: DestinationToken.check(z.describe('Token received by the swap.')),
288
+ meta: z.optional(createQuote.Meta).check(z.describe('Valuation rate provenance for this quote.')),
289
+ mode: z
290
+ .literal('exactDestination')
291
+ .check(z.describe('The destination amount is fixed and the source has a maximum.'), z.meta({ examples: ['exactDestination'] })),
292
+ sourceAmount: ValuedAmount.check(z.describe('Quoted source amount.')),
293
+ sourceAmountMax: ValuedAmount.check(z.describe('Maximum source amount after slippage.')),
294
+ sourceToken: SourceToken,
295
+ transaction: Transaction,
296
+ }),
297
+ ])
298
+ .check(z.describe('Executable native DEX quote with slippage-protected calls.'));
299
+ })(createQuote = schema.createQuote || (schema.createQuote = {}));
160
300
  /** Schemas for the getPairs operation. */
161
301
  let getPairs;
162
302
  (function (getPairs) {
@@ -255,20 +395,12 @@ export var schema;
255
395
  /** Schemas for the getSwaps operation (the swap feed, optionally pair-scoped). */
256
396
  let getSwaps;
257
397
  (function (getSwaps) {
258
- /**
259
- * Optional related resource that callers opt into via `include`. Currently
260
- * only `tokens` (embeds metadata for both pair tokens on each row's
261
- * `sourceToken`/`destinationToken`).
262
- */
398
+ /** Optional token fields that require sources beyond RPC metadata. */
263
399
  getSwaps.Include = z
264
- .enum(['tokens'])
265
- .check(z.describe('Related resource you can opt into with `include`.'));
266
- /**
267
- * Parses a comma-separated `include` query value into a list of optional
268
- * resources to embed. Extra lookups (e.g. token metadata) only run when
269
- * explicitly requested, keeping the base swap feed fast.
270
- */
271
- getSwaps.includeQuery = Schema.includeQuery(getSwaps.Include, 'Comma-separated optional resources to embed, e.g. `tokens`.');
400
+ .enum(tokenFields)
401
+ .check(z.describe('Additional token fields to include in swap responses.'));
402
+ /** Parses comma-separated optional token fields. */
403
+ getSwaps.includeQuery = Schema.includeQuery(getSwaps.Include, 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.');
272
404
  /** Query parameters for swap feed requests. */
273
405
  getSwaps.Query = z
274
406
  .strictObject({
@@ -276,6 +408,7 @@ export var schema;
276
408
  'blockNumber.to': Schema.blockNumberBound('swaps', 'to'),
277
409
  chainId: Schema.ChainIdQuery,
278
410
  cursor: Schema.Cursor,
411
+ denomination: Schema.Denomination,
279
412
  include: getSwaps.includeQuery,
280
413
  limit: Schema.Limit,
281
414
  maker: z
@@ -297,20 +430,13 @@ export var schema;
297
430
  .check(z.describe('Query parameters for swap feed requests. Cursor-paginated only — a batched ' +
298
431
  'transaction expands one fill group into a variable number of swap rows, so a ' +
299
432
  'positional `page` has no stable meaning on this feed.'));
300
- /**
301
- * One side of a swap: a `PairToken` (address plus optional metadata when
302
- * requested via `include=tokens`) annotated with the `amount` that moved
303
- * on this side, as a decimal integer string in base units.
304
- */
305
- getSwaps.SwapSide = z
306
- .extend(schema.PairToken, {
307
- amount: Schema.DecimalString.check(z.describe('Amount moved on this side, returned as a decimal integer string in the token’s smallest units.'), z.meta({ examples: ['1000000'] })),
308
- })
309
- .check(z.describe('One side of a swap: the token and amount that moved.'));
433
+ /** One swap-side token with RPC metadata and optional curated fields. */
434
+ getSwaps.SwapSide = Schema.describe(Tokens.schema.TokenReference, 'A token referenced by one side of a swap.');
310
435
  /** A single maker-order fill within a swap, framed from the taker's perspective. */
311
436
  getSwaps.Fill = z
312
437
  .object({
313
- destinationToken: getSwaps.SwapSide.check(z.describe('Token and amount the taker received in this fill, with token metadata when requested via `include=tokens`.')),
438
+ destinationAmount: ValuedAmount.check(z.describe('Amount the taker received in this fill.')),
439
+ destinationToken: getSwaps.SwapSide.check(z.describe('Token the taker received in this fill.')),
314
440
  logIndex: z
315
441
  .number()
316
442
  .check(z.int(), z.nonnegative(), z.describe('Log index of this fill within its block.'), z.meta({ examples: [0] })),
@@ -322,7 +448,8 @@ export var schema;
322
448
  price: z
323
449
  .string()
324
450
  .check(z.regex(/^\d+(\.\d+)?$/), z.describe('Quote-per-base price at fill time, returned as a fixed-decimal string with 5 decimal places. This value is direction-independent and works well for charts.'), z.meta({ examples: ['1.00000'] })),
325
- sourceToken: getSwaps.SwapSide.check(z.describe('Token and amount the taker sent in this fill, with token metadata when requested via `include=tokens`.')),
451
+ sourceAmount: ValuedAmount.check(z.describe('Amount the taker sent in this fill.')),
452
+ sourceToken: getSwaps.SwapSide.check(z.describe('Token the taker sent in this fill.')),
326
453
  })
327
454
  .check(z.describe('One maker-order fill within a swap, described from the taker’s perspective.'));
328
455
  /**
@@ -337,7 +464,8 @@ export var schema;
337
464
  blockNumber: z
338
465
  .number()
339
466
  .check(z.int(), z.nonnegative(), z.describe('Block number where the swap was included.'), z.meta({ examples: [23456789] })),
340
- destinationToken: getSwaps.SwapSide.check(z.describe('Destination side of the swap: what the taker received, summed across the final hop.')),
467
+ destinationAmount: ValuedAmount.check(z.describe('Total amount the taker received in this swap.')),
468
+ destinationToken: getSwaps.SwapSide.check(z.describe('Destination token for the final hop.')),
341
469
  filledAt: z.iso
342
470
  .datetime()
343
471
  .check(z.describe('Block timestamp when the swap was filled onchain.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })),
@@ -359,7 +487,8 @@ export var schema;
359
487
  route: z
360
488
  .array(Schema.TokenAddress)
361
489
  .check(z.describe('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.')),
362
- sourceToken: getSwaps.SwapSide.check(z.describe('Source side of the swap: what the taker sent, summed across the first hop.')),
490
+ sourceAmount: ValuedAmount.check(z.describe('Total amount the taker sent in this swap.')),
491
+ sourceToken: getSwaps.SwapSide.check(z.describe('Source token for the first hop.')),
363
492
  taker: Schema.Address.check(z.describe('Taker address that initiated the swap.')),
364
493
  transactionHash: Schema.Hash.check(z.describe('Transaction hash for the transaction containing the swap.')),
365
494
  })
@@ -371,9 +500,21 @@ export var schema;
371
500
  * rows per page; a transaction batching several unrelated swaps by one
372
501
  * taker may have its rows split across consecutive pages.
373
502
  */
503
+ /** Page-level resources: valuation rate provenance. */
504
+ getSwaps.Meta = z
505
+ .object({
506
+ pricing: z
507
+ .optional(Valuation.schema.Pricing)
508
+ .check(z.describe('Rate provenance for amount valuations. Present when conversion rates were ' +
509
+ 'consulted; absent when every value was identity-valued or rates were unavailable.')),
510
+ })
511
+ .check(z.describe('Page-level resources attached to this response.'));
374
512
  getSwaps.Response = z
375
513
  .object({
376
514
  data: z.array(getSwaps.Swap).check(z.describe('Swaps returned on this page.')),
515
+ meta: z
516
+ .optional(getSwaps.Meta)
517
+ .check(z.describe('Page-level resources, such as valuation rate provenance.')),
377
518
  nextCursor: Schema.NextCursor,
378
519
  })
379
520
  .check(z.describe('A paginated list of swaps on Tempo’s built-in stablecoin exchange.'));
@@ -802,8 +943,206 @@ export var schema;
802
943
  * `token_balances_snapshot` row attribute cleanly to one pair. Pairs whose
803
944
  * DEX-escrow balance is zero are excluded from the liquidity ranking.
804
945
  */
805
- export function exchanges() {
946
+ export function exchanges(options = {}) {
947
+ const oracle = options.fx?.oracle ?? FxOracle.ecb();
806
948
  return new Hono()
949
+ .post('/v1/exchange/quotes', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('query', schema.createQuote.Query, {
950
+ code: 'query_invalid',
951
+ message: 'Invalid query parameters',
952
+ }), OpenApi.validate('json', schema.createQuote.Request, {
953
+ code: 'body_invalid',
954
+ message: 'Invalid request body',
955
+ }), OpenApi.describeRoute({
956
+ description: 'Creates a priced exchange preview on Tempo’s native stablecoin DEX with unsigned approval and swap calls. This endpoint does not submit a transaction.',
957
+ operationId: 'createExchangeQuote',
958
+ responses: OpenApi.responses({
959
+ errors: {
960
+ 400: { codes: ['body_invalid', 'query_invalid', 'quote_amount_out_of_range'] },
961
+ 404: {
962
+ codes: ['quote_not_available'],
963
+ description: 'The native DEX cannot price an executable exchange for these terms.',
964
+ },
965
+ 502: 'The Tempo RPC could not quote the swap or resolve token metadata.',
966
+ },
967
+ success: {
968
+ description: 'A native DEX quote and the unsigned calls needed to execute it.',
969
+ schema: schema.createQuote.Response,
970
+ },
971
+ }),
972
+ summary: 'Create quote',
973
+ tags: ['Exchange'],
974
+ }), async (c) => {
975
+ if (Auth.narrowAccess)
976
+ return Auth.paidAccessError(c);
977
+ if (OpenApi.narrowValidation)
978
+ return OpenApi.validationError(c, {
979
+ code: 'query_invalid',
980
+ message: 'Invalid query parameters',
981
+ });
982
+ if (OpenApi.narrowValidation)
983
+ return OpenApi.validationError(c, {
984
+ code: 'body_invalid',
985
+ message: 'Invalid request body',
986
+ });
987
+ const request = c.req.valid('json');
988
+ const options = c.req.valid('query');
989
+ const chainId = options.chainId ?? c.get('chainId');
990
+ const client = c.get('getClient')(chainId);
991
+ const amount = BigInt(request.amount);
992
+ const slippageBps = BigInt(request.slippageBps);
993
+ c.header('Cache-Control', 'no-store');
994
+ try {
995
+ // Value quoted amounts via their curated display currency; snapshot
996
+ // or rate failures degrade the values to `null`, never the quote.
997
+ const denomination = options.denomination ?? 'USD';
998
+ const snapshot = await VerifiedTokens.snapshot(c, chainId).catch(() => undefined);
999
+ const rates = await Valuation.ratesFor(c, {
1000
+ currencies: snapshot
1001
+ ? [request.destinationToken, request.sourceToken].flatMap((address) => {
1002
+ const held = snapshot.byAddress.get(address.toLowerCase())?.currency;
1003
+ return held === undefined ? [] : [held];
1004
+ })
1005
+ : [],
1006
+ denomination,
1007
+ oracle,
1008
+ });
1009
+ const valued = (baseUnits, address, token) => ({
1010
+ ...Value.tokenAmount({
1011
+ baseUnits,
1012
+ currency: token.currency,
1013
+ decimals: token.decimals,
1014
+ }),
1015
+ valuation: Valuation.valuationFor({
1016
+ amount: BigInt(baseUnits),
1017
+ denomination,
1018
+ rates,
1019
+ token: snapshot?.byAddress.get(address.toLowerCase()),
1020
+ }),
1021
+ });
1022
+ if (request.mode === 'exactSource') {
1023
+ const destinationAmount = await Actions.dex.getSellQuote(client, {
1024
+ amountIn: amount,
1025
+ tokenIn: request.sourceToken,
1026
+ tokenOut: request.destinationToken,
1027
+ });
1028
+ if (destinationAmount === 0n)
1029
+ return quoteNotAvailable(c);
1030
+ const minimumAmount = (destinationAmount * (slippageDenominator - slippageBps) + slippageDenominator - 1n) /
1031
+ slippageDenominator;
1032
+ const approve = Actions.token.approve.call(client, {
1033
+ amount,
1034
+ spender: Addresses.stablecoinDex,
1035
+ token: request.sourceToken,
1036
+ });
1037
+ const swap = Actions.dex.sell.call({
1038
+ amountIn: amount,
1039
+ minAmountOut: minimumAmount,
1040
+ tokenIn: request.sourceToken,
1041
+ tokenOut: request.destinationToken,
1042
+ });
1043
+ const tokensByAddress = await resolveTokensByAddress(c, {
1044
+ addresses: [request.destinationToken, request.sourceToken],
1045
+ chainId,
1046
+ include: options.include,
1047
+ });
1048
+ const destinationToken = tokensByAddress.get(request.destinationToken);
1049
+ const sourceToken = tokensByAddress.get(request.sourceToken);
1050
+ if (!destinationToken || !sourceToken)
1051
+ throw new Error('Unable to resolve exchange quote token metadata');
1052
+ return c.json(Response.validated(schema.createQuote.Response, {
1053
+ destinationAmount: valued(destinationAmount, request.destinationToken, destinationToken),
1054
+ destinationAmountMin: valued(minimumAmount, request.destinationToken, destinationToken),
1055
+ destinationToken: {
1056
+ ...destinationToken,
1057
+ address: request.destinationToken,
1058
+ },
1059
+ ...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
1060
+ mode: request.mode,
1061
+ sourceAmount: valued(request.amount, request.sourceToken, sourceToken),
1062
+ sourceToken: {
1063
+ ...sourceToken,
1064
+ address: request.sourceToken,
1065
+ },
1066
+ transaction: {
1067
+ calls: [
1068
+ { data: approve.data, to: approve.to, value: '0x0' },
1069
+ { data: swap.data, to: swap.to, value: '0x0' },
1070
+ ],
1071
+ chainId,
1072
+ },
1073
+ }), 200);
1074
+ }
1075
+ const sourceAmount = await Actions.dex.getBuyQuote(client, {
1076
+ amountOut: amount,
1077
+ tokenIn: request.sourceToken,
1078
+ tokenOut: request.destinationToken,
1079
+ });
1080
+ if (sourceAmount === 0n)
1081
+ return quoteNotAvailable(c);
1082
+ const maximumAmount = (sourceAmount * (slippageDenominator + slippageBps)) / slippageDenominator;
1083
+ if (maximumAmount > maxSwapAmount)
1084
+ return Response.error(c, {
1085
+ code: 'quote_amount_out_of_range',
1086
+ message: 'Slippage-adjusted source amount exceeds the native DEX limit',
1087
+ status: 400,
1088
+ });
1089
+ const approve = Actions.token.approve.call(client, {
1090
+ amount: maximumAmount,
1091
+ spender: Addresses.stablecoinDex,
1092
+ token: request.sourceToken,
1093
+ });
1094
+ const swap = Actions.dex.buy.call({
1095
+ amountOut: amount,
1096
+ maxAmountIn: maximumAmount,
1097
+ tokenIn: request.sourceToken,
1098
+ tokenOut: request.destinationToken,
1099
+ });
1100
+ const tokensByAddress = await resolveTokensByAddress(c, {
1101
+ addresses: [request.destinationToken, request.sourceToken],
1102
+ chainId,
1103
+ include: options.include,
1104
+ });
1105
+ const destinationToken = tokensByAddress.get(request.destinationToken);
1106
+ const sourceToken = tokensByAddress.get(request.sourceToken);
1107
+ if (!destinationToken || !sourceToken)
1108
+ throw new Error('Unable to resolve exchange quote token metadata');
1109
+ return c.json(Response.validated(schema.createQuote.Response, {
1110
+ destinationAmount: valued(request.amount, request.destinationToken, destinationToken),
1111
+ destinationToken: {
1112
+ ...destinationToken,
1113
+ address: request.destinationToken,
1114
+ },
1115
+ ...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
1116
+ mode: request.mode,
1117
+ sourceAmount: valued(sourceAmount, request.sourceToken, sourceToken),
1118
+ sourceAmountMax: valued(maximumAmount, request.sourceToken, sourceToken),
1119
+ sourceToken: {
1120
+ ...sourceToken,
1121
+ address: request.sourceToken,
1122
+ },
1123
+ transaction: {
1124
+ calls: [
1125
+ { data: approve.data, to: approve.to, value: '0x0' },
1126
+ { data: swap.data, to: swap.to, value: '0x0' },
1127
+ ],
1128
+ chainId,
1129
+ },
1130
+ }), 200);
1131
+ }
1132
+ catch (cause) {
1133
+ if (request.mode === 'exactSource' && isQuoteAmountOutOfRange(cause))
1134
+ return Response.error(c, {
1135
+ code: 'quote_amount_out_of_range',
1136
+ message: 'Quote amount exceeds the native DEX limit',
1137
+ status: 400,
1138
+ });
1139
+ if (isQuoteUnavailable(cause))
1140
+ return quoteNotAvailable(c);
1141
+ if (cause instanceof Valuation.UnsupportedDenominationError)
1142
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 });
1143
+ return Response.upstream(c, cause);
1144
+ }
1145
+ })
807
1146
  .get('/v1/exchange/swaps', Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }), OpenApi.validate('query', schema.getSwaps.Query, {
808
1147
  code: 'query_invalid',
809
1148
  message: 'Invalid query parameters',
@@ -812,7 +1151,7 @@ export function exchanges() {
812
1151
  operationId: 'getSwaps',
813
1152
  responses: OpenApi.responses({
814
1153
  errors: {
815
- 502: 'The indexer could not serve the exchange data right now.',
1154
+ 502: 'The indexer or Tempo RPC could not serve the exchange data right now.',
816
1155
  },
817
1156
  success: {
818
1157
  description: 'A page of swaps from Tempo’s built-in stablecoin exchange.',
@@ -822,11 +1161,10 @@ export function exchanges() {
822
1161
  summary: 'List swaps',
823
1162
  tags: ['Exchange'],
824
1163
  }), Cache.response({
825
- // Exchanges are a newest-first feed; refresh quickly. The `v2` cache
826
- // segment isolates the swap-level row shape from previously cached
827
- // per-fill pages (cursor pages cache as `immutable`).
1164
+ // Exchanges are a newest-first feed; refresh quickly. The `v3` cache
1165
+ // segment isolates this response shape from earlier swap pages.
828
1166
  cacheControl: Cache.policies.feed,
829
- name: 'tempo-api:exchange:v2:swaps',
1167
+ name: 'tempo-api:exchange:v3:swaps',
830
1168
  key: (c) => Cache.urlKey(c, schema.getSwaps.Query),
831
1169
  }), async (c) => {
832
1170
  if (Auth.narrowAccess)
@@ -838,11 +1176,9 @@ export function exchanges() {
838
1176
  });
839
1177
  const options = c.req.valid('query');
840
1178
  const chainId = options.chainId ?? c.get('chainId');
841
- // Cursor pages anchor below the head and are effectively immutable, so
842
- // upgrade their cache policy; the head page keeps the route's `feed`
843
- // default.
1179
+ // Cursor pages carry current valuations and curated verification data.
844
1180
  if (options.cursor !== undefined)
845
- Cache.setPolicy(c, Cache.policies.immutable);
1181
+ Cache.setPolicy(c, Cache.policies.metadata);
846
1182
  try {
847
1183
  // The pair index (base address → pair) resolves each fill's book
848
1184
  // orientation (the order events only carry the base-side `token`;
@@ -863,39 +1199,89 @@ export function exchanges() {
863
1199
  toTimestamp: options['timestamp.to'],
864
1200
  transactionHash: options.transactionHash,
865
1201
  });
866
- // Token metadata is opt-in. Collect every token address across the
867
- // page (`route` spans each swap's source, intermediates, and
868
- // destination a superset of every fill side) and resolve each
869
- // once, then spread it onto the swap-level sides and every fill
870
- // side. Resolution is best-effort: a token whose metadata is
871
- // unavailable keeps just `address`/`amount`.
872
- let data = page.data;
873
- if (options.include.includes('tokens') && data.length > 0) {
874
- const tokensByAddress = await resolveTokensByAddress(c, {
875
- addresses: data.flatMap((swap) => swap.route),
876
- chainId,
877
- });
878
- const spread = (side) => {
879
- const token = tokensByAddress.get(side.address);
880
- return token ? { ...side, ...token } : side;
1202
+ // `route` spans every swap and fill side, so one lookup resolves each
1203
+ // token once for required RPC metadata and requested curated fields.
1204
+ const tokensByAddress = await resolveTokensByAddress(c, {
1205
+ addresses: page.data.flatMap((swap) => swap.route),
1206
+ chainId,
1207
+ include: options.include,
1208
+ });
1209
+ // Value amounts via their curated display currency; snapshot or rate
1210
+ // failures degrade the values to `null`, never the feed.
1211
+ const denomination = options.denomination ?? 'USD';
1212
+ const snapshot = page.data.length > 0
1213
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
1214
+ : undefined;
1215
+ const rates = await Valuation.ratesFor(c, {
1216
+ currencies: snapshot
1217
+ ? page.data.flatMap((swap) => swap.route.flatMap((address) => {
1218
+ const held = snapshot.byAddress.get(address.toLowerCase())?.currency;
1219
+ return held === undefined ? [] : [held];
1220
+ }))
1221
+ : [],
1222
+ denomination,
1223
+ oracle,
1224
+ });
1225
+ const enrichSide = (side) => {
1226
+ const token = tokensByAddress.get(side.address);
1227
+ if (!token)
1228
+ throw new Error(`Token metadata unavailable for ${side.address}`);
1229
+ return {
1230
+ amount: {
1231
+ ...Value.tokenAmount({
1232
+ baseUnits: side.amount,
1233
+ currency: token.currency,
1234
+ decimals: token.decimals,
1235
+ }),
1236
+ valuation: Valuation.valuationFor({
1237
+ amount: BigInt(side.amount),
1238
+ denomination,
1239
+ rates,
1240
+ token: snapshot?.byAddress.get(side.address.toLowerCase()),
1241
+ }),
1242
+ },
1243
+ token: {
1244
+ address: side.address,
1245
+ currency: token.currency,
1246
+ decimals: token.decimals,
1247
+ ...(token.logoUri !== undefined ? { logoUri: token.logoUri } : {}),
1248
+ name: token.name,
1249
+ symbol: token.symbol,
1250
+ ...(token.verified !== undefined ? { verified: token.verified } : {}),
1251
+ },
881
1252
  };
882
- data = data.map((swap) => ({
1253
+ };
1254
+ const data = page.data.map((swap) => {
1255
+ const destination = enrichSide(swap.destinationToken);
1256
+ const source = enrichSide(swap.sourceToken);
1257
+ return {
883
1258
  ...swap,
884
- destinationToken: spread(swap.destinationToken),
885
- fills: swap.fills.map((fill) => ({
886
- ...fill,
887
- destinationToken: spread(fill.destinationToken),
888
- sourceToken: spread(fill.sourceToken),
889
- })),
890
- sourceToken: spread(swap.sourceToken),
891
- }));
892
- }
1259
+ destinationAmount: destination.amount,
1260
+ destinationToken: destination.token,
1261
+ fills: swap.fills.map((fill) => {
1262
+ const destination_fill = enrichSide(fill.destinationToken);
1263
+ const source_fill = enrichSide(fill.sourceToken);
1264
+ return {
1265
+ ...fill,
1266
+ destinationAmount: destination_fill.amount,
1267
+ destinationToken: destination_fill.token,
1268
+ sourceAmount: source_fill.amount,
1269
+ sourceToken: source_fill.token,
1270
+ };
1271
+ }),
1272
+ sourceAmount: source.amount,
1273
+ sourceToken: source.token,
1274
+ };
1275
+ });
893
1276
  return c.json(Response.validated(schema.getSwaps.Response, {
894
1277
  data,
1278
+ ...(rates ? { meta: { pricing: Valuation.pricing(rates, oracle) } } : {}),
895
1279
  nextCursor: page.nextCursor,
896
1280
  }), 200);
897
1281
  }
898
1282
  catch (cause) {
1283
+ if (cause instanceof Valuation.UnsupportedDenominationError)
1284
+ return Response.error(c, { code: 'query_invalid', message: cause.message, status: 400 });
899
1285
  return Response.upstream(c, cause);
900
1286
  }
901
1287
  })
@@ -969,6 +1355,7 @@ export function exchanges() {
969
1355
  const tokensByAddress = await resolveTokensByAddress(c, {
970
1356
  addresses: data.flatMap((pair) => [pair.base.address, pair.quote.address]),
971
1357
  chainId,
1358
+ include: tokenFields,
972
1359
  });
973
1360
  data = data.map((pair) => {
974
1361
  const base = tokensByAddress.get(pair.base.address);
@@ -1053,6 +1440,7 @@ export function exchanges() {
1053
1440
  const tokensByAddress = await resolveTokensByAddress(c, {
1054
1441
  addresses: [target.base, target.quote],
1055
1442
  chainId,
1443
+ include: tokenFields,
1056
1444
  });
1057
1445
  const baseMeta = tokensByAddress.get(target.base);
1058
1446
  const quoteMeta = tokensByAddress.get(target.quote);
@@ -1150,6 +1538,7 @@ export function exchanges() {
1150
1538
  const tokensByAddress = await resolveTokensByAddress(c, {
1151
1539
  addresses: data.flatMap((o) => [o.pair.base.address, o.pair.quote.address]),
1152
1540
  chainId,
1541
+ include: tokenFields,
1153
1542
  });
1154
1543
  data = data.map((o) => {
1155
1544
  const base = tokensByAddress.get(o.pair.base.address);
@@ -1429,6 +1818,7 @@ export function exchanges() {
1429
1818
  const tokensByAddress = await resolveTokensByAddress(c, {
1430
1819
  addresses: [target.base, target.quote],
1431
1820
  chainId,
1821
+ include: tokenFields,
1432
1822
  });
1433
1823
  const baseMeta = tokensByAddress.get(target.base);
1434
1824
  const quoteMeta = tokensByAddress.get(target.quote);
@@ -1519,6 +1909,7 @@ export function exchanges() {
1519
1909
  const tokensByAddress = await resolveTokensByAddress(c, {
1520
1910
  addresses: [target.base, target.quote],
1521
1911
  chainId,
1912
+ include: tokenFields,
1522
1913
  });
1523
1914
  const baseMeta = tokensByAddress.get(target.base);
1524
1915
  const quoteMeta = tokensByAddress.get(target.quote);
@@ -1552,23 +1943,46 @@ function pairNotFoundError(c, options) {
1552
1943
  : 'Pair not found';
1553
1944
  return Response.error(c, { code: 'pair_not_found', message, status: 404 });
1554
1945
  }
1555
- /**
1556
- * Batch-resolves token metadata for a set of addresses, sharing one
1557
- * verified-token snapshot and one created-at lookup across the whole set.
1558
- * Best-effort: addresses whose metadata cannot be resolved are absent from
1559
- * the returned map, so callers keep their bare `{ address }` shapes. Shared
1560
- * by every `include=tokens` embed in this module.
1561
- */
1946
+ /** Resolves RPC token metadata and explicitly requested enrichments. Unresolvable addresses are omitted. */
1562
1947
  async function resolveTokensByAddress(c, options) {
1563
- const { addresses, chainId } = options;
1948
+ const { addresses, chainId, include } = options;
1949
+ const includeLogoUri = include.includes('token.logoUri');
1950
+ const includeVerified = include.includes('token.verified');
1564
1951
  const tokensByAddress = new Map();
1565
1952
  const uniqueTokens = [...new Set(addresses)];
1566
1953
  if (uniqueTokens.length === 0)
1567
1954
  return tokensByAddress;
1568
- const snapshot = await VerifiedTokens.snapshot(c, chainId);
1569
- const resolved = await Timing.time(c, 'tokens', () => Promise.all(uniqueTokens.map((token) => Tokens.resolveToken(c, { address: token, chainId, snapshot })
1570
- .then((metadata) => [token, metadata])
1571
- .catch(() => [token, undefined]))));
1955
+ const snapshot = includeLogoUri || includeVerified
1956
+ ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
1957
+ : undefined;
1958
+ const resolved = await Timing.time(c, 'tokens', () => Promise.all(uniqueTokens.map(async (token) => {
1959
+ try {
1960
+ const [logoUri, metadata] = await Promise.all([
1961
+ includeLogoUri
1962
+ ? Timing.time(c, 'token_logo', () => Tokens.getTokenLogo(c, { address: token, chainId })).catch(() => undefined)
1963
+ : undefined,
1964
+ Timing.time(c, 'token_metadata', () => Tokens.getTokenMetadata(c, { address: token, chainId })),
1965
+ ]);
1966
+ return [
1967
+ token,
1968
+ {
1969
+ address: token,
1970
+ currency: metadata.currency,
1971
+ decimals: metadata.decimals,
1972
+ id: token,
1973
+ logoUri: includeLogoUri
1974
+ ? (logoUri ?? snapshot?.byAddress.get(token)?.logoUri ?? metadata.logoUri)
1975
+ : undefined,
1976
+ name: metadata.name,
1977
+ symbol: metadata.symbol,
1978
+ verified: includeVerified ? snapshot?.byAddress.has(token) : undefined,
1979
+ },
1980
+ ];
1981
+ }
1982
+ catch {
1983
+ return [token, undefined];
1984
+ }
1985
+ })));
1572
1986
  for (const [token, metadata] of resolved)
1573
1987
  if (metadata)
1574
1988
  tokensByAddress.set(token, metadata);
@@ -3438,6 +3852,34 @@ function isOrderDoesNotExist(error) {
3438
3852
  return (revert instanceof ContractFunctionRevertedError &&
3439
3853
  revert.data?.errorName === 'OrderDoesNotExist');
3440
3854
  }
3855
+ /** Returns the native DEX 404 used when no executable quote is available. */
3856
+ function quoteNotAvailable(c) {
3857
+ return Response.error(c, {
3858
+ code: 'quote_not_available',
3859
+ message: 'No executable quote is available on Tempo’s native DEX',
3860
+ status: 404,
3861
+ });
3862
+ }
3863
+ /** Distinguishes native DEX route failures from RPC transport failures. */
3864
+ function isQuoteUnavailable(error) {
3865
+ if (!(error instanceof BaseError))
3866
+ return false;
3867
+ const revert = error.walk((e) => e instanceof ContractFunctionRevertedError);
3868
+ if (!(revert instanceof ContractFunctionRevertedError))
3869
+ return false;
3870
+ return (revert.raw === contractPausedSelector ||
3871
+ ['InsufficientLiquidity', 'InvalidBaseToken', 'InvalidToken', 'PairDoesNotExist'].includes(revert.data?.errorName ?? ''));
3872
+ }
3873
+ /** Detects uint128 arithmetic overflow from exact-source DEX quote calls. */
3874
+ function isQuoteAmountOutOfRange(error) {
3875
+ if (!(error instanceof BaseError))
3876
+ return false;
3877
+ const revert = error.walk((e) => e instanceof ContractFunctionRevertedError);
3878
+ if (!(revert instanceof ContractFunctionRevertedError))
3879
+ return false;
3880
+ const data = revert.data;
3881
+ return data?.errorName === 'Panic' && data.args?.[0] === 0x11n;
3882
+ }
3441
3883
  /**
3442
3884
  * Resolves a single page of `OrderFilled` events for one order. Pagination is
3443
3885
  * keyset on `(block_num, log_idx)`, which is unique and append-only for the