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
@@ -10,6 +10,7 @@ const baseUsdcRequest = {
10
10
 
11
11
  describe('listQuotes', () => {
12
12
  test('returns normalized successful provider quotes', async () => {
13
+ const attempts: FundingQuotes.ProviderAttempt[] = []
13
14
  let seen: FundingQuotes.QuoteProbeInput | undefined
14
15
  const quotes = await FundingQuotes.listQuotes({
15
16
  ...baseUsdcRequest,
@@ -33,6 +34,7 @@ describe('listQuotes', () => {
33
34
  },
34
35
  },
35
36
  },
37
+ onAttempt: (attempt) => attempts.push(attempt),
36
38
  })
37
39
 
38
40
  expect(seen?.candidate.id).toBe('base-usdc-tempo-usdce-relay')
@@ -50,13 +52,23 @@ describe('listQuotes', () => {
50
52
  outputAmountFormatted: '0.99',
51
53
  },
52
54
  })
55
+ expect(attempts.map(stableAttempt)).toEqual([{ outcome: 'available', provider: 'relay' }])
56
+ expect(attempts[0]?.durationMs).toEqual(expect.any(Number))
53
57
  })
54
58
 
55
59
  test('returns no static candidates when no adapter is configured', async () => {
56
- await expect(FundingQuotes.listQuotes(baseUsdcRequest)).resolves.toEqual([])
60
+ const attempts: FundingQuotes.ProviderAttempt[] = []
61
+ await expect(
62
+ FundingQuotes.listQuotes({
63
+ ...baseUsdcRequest,
64
+ onAttempt: (attempt) => attempts.push(attempt),
65
+ }),
66
+ ).resolves.toEqual([])
67
+ expect(attempts).toEqual([])
57
68
  })
58
69
 
59
70
  test('omits provider-declared unavailable quotes', async () => {
71
+ const attempts: FundingQuotes.ProviderAttempt[] = []
60
72
  const quotes = FundingQuotes.listQuotes({
61
73
  ...baseUsdcRequest,
62
74
  adapters: {
@@ -66,12 +78,15 @@ describe('listQuotes', () => {
66
78
  },
67
79
  },
68
80
  },
81
+ onAttempt: (attempt) => attempts.push(attempt),
69
82
  })
70
83
 
71
84
  await expect(quotes).resolves.toEqual([])
85
+ expect(attempts.map(stableAttempt)).toEqual([{ outcome: 'unavailable', provider: 'relay' }])
72
86
  })
73
87
 
74
88
  test('rejects provider failures when no quote succeeds', async () => {
89
+ const attempts: FundingQuotes.ProviderAttempt[] = []
75
90
  const quotes = FundingQuotes.listQuotes({
76
91
  ...baseUsdcRequest,
77
92
  adapters: {
@@ -81,6 +96,7 @@ describe('listQuotes', () => {
81
96
  },
82
97
  },
83
98
  },
99
+ onAttempt: (attempt) => attempts.push(attempt),
84
100
  })
85
101
 
86
102
  const cause = await quotes.catch((error: unknown) => error)
@@ -88,6 +104,31 @@ describe('listQuotes', () => {
88
104
  failures: [{ cause: expect.any(Error), provider: 'relay' }],
89
105
  name: 'FundingQuotes.ProviderQuoteError',
90
106
  })
107
+ expect(attempts.map(stableAttempt)).toEqual([
108
+ { cause: expect.any(Error), outcome: 'failed', provider: 'relay' },
109
+ ])
110
+ })
111
+
112
+ test('records thin quotes separately from available quotes', async () => {
113
+ const attempts: FundingQuotes.ProviderAttempt[] = []
114
+ const quotes = await FundingQuotes.listQuotes({
115
+ ...baseUsdcRequest,
116
+ adapters: {
117
+ relay: {
118
+ async probeQuote() {
119
+ return {
120
+ outputAmount: '990000',
121
+ sampledAt: '2026-01-01T00:00:00Z',
122
+ status: 'thin',
123
+ }
124
+ },
125
+ },
126
+ },
127
+ onAttempt: (attempt) => attempts.push(attempt),
128
+ })
129
+
130
+ expect(quotes).toHaveLength(1)
131
+ expect(attempts.map(stableAttempt)).toEqual([{ outcome: 'thin', provider: 'relay' }])
91
132
  })
92
133
 
93
134
  test('rejects successful provider responses without output amounts', async () => {
@@ -108,6 +149,7 @@ describe('listQuotes', () => {
108
149
  })
109
150
 
110
151
  test('aborts providers that exceed the quote deadline', async () => {
152
+ const attempts: FundingQuotes.ProviderAttempt[] = []
111
153
  let aborted = false
112
154
  const quotes = FundingQuotes.listQuotes({
113
155
  ...baseUsdcRequest,
@@ -122,12 +164,20 @@ describe('listQuotes', () => {
122
164
  },
123
165
  },
124
166
  },
167
+ onAttempt: (attempt) => attempts.push(attempt),
125
168
  })
126
169
 
127
170
  await expect(quotes).rejects.toThrowErrorMatchingInlineSnapshot(
128
171
  `[FundingQuotes.ProviderQuoteError]`,
129
172
  )
130
173
  expect(aborted).toBe(true)
174
+ expect(attempts.map(stableAttempt)).toEqual([
175
+ {
176
+ cause: expect.any(FundingQuotes.ProviderAdapterTimeoutError),
177
+ outcome: 'failed',
178
+ provider: 'relay',
179
+ },
180
+ ])
131
181
  })
132
182
 
133
183
  test('passes cache policy through to providers', async () => {
@@ -149,6 +199,7 @@ describe('listQuotes', () => {
149
199
  })
150
200
 
151
201
  test('returns successful quotes when another provider fails', async () => {
202
+ const attempts: FundingQuotes.ProviderAttempt[] = []
152
203
  const quotes = await FundingQuotes.listQuotes({
153
204
  ...baseUsdcRequest,
154
205
  adapters: {
@@ -167,14 +218,63 @@ describe('listQuotes', () => {
167
218
  },
168
219
  },
169
220
  },
221
+ onAttempt: (attempt) => attempts.push(attempt),
170
222
  provider: undefined,
171
223
  })
172
224
 
173
225
  expect(quotes).toHaveLength(1)
174
226
  expect(quotes[0]?.provider.id).toBe('relay')
227
+ expect(attempts.map(stableAttempt)).toEqual([
228
+ { cause: expect.any(Error), outcome: 'failed', provider: 'across' },
229
+ { outcome: 'available', provider: 'relay' },
230
+ ])
231
+ })
232
+
233
+ test('does not let attempt observers change quote outcomes', async () => {
234
+ const adapters = {
235
+ relay: {
236
+ async probeQuote() {
237
+ return {
238
+ outputAmount: '990000',
239
+ sampledAt: '2026-01-01T00:00:00Z',
240
+ status: 'available' as const,
241
+ }
242
+ },
243
+ },
244
+ }
245
+ await expect(
246
+ FundingQuotes.listQuotes({
247
+ ...baseUsdcRequest,
248
+ adapters,
249
+ onAttempt() {
250
+ throw new Error('observer failed')
251
+ },
252
+ }),
253
+ ).resolves.toHaveLength(1)
254
+
255
+ await expect(
256
+ FundingQuotes.listQuotes({
257
+ ...baseUsdcRequest,
258
+ adapters: {
259
+ relay: {
260
+ async probeQuote() {
261
+ throw new Error('provider failed')
262
+ },
263
+ },
264
+ },
265
+ onAttempt() {
266
+ throw new Error('observer failed')
267
+ },
268
+ }),
269
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`[FundingQuotes.ProviderQuoteError]`)
175
270
  })
176
271
  })
177
272
 
273
+ function stableAttempt(attempt: FundingQuotes.ProviderAttempt) {
274
+ const { durationMs: _durationMs, ...rest } = attempt
275
+ return rest
276
+ }
277
+
178
278
  describe('listChains', () => {
179
279
  test('returns only chain and token inputs with quote inventory', () => {
180
280
  expect(FundingQuotes.listChains().map((chain) => chain.id)).toEqual([
@@ -261,6 +361,41 @@ describe('listQuoteCandidates', () => {
261
361
  'ethereum-usdt-tempo-usdt0-squid',
262
362
  ])
263
363
  })
364
+
365
+ test('selects proven Symbiosis stablecoin corridors', () => {
366
+ const candidates = [
367
+ FundingQuotes.listQuoteCandidates({
368
+ destinationToken: 'USDC.e',
369
+ provider: 'symbiosis',
370
+ sourceAmount: '1000000',
371
+ sourceAmountUnits: 'baseUnits',
372
+ sourceChain: 'base',
373
+ sourceToken: 'USDC',
374
+ })[0],
375
+ FundingQuotes.listQuoteCandidates({
376
+ destinationToken: 'USDT0',
377
+ provider: 'symbiosis',
378
+ sourceAmount: '1000000',
379
+ sourceAmountUnits: 'baseUnits',
380
+ sourceChain: 'ethereum',
381
+ sourceToken: 'USDT',
382
+ })[0],
383
+ FundingQuotes.listQuoteCandidates({
384
+ destinationToken: 'USDT0',
385
+ provider: 'symbiosis',
386
+ sourceAmount: '1000000',
387
+ sourceAmountUnits: 'baseUnits',
388
+ sourceChain: 'tron',
389
+ sourceToken: 'USDT',
390
+ })[0],
391
+ ]
392
+
393
+ expect(candidates.map((candidate) => candidate?.id)).toEqual([
394
+ 'base-usdc-tempo-usdce-symbiosis',
395
+ 'ethereum-usdt-tempo-usdt0-symbiosis',
396
+ 'tron-usdt-tempo-usdt0-symbiosis',
397
+ ])
398
+ })
264
399
  })
265
400
 
266
401
  describe('listProviders', () => {
@@ -295,6 +430,13 @@ describe('listProviders', () => {
295
430
  "requiresApiKey": true,
296
431
  "status": "unconfigured",
297
432
  },
433
+ {
434
+ "id": "symbiosis",
435
+ "kind": "crossChainDex",
436
+ "name": "Symbiosis",
437
+ "requiresApiKey": false,
438
+ "status": "unconfigured",
439
+ },
298
440
  ]
299
441
  `)
300
442
  })
@@ -20,6 +20,7 @@ const sourceChains = [
20
20
  providerIds: {
21
21
  across: '1',
22
22
  relay: '1',
23
+ symbiosis: '1',
23
24
  },
24
25
  slug: 'ethereum',
25
26
  tokens: [
@@ -75,6 +76,7 @@ const sourceChains = [
75
76
  across: '8453',
76
77
  relay: '8453',
77
78
  rhino: 'BASE',
79
+ symbiosis: '8453',
78
80
  },
79
81
  slug: 'base',
80
82
  tokens: [
@@ -113,6 +115,7 @@ const sourceChains = [
113
115
  providerIds: {
114
116
  across: '56',
115
117
  relay: '56',
118
+ symbiosis: '56',
116
119
  },
117
120
  slug: 'bnb',
118
121
  tokens: [
@@ -163,6 +166,7 @@ const sourceChains = [
163
166
  name: 'Tron',
164
167
  providerIds: {
165
168
  rhino: 'TRON',
169
+ symbiosis: '728126428',
166
170
  },
167
171
  slug: 'tron',
168
172
  tokens: [
@@ -252,6 +256,13 @@ const providers = [
252
256
  requiresApiKey: true,
253
257
  status: 'unconfigured',
254
258
  },
259
+ {
260
+ id: 'symbiosis',
261
+ kind: 'crossChainDex',
262
+ name: 'Symbiosis',
263
+ requiresApiKey: false,
264
+ status: 'unconfigured',
265
+ },
255
266
  ] as const
256
267
 
257
268
  type TokenInput = (typeof sourceChains)[number]['tokens'][number]
@@ -363,6 +374,27 @@ export type ProviderAdapter = {
363
374
  /** Provider adapter registry, keyed by provider id. */
364
375
  export type ProviderAdapters = Partial<Record<ProviderId, ProviderAdapter>>
365
376
 
377
+ /** One configured provider attempt made while resolving a funding quote request. */
378
+ export type ProviderAttempt =
379
+ | {
380
+ /** Wall-clock duration of the provider attempt in milliseconds. */
381
+ durationMs: number
382
+ /** Successful or expected-unavailable provider outcome. */
383
+ outcome: 'available' | 'thin' | 'unavailable'
384
+ /** Provider that handled the quote candidate. */
385
+ provider: ProviderId
386
+ }
387
+ | {
388
+ /** Error thrown by the provider boundary. */
389
+ cause: unknown
390
+ /** Wall-clock duration of the provider attempt in milliseconds. */
391
+ durationMs: number
392
+ /** Failed provider outcome. */
393
+ outcome: 'failed'
394
+ /** Provider that handled the quote candidate. */
395
+ provider: ProviderId
396
+ }
397
+
366
398
  /** A provider adapter failure retained for bounded request diagnostics. */
367
399
  export type ProviderFailure = {
368
400
  /** Error thrown by the provider boundary. */
@@ -377,8 +409,10 @@ const inventory: readonly InventoryEntry[] = [
377
409
  { destinationToken: 'usdce', provider: 'relay', sourceChains: ['ethereum', 'base', 'bnb'], sourceTokens: ['USDC', 'USDT'] }, // prettier-ignore
378
410
  { destinationToken: 'usdce', provider: 'rhino', sourceChains: ['base', 'solana'], sourceTokens: ['USDC'] }, // prettier-ignore
379
411
  { destinationToken: 'usdce', provider: 'squid', sourceChains: ['ethereum', 'base', 'bnb'], sourceTokens: ['USDC'] }, // prettier-ignore
412
+ { destinationToken: 'usdce', provider: 'symbiosis', sourceChains: ['ethereum', 'base', 'bnb'], sourceTokens: ['USDC'] }, // prettier-ignore
380
413
  { destinationToken: 'usdt0', provider: 'rhino', sourceChains: ['tron'], sourceTokens: ['USDT'] }, // prettier-ignore
381
414
  { destinationToken: 'usdt0', provider: 'squid', sourceChains: ['ethereum', 'base', 'bnb'], sourceTokens: ['USDT'] }, // prettier-ignore
415
+ { destinationToken: 'usdt0', provider: 'symbiosis', sourceChains: ['ethereum', 'base', 'bnb', 'tron'], sourceTokens: ['USDT'] }, // prettier-ignore
382
416
  ]
383
417
 
384
418
  type InventoryEntry = {
@@ -428,30 +462,61 @@ export declare namespace listProviders {
428
462
 
429
463
  /** Returns successful live funding quotes from configured providers. */
430
464
  export async function listQuotes(options: listQuotes.Options) {
431
- const candidates = listQuoteCandidates(options)
465
+ const adapters = options.adapters ?? {}
466
+ const candidates = listQuoteCandidates(options).flatMap((candidate) => {
467
+ const adapter = adapters[candidate.provider.id as ProviderId]
468
+ return adapter ? [{ adapter, candidate }] : []
469
+ })
470
+ const attempts = new Map<number, ProviderAttempt>()
432
471
  const results = await Promise.allSettled(
433
- candidates.map((candidate) =>
434
- quoteCandidate(candidate, {
435
- adapters: options.adapters ?? {},
436
- cache: options.cache ?? { bypass: false },
437
- request: {
438
- destinationToken: options.destinationToken,
439
- provider: options.provider,
440
- sourceAmount: options.sourceAmount,
441
- sourceAmountUnits: options.sourceAmountUnits,
442
- sourceChain: options.sourceChain,
443
- sourceToken: options.sourceToken,
444
- },
445
- timeoutMs: options.adapterTimeoutMs ?? defaultAdapterTimeoutMs,
446
- }),
447
- ),
472
+ candidates.map(async ({ adapter, candidate }, index) => {
473
+ const start = performance.now()
474
+ try {
475
+ const result = await quoteCandidate(candidate, {
476
+ adapter,
477
+ cache: options.cache ?? { bypass: false },
478
+ request: {
479
+ destinationToken: options.destinationToken,
480
+ provider: options.provider,
481
+ sourceAmount: options.sourceAmount,
482
+ sourceAmountUnits: options.sourceAmountUnits,
483
+ sourceChain: options.sourceChain,
484
+ sourceToken: options.sourceToken,
485
+ },
486
+ timeoutMs: options.adapterTimeoutMs ?? defaultAdapterTimeoutMs,
487
+ })
488
+ attempts.set(index, {
489
+ durationMs: performance.now() - start,
490
+ outcome: result.outcome,
491
+ provider: candidate.provider.id,
492
+ })
493
+ return 'quote' in result ? result.quote : undefined
494
+ } catch (cause) {
495
+ attempts.set(index, {
496
+ cause,
497
+ durationMs: performance.now() - start,
498
+ outcome: 'failed',
499
+ provider: candidate.provider.id,
500
+ })
501
+ throw cause
502
+ }
503
+ }),
448
504
  )
505
+ for (const index of candidates.keys()) {
506
+ const attempt = attempts.get(index)
507
+ if (!attempt) continue
508
+ try {
509
+ options.onAttempt?.(attempt)
510
+ } catch {
511
+ // Observability hooks never alter funding quote behavior.
512
+ }
513
+ }
449
514
  const quotes = results.flatMap((result) =>
450
515
  result.status === 'fulfilled' && result.value ? [result.value] : [],
451
516
  )
452
517
  const failures = results.flatMap((result, index) =>
453
518
  result.status === 'rejected'
454
- ? [{ cause: result.reason, provider: candidates[index]!.provider.id }]
519
+ ? [{ cause: result.reason, provider: candidates[index]!.candidate.provider.id }]
455
520
  : [],
456
521
  )
457
522
  if (quotes.length === 0 && failures.length > 0) throw new ProviderQuoteError(failures)
@@ -519,6 +584,8 @@ export declare namespace listQuotes {
519
584
  adapterTimeoutMs?: number | undefined
520
585
  /** Cache policy for provider-side discovery and quote lookups. */
521
586
  cache?: CachePolicy | undefined
587
+ /** Receives one bounded observation for every configured provider attempt. */
588
+ onAttempt?: ((attempt: ProviderAttempt) => void) | undefined
522
589
  }
523
590
  }
524
591
 
@@ -544,18 +611,15 @@ function decimalFromBaseUnits(baseUnits: string, decimals: number) {
544
611
  async function quoteCandidate(
545
612
  candidate: QuoteCandidate,
546
613
  options: {
547
- adapters: ProviderAdapters
614
+ adapter: ProviderAdapter
548
615
  cache: CachePolicy
549
616
  request: QuoteRequest
550
617
  timeoutMs: number
551
618
  },
552
619
  ) {
553
- const adapter = options.adapters[candidate.provider.id as ProviderId]
554
- if (!adapter) return undefined
555
-
556
620
  const quote = await runWithTimeout(
557
621
  (signal) =>
558
- adapter.probeQuote(
622
+ options.adapter.probeQuote(
559
623
  {
560
624
  cache: options.cache,
561
625
  destinationChain: candidate.destinationChain,
@@ -570,18 +634,21 @@ async function quoteCandidate(
570
634
  ),
571
635
  options.timeoutMs,
572
636
  )
573
- if (quote.status === 'unavailable') return undefined
637
+ if (quote.status === 'unavailable') return { outcome: quote.status }
574
638
  if (
575
639
  (quote.status !== 'available' && quote.status !== 'thin') ||
576
640
  quote.outputAmount === undefined ||
577
641
  quote.outputAmount === null
578
642
  )
579
643
  throw new InvalidProviderQuoteError()
580
- return formatFundingQuote(candidate, {
581
- ...quote,
582
- outputAmount: quote.outputAmount,
583
- status: quote.status,
584
- })
644
+ return {
645
+ outcome: quote.status,
646
+ quote: formatFundingQuote(candidate, {
647
+ ...quote,
648
+ outputAmount: quote.outputAmount,
649
+ status: quote.status,
650
+ }),
651
+ }
585
652
  }
586
653
 
587
654
  function formatChain(chain: SourceChainInput) {
@@ -0,0 +1,21 @@
1
+ import type * as FundingQuotes from './FundingQuotes.js'
2
+ import type * as Log from './Log.js'
3
+
4
+ test('bounds funding provider metric identifiers', () => {
5
+ const provider = null as never as FundingQuotes.ProviderId
6
+ const id: Log.FundingProviderAttempt['id'] = provider
7
+ const provider_roundTrip: FundingQuotes.ProviderId = id
8
+ void provider_roundTrip
9
+
10
+ const attempt = {
11
+ durationMs: 1,
12
+ id: 'relay',
13
+ operation: 'quote/v2',
14
+ outcome: 'available',
15
+ } satisfies Log.FundingProviderAttempt
16
+ void attempt
17
+
18
+ // @ts-expect-error arbitrary provider names would create unbounded metric tags
19
+ const invalid: Log.FundingProviderAttempt['id'] = 'user-controlled-provider'
20
+ void invalid
21
+ })
@@ -177,6 +177,56 @@ describe('middleware', () => {
177
177
  `)
178
178
  })
179
179
 
180
+ test('includes bounded funding provider attempts', async () => {
181
+ const entries: Log.Entry[] = []
182
+ const api = app({ emit: (entry) => void entries.push(entry) })
183
+ api.get('/funding', (c) => {
184
+ c.set('fundingProviderAttempts', [
185
+ {
186
+ durationMs: 12.5,
187
+ id: 'relay',
188
+ operation: 'quote/v2',
189
+ outcome: 'available',
190
+ },
191
+ {
192
+ durationMs: 25,
193
+ failure: 'timeout',
194
+ id: 'across',
195
+ operation: 'swap/approval',
196
+ outcome: 'failed',
197
+ },
198
+ ])
199
+ return c.json({ ok: true }, 200)
200
+ })
201
+
202
+ await api.request('/funding')
203
+
204
+ expect(stable(entries[0]!)).toMatchInlineSnapshot(`
205
+ {
206
+ "fundingProviderAttempts": [
207
+ {
208
+ "durationMs": 12.5,
209
+ "id": "relay",
210
+ "operation": "quote/v2",
211
+ "outcome": "available",
212
+ },
213
+ {
214
+ "durationMs": 25,
215
+ "failure": "timeout",
216
+ "id": "across",
217
+ "operation": "swap/approval",
218
+ "outcome": "failed",
219
+ },
220
+ ],
221
+ "level": "info",
222
+ "method": "GET",
223
+ "path": "/funding",
224
+ "route": "/funding",
225
+ "status": 200,
226
+ }
227
+ `)
228
+ })
229
+
180
230
  test('logs thrown errors at error level', async () => {
181
231
  const entries: Log.Entry[] = []
182
232
  const api = app({ emit: (entry) => void entries.push(entry) })
@@ -4,12 +4,15 @@ import type { RequestIdVariables } from 'hono/request-id'
4
4
  import * as ApiKey from '../ApiKey.js'
5
5
  import type * as Auth from './Auth.js'
6
6
  import type * as EdgeCache from './EdgeCache.js'
7
+ import type * as FundingQuotes from './FundingQuotes.js'
7
8
  import type * as Timing from './Timing.js'
8
9
 
9
10
  /** Hono variables read by the request logger and written by other layers. */
10
11
  export type Variables = {
11
12
  /** Stable error code recorded by `Response.error` for the request log entry. */
12
13
  errorCode?: string | undefined
14
+ /** Bounded outcomes for funding providers attempted by the request. */
15
+ fundingProviderAttempts?: Entry['fundingProviderAttempts'] | undefined
13
16
  /** Redacted provider failure details recorded by an upstream adapter. */
14
17
  providerFailure?: Entry['provider'] | undefined
15
18
  /** Redacted provider failures when multiple upstream adapters fail. */
@@ -35,7 +38,7 @@ export type ProviderFailure = {
35
38
  /** Stable provider error code, when one is safe to retain. */
36
39
  code?: string | undefined
37
40
  /** Failure boundary that rejected the provider request. */
38
- failure: 'http' | 'network' | 'payload' | 'timeout' | 'unknown'
41
+ failure: 'http' | 'network' | 'payload' | 'rate_limit' | 'timeout' | 'unknown'
39
42
  /** Provider identifier. */
40
43
  id: string
41
44
  /** Provider operation identifier. */
@@ -44,6 +47,20 @@ export type ProviderFailure = {
44
47
  status?: number | undefined
45
48
  }
46
49
 
50
+ /** Bounded operational result for one funding quote provider attempt. */
51
+ export type FundingProviderAttempt = {
52
+ /** Wall-clock duration of the provider attempt in milliseconds. */
53
+ durationMs: number
54
+ /** Failure class, present only when the provider attempt failed. */
55
+ failure?: ProviderFailure['failure'] | undefined
56
+ /** Provider identifier. */
57
+ id: FundingQuotes.ProviderId
58
+ /** Stable upstream operation identifier. */
59
+ operation: string
60
+ /** Provider attempt outcome. */
61
+ outcome: 'available' | 'failed' | 'thin' | 'unavailable'
62
+ }
63
+
47
64
  /** One canonical request log entry. */
48
65
  export type Entry = {
49
66
  /** `'hit'` when the edge response cache served the request without reaching the origin handlers. */
@@ -52,6 +69,8 @@ export type Entry = {
52
69
  duration: number
53
70
  /** Stable error code when the response is a JSON error envelope (e.g. `not_found`, `api_key_invalid`). */
54
71
  errorCode?: string | undefined
72
+ /** Bounded funding provider attempts made while serving the request. */
73
+ fundingProviderAttempts?: readonly FundingProviderAttempt[] | undefined
55
74
  /** Log level derived from the response status: 5xx `error`, 4xx `warn`, else `info`. */
56
75
  level: Level
57
76
  /** Request method. */
@@ -140,6 +159,9 @@ export function middleware<environment extends Environment = Environment>(
140
159
  // (the same pattern EdgeCache uses for its cross-layer reads).
141
160
  const principal = c.get('principal' as never) as Auth.Principal | undefined
142
161
  const errorCode = c.get('errorCode' as never) as string | undefined
162
+ const fundingProviderAttempts = c.get('fundingProviderAttempts' as never) as
163
+ | Entry['fundingProviderAttempts']
164
+ | undefined
143
165
  const provider = c.get('providerFailure' as never) as Entry['provider']
144
166
  const providerFailures = c.get('providerFailures' as never) as Entry['providerFailures']
145
167
 
@@ -170,6 +192,7 @@ export function middleware<environment extends Environment = Environment>(
170
192
  ...(c.get('edgeCacheStatus' as never) === 'hit' ? { cache: 'hit' as const } : {}),
171
193
  duration: round(duration),
172
194
  ...(errorCode === undefined ? {} : { errorCode }),
195
+ ...(fundingProviderAttempts === undefined ? {} : { fundingProviderAttempts }),
173
196
  level: status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info',
174
197
  method: c.req.method,
175
198
  path: url.pathname,