tapimo 0.7.0 → 0.7.2

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 (689) hide show
  1. package/dist/ApiKeys.d.ts +24 -7
  2. package/dist/ApiKeys.d.ts.map +1 -1
  3. package/dist/ApiKeys.js +17 -5
  4. package/dist/ApiKeys.js.map +1 -1
  5. package/dist/App.d.ts +12104 -7861
  6. package/dist/App.d.ts.map +1 -1
  7. package/dist/App.js +191 -128
  8. package/dist/App.js.map +1 -1
  9. package/dist/Assets.d.ts +10 -0
  10. package/dist/Assets.d.ts.map +1 -1
  11. package/dist/Assets.js +17 -4
  12. package/dist/Assets.js.map +1 -1
  13. package/dist/Scope.d.ts +19 -8
  14. package/dist/Scope.d.ts.map +1 -1
  15. package/dist/Scope.js +25 -9
  16. package/dist/Scope.js.map +1 -1
  17. package/dist/Webhooks.d.ts +1 -1
  18. package/dist/admin/App.d.ts +601 -20
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/App.js +12 -2
  21. package/dist/admin/App.js.map +1 -1
  22. package/dist/admin/apps/api-keys.d.ts +94 -11
  23. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  24. package/dist/admin/apps/api-keys.js +141 -32
  25. package/dist/admin/apps/api-keys.js.map +1 -1
  26. package/dist/admin/apps/earn-vaults.d.ts +1069 -0
  27. package/dist/admin/apps/earn-vaults.d.ts.map +1 -0
  28. package/dist/admin/apps/earn-vaults.js +409 -0
  29. package/dist/admin/apps/earn-vaults.js.map +1 -0
  30. package/dist/admin/apps/verified-tokens.d.ts +166 -166
  31. package/dist/admin/ui.generated.d.ts +1 -1
  32. package/dist/admin/ui.generated.d.ts.map +1 -1
  33. package/dist/admin/ui.generated.js +151 -95
  34. package/dist/admin/ui.generated.js.map +1 -1
  35. package/dist/analytics/Analytics.d.ts +2 -0
  36. package/dist/analytics/Analytics.d.ts.map +1 -1
  37. package/dist/analytics/Analytics.js +18 -2
  38. package/dist/analytics/Analytics.js.map +1 -1
  39. package/dist/analytics/tables/requestEvents.d.ts +22 -1
  40. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  41. package/dist/analytics/tables/requestEvents.js +59 -39
  42. package/dist/analytics/tables/requestEvents.js.map +1 -1
  43. package/dist/apps/Zones.d.ts +8335 -0
  44. package/dist/apps/Zones.d.ts.map +1 -0
  45. package/dist/apps/Zones.js +38 -0
  46. package/dist/apps/Zones.js.map +1 -0
  47. package/dist/apps/data/App.d.ts +14377 -10165
  48. package/dist/apps/data/App.d.ts.map +1 -1
  49. package/dist/apps/data/App.js +91 -19
  50. package/dist/apps/data/App.js.map +1 -1
  51. package/dist/apps/data/FxOracle.d.ts +1 -1
  52. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  53. package/dist/apps/data/index.d.ts +0 -1
  54. package/dist/apps/data/index.d.ts.map +1 -1
  55. package/dist/apps/data/index.js +0 -1
  56. package/dist/apps/data/index.js.map +1 -1
  57. package/dist/apps/data/routes/activities.d.ts +32 -32
  58. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  59. package/dist/apps/data/routes/activities.js +130 -65
  60. package/dist/apps/data/routes/activities.js.map +1 -1
  61. package/dist/apps/data/routes/addresses.d.ts +312 -312
  62. package/dist/apps/data/routes/balances.d.ts +18 -18
  63. package/dist/apps/data/routes/balances.js +5 -5
  64. package/dist/apps/data/routes/balances.js.map +1 -1
  65. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  66. package/dist/apps/data/routes/coingecko.d.ts.map +1 -1
  67. package/dist/apps/data/routes/coingecko.js +62 -35
  68. package/dist/apps/data/routes/coingecko.js.map +1 -1
  69. package/dist/apps/data/routes/earn.d.ts +2144 -0
  70. package/dist/apps/data/routes/earn.d.ts.map +1 -0
  71. package/dist/apps/data/routes/earn.js +947 -0
  72. package/dist/apps/data/routes/earn.js.map +1 -0
  73. package/dist/apps/data/routes/exchanges.d.ts +1231 -324
  74. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  75. package/dist/apps/data/routes/exchanges.js +656 -193
  76. package/dist/apps/data/routes/exchanges.js.map +1 -1
  77. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  78. package/dist/apps/data/routes/fee-amm.js +59 -8
  79. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  80. package/dist/apps/data/routes/funding.d.ts +4092 -387
  81. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  82. package/dist/apps/data/routes/funding.js +1403 -256
  83. package/dist/apps/data/routes/funding.js.map +1 -1
  84. package/dist/apps/data/routes/indexer.d.ts +5 -5
  85. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  86. package/dist/apps/data/routes/indexer.js +19 -2
  87. package/dist/apps/data/routes/indexer.js.map +1 -1
  88. package/dist/apps/data/routes/receipts.d.ts +265 -265
  89. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  90. package/dist/apps/data/routes/receipts.js +11 -11
  91. package/dist/apps/data/routes/receipts.js.map +1 -1
  92. package/dist/apps/data/routes/rpc.d.ts +5 -5
  93. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  94. package/dist/apps/data/routes/rpc.js +211 -22
  95. package/dist/apps/data/routes/rpc.js.map +1 -1
  96. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  97. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  98. package/dist/apps/data/routes/tokenlist.js +49 -9
  99. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  100. package/dist/apps/data/routes/tokens.d.ts +116 -88
  101. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  102. package/dist/apps/data/routes/tokens.js +198 -91
  103. package/dist/apps/data/routes/tokens.js.map +1 -1
  104. package/dist/apps/data/routes/transactions.d.ts +55 -55
  105. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  106. package/dist/apps/data/routes/transactions.js +78 -39
  107. package/dist/apps/data/routes/transactions.js.map +1 -1
  108. package/dist/apps/data/routes/transfers.d.ts +78 -29
  109. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  110. package/dist/apps/data/routes/transfers.js +165 -67
  111. package/dist/apps/data/routes/transfers.js.map +1 -1
  112. package/dist/apps/data/routes/valuation.d.ts +10 -10
  113. package/dist/apps/data/routes/valuation.js +2 -2
  114. package/dist/apps/data/routes/valuation.js.map +1 -1
  115. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  116. package/dist/apps/data/routes/verified-tokens.js +34 -9
  117. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  118. package/dist/apps/data/routes/webhooks.d.ts +5 -3
  119. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  120. package/dist/apps/data/routes/webhooks.js +175 -65
  121. package/dist/apps/data/routes/webhooks.js.map +1 -1
  122. package/dist/apps/data/routes/zones.d.ts +281 -0
  123. package/dist/apps/data/routes/zones.d.ts.map +1 -0
  124. package/dist/apps/data/routes/zones.js +252 -0
  125. package/dist/apps/data/routes/zones.js.map +1 -0
  126. package/dist/apps/docs/reference.js +6 -5
  127. package/dist/apps/docs/reference.js.map +1 -1
  128. package/dist/apps/index.d.ts +2 -1
  129. package/dist/apps/index.d.ts.map +1 -1
  130. package/dist/apps/index.js +2 -1
  131. package/dist/apps/index.js.map +1 -1
  132. package/dist/apps/management/App.d.ts +670 -670
  133. package/dist/apps/management/Billing.d.ts +8 -0
  134. package/dist/apps/management/Billing.d.ts.map +1 -1
  135. package/dist/apps/management/Billing.js +12 -0
  136. package/dist/apps/management/Billing.js.map +1 -1
  137. package/dist/apps/management/Error.d.ts +12 -0
  138. package/dist/apps/management/Error.d.ts.map +1 -0
  139. package/dist/apps/management/Error.js +10 -0
  140. package/dist/apps/management/Error.js.map +1 -0
  141. package/dist/apps/management/routes/api-keys.d.ts +5 -5
  142. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  143. package/dist/apps/management/routes/api-keys.js +2 -8
  144. package/dist/apps/management/routes/api-keys.js.map +1 -1
  145. package/dist/apps/management/routes/billing.d.ts +119 -119
  146. package/dist/apps/management/routes/faucet.d.ts +35 -35
  147. package/dist/apps/management/routes/invitations.d.ts +62 -62
  148. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  149. package/dist/apps/management/routes/invitations.js +4 -10
  150. package/dist/apps/management/routes/invitations.js.map +1 -1
  151. package/dist/apps/management/routes/invite-links.d.ts +30 -30
  152. package/dist/apps/management/routes/me.d.ts +6 -6
  153. package/dist/apps/management/routes/members.d.ts +78 -78
  154. package/dist/apps/management/routes/orgs.d.ts +48 -48
  155. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  156. package/dist/apps/management/routes/orgs.js +2 -8
  157. package/dist/apps/management/routes/orgs.js.map +1 -1
  158. package/dist/apps/management/routes/projects.d.ts +9 -9
  159. package/dist/apps/management/routes/scopes.js +2 -2
  160. package/dist/apps/management/routes/scopes.js.map +1 -1
  161. package/dist/apps/management/routes/usage.d.ts +80 -80
  162. package/dist/apps/management/routes/verified-token-requests.d.ts +36 -36
  163. package/dist/apps/management/routes/webhooks.d.ts +50 -50
  164. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  165. package/dist/apps/management/routes/webhooks.js +15 -3
  166. package/dist/apps/management/routes/webhooks.js.map +1 -1
  167. package/dist/apps/mpp/App.d.ts +55 -56
  168. package/dist/apps/mpp/App.d.ts.map +1 -1
  169. package/dist/apps/mpp/App.js +192 -27
  170. package/dist/apps/mpp/App.js.map +1 -1
  171. package/dist/apps/mpp/Mpp.d.ts +0 -20
  172. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  173. package/dist/apps/mpp/Mpp.js +0 -24
  174. package/dist/apps/mpp/Mpp.js.map +1 -1
  175. package/dist/apps/mpp/internal/Adapter.d.ts +40 -0
  176. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -0
  177. package/dist/apps/mpp/internal/Adapter.js +66 -0
  178. package/dist/apps/mpp/internal/Adapter.js.map +1 -0
  179. package/dist/apps/mpp/internal/Error.d.ts +4 -0
  180. package/dist/apps/mpp/internal/Error.d.ts.map +1 -0
  181. package/dist/apps/mpp/internal/Error.js +41 -0
  182. package/dist/apps/mpp/internal/Error.js.map +1 -0
  183. package/dist/apps/mpp/internal/Idempotency.d.ts +33 -0
  184. package/dist/apps/mpp/internal/Idempotency.d.ts.map +1 -0
  185. package/dist/apps/mpp/internal/Idempotency.js +28 -0
  186. package/dist/apps/mpp/internal/Idempotency.js.map +1 -0
  187. package/dist/apps/mpp/internal/Policy.d.ts +46 -0
  188. package/dist/apps/mpp/internal/Policy.d.ts.map +1 -0
  189. package/dist/apps/mpp/internal/Policy.js +57 -0
  190. package/dist/apps/mpp/internal/Policy.js.map +1 -0
  191. package/dist/apps/mpp/internal/ScreenManager.d.ts +27 -0
  192. package/dist/apps/mpp/internal/ScreenManager.d.ts.map +1 -0
  193. package/dist/apps/mpp/internal/ScreenManager.js +18 -0
  194. package/dist/apps/mpp/internal/ScreenManager.js.map +1 -0
  195. package/dist/apps/mpp/internal/Screening.d.ts +35 -0
  196. package/dist/apps/mpp/internal/Screening.d.ts.map +1 -0
  197. package/dist/apps/mpp/internal/Screening.js +20 -0
  198. package/dist/apps/mpp/internal/Screening.js.map +1 -0
  199. package/dist/apps/mpp/internal/Sponsorship.d.ts +36 -0
  200. package/dist/apps/mpp/internal/Sponsorship.d.ts.map +1 -0
  201. package/dist/apps/mpp/internal/Sponsorship.js +103 -0
  202. package/dist/apps/mpp/internal/Sponsorship.js.map +1 -0
  203. package/dist/apps/mpp/internal/Utils.d.ts +7 -0
  204. package/dist/apps/mpp/internal/Utils.d.ts.map +1 -0
  205. package/dist/apps/mpp/internal/Utils.js +24 -0
  206. package/dist/apps/mpp/internal/Utils.js.map +1 -0
  207. package/dist/apps/relay/App.js +184 -24
  208. package/dist/apps/relay/App.js.map +1 -1
  209. package/dist/apps/relay/Sponsorships.d.ts +4 -0
  210. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  211. package/dist/apps/relay/Sponsorships.js +16 -7
  212. package/dist/apps/relay/Sponsorships.js.map +1 -1
  213. package/dist/db/Db.d.ts +20 -0
  214. package/dist/db/Db.d.ts.map +1 -1
  215. package/dist/db/Db.js +342 -0
  216. package/dist/db/Db.js.map +1 -1
  217. package/dist/db/Schema.d.ts +921 -1
  218. package/dist/db/Schema.d.ts.map +1 -1
  219. package/dist/db/Schema.js +269 -1
  220. package/dist/db/Schema.js.map +1 -1
  221. package/dist/db/tables/earnVaults.d.ts +68 -0
  222. package/dist/db/tables/earnVaults.d.ts.map +1 -0
  223. package/dist/db/tables/earnVaults.js +72 -0
  224. package/dist/db/tables/earnVaults.js.map +1 -0
  225. package/dist/db/tables/fundingCatalog.d.ts +56 -0
  226. package/dist/db/tables/fundingCatalog.d.ts.map +1 -0
  227. package/dist/db/tables/fundingCatalog.js +85 -0
  228. package/dist/db/tables/fundingCatalog.js.map +1 -0
  229. package/dist/db/tables/fundingDepositAddresses.d.ts +159 -0
  230. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -0
  231. package/dist/db/tables/fundingDepositAddresses.js +206 -0
  232. package/dist/db/tables/fundingDepositAddresses.js.map +1 -0
  233. package/dist/db/tables/fundingDeposits.d.ts +115 -0
  234. package/dist/db/tables/fundingDeposits.d.ts.map +1 -0
  235. package/dist/db/tables/fundingDeposits.js +100 -0
  236. package/dist/db/tables/fundingDeposits.js.map +1 -0
  237. package/dist/db/tables/fundingIdempotency.d.ts +91 -0
  238. package/dist/db/tables/fundingIdempotency.d.ts.map +1 -0
  239. package/dist/db/tables/fundingIdempotency.js +103 -0
  240. package/dist/db/tables/fundingIdempotency.js.map +1 -0
  241. package/dist/db/tables/fundingTransferEvents.d.ts +12 -0
  242. package/dist/db/tables/fundingTransferEvents.d.ts.map +1 -0
  243. package/dist/db/tables/fundingTransferEvents.js +18 -0
  244. package/dist/db/tables/fundingTransferEvents.js.map +1 -0
  245. package/dist/db/tables/fundingTransferTransactions.d.ts +15 -0
  246. package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -0
  247. package/dist/db/tables/fundingTransferTransactions.js +27 -0
  248. package/dist/db/tables/fundingTransferTransactions.js.map +1 -0
  249. package/dist/db/tables/fundingTransfers.d.ts +82 -0
  250. package/dist/db/tables/fundingTransfers.d.ts.map +1 -0
  251. package/dist/db/tables/fundingTransfers.js +89 -0
  252. package/dist/db/tables/fundingTransfers.js.map +1 -0
  253. package/dist/db/tables/organizations.d.ts +8 -0
  254. package/dist/db/tables/organizations.d.ts.map +1 -1
  255. package/dist/db/tables/organizations.js +35 -0
  256. package/dist/db/tables/organizations.js.map +1 -1
  257. package/dist/db/tables/projects.d.ts +8 -0
  258. package/dist/db/tables/projects.d.ts.map +1 -1
  259. package/dist/db/tables/projects.js +16 -0
  260. package/dist/db/tables/projects.js.map +1 -1
  261. package/dist/db/tables/requestUsage.d.ts +13 -0
  262. package/dist/db/tables/requestUsage.d.ts.map +1 -1
  263. package/dist/db/tables/requestUsage.js +16 -0
  264. package/dist/db/tables/requestUsage.js.map +1 -1
  265. package/dist/db/tables/sponsoredTransactions.d.ts +4 -2
  266. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  267. package/dist/db/tables/sponsoredTransactions.js +14 -4
  268. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  269. package/dist/db/tables/users.d.ts +8 -0
  270. package/dist/db/tables/users.d.ts.map +1 -1
  271. package/dist/db/tables/users.js +16 -0
  272. package/dist/db/tables/users.js.map +1 -1
  273. package/dist/db/tables/verifiedTokens.d.ts +14 -7
  274. package/dist/db/tables/verifiedTokens.d.ts.map +1 -1
  275. package/dist/db/tables/verifiedTokens.js +9 -7
  276. package/dist/db/tables/verifiedTokens.js.map +1 -1
  277. package/dist/db/tables/webhookSubscriptions.d.ts +31 -1
  278. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  279. package/dist/db/tables/webhookSubscriptions.js +24 -3
  280. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  281. package/dist/handlers/relay.js +108 -108
  282. package/dist/handlers/relay.js.map +1 -1
  283. package/dist/internal/Auth.d.ts +26 -0
  284. package/dist/internal/Auth.d.ts.map +1 -1
  285. package/dist/internal/Auth.js +35 -2
  286. package/dist/internal/Auth.js.map +1 -1
  287. package/dist/internal/Cache.d.ts.map +1 -1
  288. package/dist/internal/Cache.js +16 -10
  289. package/dist/internal/Cache.js.map +1 -1
  290. package/dist/internal/Cursor.js +1 -1
  291. package/dist/internal/Cursor.js.map +1 -1
  292. package/dist/internal/EarnRates.d.ts +89 -0
  293. package/dist/internal/EarnRates.d.ts.map +1 -0
  294. package/dist/internal/EarnRates.js +266 -0
  295. package/dist/internal/EarnRates.js.map +1 -0
  296. package/dist/internal/EarnVaults.d.ts +171 -0
  297. package/dist/internal/EarnVaults.d.ts.map +1 -0
  298. package/dist/internal/EarnVaults.js +547 -0
  299. package/dist/internal/EarnVaults.js.map +1 -0
  300. package/dist/internal/EdgeCache.d.ts +9 -2
  301. package/dist/internal/EdgeCache.d.ts.map +1 -1
  302. package/dist/internal/EdgeCache.js +36 -9
  303. package/dist/internal/EdgeCache.js.map +1 -1
  304. package/dist/internal/Id.d.ts +11 -0
  305. package/dist/internal/Id.d.ts.map +1 -1
  306. package/dist/internal/Id.js +13 -0
  307. package/dist/internal/Id.js.map +1 -1
  308. package/dist/internal/Idempotency.d.ts +83 -0
  309. package/dist/internal/Idempotency.d.ts.map +1 -0
  310. package/dist/internal/Idempotency.js +113 -0
  311. package/dist/internal/Idempotency.js.map +1 -0
  312. package/dist/internal/Log.d.ts +64 -9
  313. package/dist/internal/Log.d.ts.map +1 -1
  314. package/dist/internal/Log.js +29 -7
  315. package/dist/internal/Log.js.map +1 -1
  316. package/dist/internal/MetricSink.d.ts.map +1 -1
  317. package/dist/internal/MetricSink.js +47 -1
  318. package/dist/internal/MetricSink.js.map +1 -1
  319. package/dist/internal/OpenApi.d.ts +1 -1
  320. package/dist/internal/OpenApi.d.ts.map +1 -1
  321. package/dist/internal/OpenApi.js +3 -0
  322. package/dist/internal/OpenApi.js.map +1 -1
  323. package/dist/internal/Provider.d.ts +14 -0
  324. package/dist/internal/Provider.d.ts.map +1 -0
  325. package/dist/internal/Provider.js +5 -0
  326. package/dist/internal/Provider.js.map +1 -0
  327. package/dist/internal/Schema.d.ts +8 -3
  328. package/dist/internal/Schema.d.ts.map +1 -1
  329. package/dist/internal/Schema.js +24 -13
  330. package/dist/internal/Schema.js.map +1 -1
  331. package/dist/internal/Store.d.ts +6 -0
  332. package/dist/internal/Store.d.ts.map +1 -1
  333. package/dist/internal/Store.js +35 -0
  334. package/dist/internal/Store.js.map +1 -1
  335. package/dist/internal/Tidx.d.ts +43 -24
  336. package/dist/internal/Tidx.d.ts.map +1 -1
  337. package/dist/internal/Tidx.js +79 -11
  338. package/dist/internal/Tidx.js.map +1 -1
  339. package/dist/internal/Viem.d.ts +82 -65
  340. package/dist/internal/Viem.d.ts.map +1 -1
  341. package/dist/internal/Viem.js +83 -12
  342. package/dist/internal/Viem.js.map +1 -1
  343. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  344. package/dist/internal/WebhookDestination.js +34 -28
  345. package/dist/internal/WebhookDestination.js.map +1 -1
  346. package/dist/internal/Webhooks.d.ts +9 -0
  347. package/dist/internal/Webhooks.d.ts.map +1 -1
  348. package/dist/internal/Webhooks.js +15 -10
  349. package/dist/internal/Webhooks.js.map +1 -1
  350. package/dist/internal/exchange/Provider.d.ts +219 -0
  351. package/dist/internal/exchange/Provider.d.ts.map +1 -0
  352. package/dist/internal/exchange/Provider.js +117 -0
  353. package/dist/internal/exchange/Provider.js.map +1 -0
  354. package/dist/internal/exchange/index.d.ts +2 -0
  355. package/dist/internal/exchange/index.d.ts.map +1 -0
  356. package/dist/internal/exchange/index.js +2 -0
  357. package/dist/internal/exchange/index.js.map +1 -0
  358. package/dist/internal/exchange/providers/uniswap.d.ts +57 -0
  359. package/dist/internal/exchange/providers/uniswap.d.ts.map +1 -0
  360. package/dist/internal/exchange/providers/uniswap.js +553 -0
  361. package/dist/internal/exchange/providers/uniswap.js.map +1 -0
  362. package/dist/internal/funding/Action.d.ts +55 -0
  363. package/dist/internal/funding/Action.d.ts.map +1 -0
  364. package/dist/internal/funding/Action.js +74 -0
  365. package/dist/internal/funding/Action.js.map +1 -0
  366. package/dist/internal/funding/Catalog.d.ts +107 -0
  367. package/dist/internal/funding/Catalog.d.ts.map +1 -0
  368. package/dist/internal/funding/Catalog.js +270 -0
  369. package/dist/internal/funding/Catalog.js.map +1 -0
  370. package/dist/internal/funding/Chain.d.ts +76 -0
  371. package/dist/internal/funding/Chain.d.ts.map +1 -0
  372. package/dist/internal/funding/Chain.js +38 -0
  373. package/dist/internal/funding/Chain.js.map +1 -0
  374. package/dist/internal/funding/Deposit.d.ts +366 -0
  375. package/dist/internal/funding/Deposit.d.ts.map +1 -0
  376. package/dist/internal/funding/Deposit.js +286 -0
  377. package/dist/internal/funding/Deposit.js.map +1 -0
  378. package/dist/internal/funding/DepositAddress.d.ts +345 -0
  379. package/dist/internal/funding/DepositAddress.d.ts.map +1 -0
  380. package/dist/internal/funding/DepositAddress.js +189 -0
  381. package/dist/internal/funding/DepositAddress.js.map +1 -0
  382. package/dist/internal/funding/Evidence.d.ts +52 -0
  383. package/dist/internal/funding/Evidence.d.ts.map +1 -0
  384. package/dist/internal/funding/Evidence.js +314 -0
  385. package/dist/internal/funding/Evidence.js.map +1 -0
  386. package/dist/internal/funding/Provider.d.ts +1186 -0
  387. package/dist/internal/funding/Provider.d.ts.map +1 -0
  388. package/dist/internal/funding/Provider.js +1081 -0
  389. package/dist/internal/funding/Provider.js.map +1 -0
  390. package/dist/internal/funding/Reconciliation.d.ts +113 -0
  391. package/dist/internal/funding/Reconciliation.d.ts.map +1 -0
  392. package/dist/internal/funding/Reconciliation.js +676 -0
  393. package/dist/internal/funding/Reconciliation.js.map +1 -0
  394. package/dist/internal/funding/Route.d.ts +21 -0
  395. package/dist/internal/funding/Route.d.ts.map +1 -0
  396. package/dist/internal/funding/Route.js +5 -0
  397. package/dist/internal/funding/Route.js.map +1 -0
  398. package/dist/internal/funding/Subsidy.d.ts +113 -0
  399. package/dist/internal/funding/Subsidy.d.ts.map +1 -0
  400. package/dist/internal/funding/Subsidy.js +138 -0
  401. package/dist/internal/funding/Subsidy.js.map +1 -0
  402. package/dist/internal/funding/Token.d.ts +41 -0
  403. package/dist/internal/funding/Token.d.ts.map +1 -0
  404. package/dist/internal/funding/Token.js +22 -0
  405. package/dist/internal/funding/Token.js.map +1 -0
  406. package/dist/internal/funding/Transfer.d.ts +643 -0
  407. package/dist/internal/funding/Transfer.d.ts.map +1 -0
  408. package/dist/internal/funding/Transfer.js +332 -0
  409. package/dist/internal/funding/Transfer.js.map +1 -0
  410. package/dist/internal/funding/index.d.ts +13 -0
  411. package/dist/internal/funding/index.d.ts.map +1 -0
  412. package/dist/internal/funding/index.js +13 -0
  413. package/dist/internal/funding/index.js.map +1 -0
  414. package/dist/internal/funding/providers/across.d.ts +21 -0
  415. package/dist/internal/funding/providers/across.d.ts.map +1 -0
  416. package/dist/internal/funding/providers/across.js +117 -0
  417. package/dist/internal/funding/providers/across.js.map +1 -0
  418. package/dist/internal/funding/providers/relay.d.ts +46 -0
  419. package/dist/internal/funding/providers/relay.d.ts.map +1 -0
  420. package/dist/internal/funding/providers/relay.js +464 -0
  421. package/dist/internal/funding/providers/relay.js.map +1 -0
  422. package/dist/internal/funding/providers/rhino.d.ts +15 -0
  423. package/dist/internal/funding/providers/rhino.d.ts.map +1 -0
  424. package/dist/internal/funding/providers/rhino.js +116 -0
  425. package/dist/internal/funding/providers/rhino.js.map +1 -0
  426. package/dist/internal/funding/providers/squid.d.ts +19 -0
  427. package/dist/internal/funding/providers/squid.d.ts.map +1 -0
  428. package/dist/internal/funding/providers/squid.js +120 -0
  429. package/dist/internal/funding/providers/squid.js.map +1 -0
  430. package/dist/internal/funding/providers/symbiosis.d.ts +20 -0
  431. package/dist/internal/funding/providers/symbiosis.d.ts.map +1 -0
  432. package/dist/internal/funding/providers/symbiosis.js +176 -0
  433. package/dist/internal/funding/providers/symbiosis.js.map +1 -0
  434. package/dist/internal/index.d.ts +5 -1
  435. package/dist/internal/index.d.ts.map +1 -1
  436. package/dist/internal/index.js +5 -1
  437. package/dist/internal/index.js.map +1 -1
  438. package/dist/internal/webhookDestinations/url.js +61 -57
  439. package/dist/internal/webhookDestinations/url.js.map +1 -1
  440. package/dist/providers/index.d.ts +43 -0
  441. package/dist/providers/index.d.ts.map +1 -0
  442. package/dist/providers/index.js +19 -0
  443. package/dist/providers/index.js.map +1 -0
  444. package/package.json +9 -4
  445. package/src/ApiKeys.test.ts +11 -7
  446. package/src/ApiKeys.ts +37 -9
  447. package/src/App.test.ts +85 -10
  448. package/src/App.ts +224 -137
  449. package/src/Assets.ts +29 -4
  450. package/src/Client.test-d.ts +650 -30
  451. package/src/Scope.test.ts +19 -0
  452. package/src/Scope.ts +28 -11
  453. package/src/Webhooks.test.ts +25 -0
  454. package/src/Webhooks.ts +1 -1
  455. package/src/admin/App.test.ts +9 -2
  456. package/src/admin/App.ts +32 -9
  457. package/src/admin/apps/api-keys.test.ts +167 -23
  458. package/src/admin/apps/api-keys.ts +166 -28
  459. package/src/admin/apps/earn-vaults.test.ts +338 -0
  460. package/src/admin/apps/earn-vaults.ts +572 -0
  461. package/src/admin/apps/scopes.test.ts +16 -1
  462. package/src/admin/ui/plugin.ts +2 -2
  463. package/src/admin/ui/src/components/EarnVaultForm.tsx +137 -0
  464. package/src/admin/ui/src/components/MobileProfile.tsx +2 -2
  465. package/src/admin/ui/src/components/Page.tsx +1 -1
  466. package/src/admin/ui/src/components/ThemeToggle.tsx +1 -1
  467. package/src/admin/ui/src/lib/api-keys.ts +37 -7
  468. package/src/admin/ui/src/lib/client.ts +1 -1
  469. package/src/admin/ui/src/lib/config.ts +1 -1
  470. package/src/admin/ui/src/lib/early-access.ts +8 -9
  471. package/src/admin/ui/src/lib/earn-vaults.ts +207 -0
  472. package/src/admin/ui/src/lib/format.ts +13 -0
  473. package/src/admin/ui/src/lib/organizations.test.ts +14 -4
  474. package/src/admin/ui/src/lib/organizations.ts +7 -1
  475. package/src/admin/ui/src/lib/response.ts +22 -0
  476. package/src/admin/ui/src/lib/scopes.test.ts +19 -11
  477. package/src/admin/ui/src/lib/scopes.ts +28 -7
  478. package/src/admin/ui/src/lib/verified-tokens.ts +10 -9
  479. package/src/admin/ui/src/routeTree.gen.ts +70 -7
  480. package/src/admin/ui/src/router.tsx +2 -2
  481. package/src/admin/ui/src/routes/__root.tsx +9 -8
  482. package/src/admin/ui/src/routes/api-keys.tsx +475 -64
  483. package/src/admin/ui/src/routes/early-access.tsx +5 -12
  484. package/src/admin/ui/src/routes/earn.vaults.tsx +348 -0
  485. package/src/admin/ui/src/routes/earn.vaults_.$vaultId.edit.tsx +154 -0
  486. package/src/admin/ui/src/routes/earn.vaults_.new.tsx +190 -0
  487. package/src/admin/ui/src/routes/organizations.tsx +4 -7
  488. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +56 -54
  489. package/src/admin/ui/src/routes/verified-tokens.tsx +4 -4
  490. package/src/admin/ui/vite.config.ts +4 -1
  491. package/src/admin/ui.generated.ts +189 -95
  492. package/src/analytics/Analytics.test.ts +69 -10
  493. package/src/analytics/Analytics.ts +20 -1
  494. package/src/analytics/tables/requestEvents.ts +79 -37
  495. package/src/apps/Zones.test.ts +33 -0
  496. package/src/apps/Zones.ts +40 -0
  497. package/src/apps/composition.test.ts +84 -1
  498. package/src/apps/data/App.test.ts +38 -9
  499. package/src/apps/data/App.ts +96 -33
  500. package/src/apps/data/FxOracle.ts +1 -1
  501. package/src/apps/data/index.ts +0 -1
  502. package/src/apps/data/routes/activities.test.ts +82 -6
  503. package/src/apps/data/routes/activities.ts +131 -71
  504. package/src/apps/data/routes/balances.test.ts +50 -37
  505. package/src/apps/data/routes/balances.ts +5 -5
  506. package/src/apps/data/routes/blocks.test.ts +2 -0
  507. package/src/apps/data/routes/coingecko.test.ts +22 -0
  508. package/src/apps/data/routes/coingecko.ts +84 -48
  509. package/src/apps/data/routes/earn.test.ts +1108 -0
  510. package/src/apps/data/routes/earn.ts +1391 -0
  511. package/src/apps/data/routes/exchanges.test.ts +82 -33
  512. package/src/apps/data/routes/exchanges.ts +842 -227
  513. package/src/apps/data/routes/fee-amm.test.ts +4 -6
  514. package/src/apps/data/routes/fee-amm.ts +69 -12
  515. package/src/apps/data/routes/funding.test.ts +2712 -221
  516. package/src/apps/data/routes/funding.ts +1604 -387
  517. package/src/apps/data/routes/indexer.test.ts +43 -34
  518. package/src/apps/data/routes/indexer.ts +19 -2
  519. package/src/apps/data/routes/receipts.test.ts +5 -3
  520. package/src/apps/data/routes/receipts.ts +13 -11
  521. package/src/apps/data/routes/rpc.test.ts +446 -28
  522. package/src/apps/data/routes/rpc.ts +243 -25
  523. package/src/apps/data/routes/tokenlist.test.ts +15 -0
  524. package/src/apps/data/routes/tokenlist.ts +88 -22
  525. package/src/apps/data/routes/tokens.ts +262 -86
  526. package/src/apps/data/routes/transactions.test.ts +2 -0
  527. package/src/apps/data/routes/transactions.ts +77 -42
  528. package/src/apps/data/routes/transfers.test.ts +166 -4
  529. package/src/apps/data/routes/transfers.ts +216 -78
  530. package/src/apps/data/routes/valuation.test.ts +62 -9
  531. package/src/apps/data/routes/valuation.ts +2 -2
  532. package/src/apps/data/routes/verified-tokens.test.ts +51 -0
  533. package/src/apps/data/routes/verified-tokens.ts +52 -15
  534. package/src/apps/data/routes/webhooks.test.ts +432 -33
  535. package/src/apps/data/routes/webhooks.ts +189 -79
  536. package/src/apps/data/routes/zones.test.ts +473 -0
  537. package/src/apps/data/routes/zones.ts +338 -0
  538. package/src/apps/docs/reference.ts +8 -6
  539. package/src/apps/index.ts +2 -1
  540. package/src/apps/management/Billing.test.ts +29 -0
  541. package/src/apps/management/Billing.ts +23 -0
  542. package/src/apps/management/Error.ts +12 -0
  543. package/src/apps/management/routes/api-keys.test.ts +5 -3
  544. package/src/apps/management/routes/api-keys.ts +2 -9
  545. package/src/apps/management/routes/invitations.ts +4 -11
  546. package/src/apps/management/routes/orgs.ts +2 -9
  547. package/src/apps/management/routes/scopes.test.ts +7 -2
  548. package/src/apps/management/routes/scopes.ts +2 -2
  549. package/src/apps/management/routes/webhooks.test.ts +6 -1
  550. package/src/apps/management/routes/webhooks.ts +15 -3
  551. package/src/apps/mpp/App.ts +276 -29
  552. package/src/apps/mpp/Mpp.test.ts +631 -26
  553. package/src/apps/mpp/Mpp.ts +0 -42
  554. package/src/apps/mpp/internal/Adapter.ts +108 -3
  555. package/src/apps/mpp/internal/Error.test.ts +33 -0
  556. package/src/apps/mpp/internal/Error.ts +44 -6
  557. package/src/apps/mpp/internal/Idempotency.test.ts +213 -0
  558. package/src/apps/mpp/internal/Idempotency.ts +59 -5
  559. package/src/apps/mpp/internal/Policy.test.ts +122 -0
  560. package/src/apps/mpp/internal/Policy.ts +98 -0
  561. package/src/apps/mpp/internal/ScreenManager.test.ts +68 -0
  562. package/src/apps/mpp/internal/ScreenManager.ts +41 -0
  563. package/src/apps/mpp/internal/Screening.ts +40 -3
  564. package/src/apps/mpp/internal/Sponsorship.ts +144 -0
  565. package/src/apps/mpp/internal/Utils.ts +30 -0
  566. package/src/apps/relay/App.test.ts +495 -64
  567. package/src/apps/relay/App.ts +234 -24
  568. package/src/apps/relay/Sponsorships.test.ts +51 -0
  569. package/src/apps/relay/Sponsorships.ts +19 -7
  570. package/src/db/Db.ts +439 -0
  571. package/src/db/Schema.ts +325 -2
  572. package/src/db/tables/earnVaults.test.ts +185 -0
  573. package/src/db/tables/earnVaults.ts +151 -0
  574. package/src/db/tables/fundingCatalog.test.ts +108 -0
  575. package/src/db/tables/fundingCatalog.ts +157 -0
  576. package/src/db/tables/fundingDepositAddresses.test.ts +580 -0
  577. package/src/db/tables/fundingDepositAddresses.ts +376 -0
  578. package/src/db/tables/fundingDeposits.test.ts +261 -0
  579. package/src/db/tables/fundingDeposits.ts +229 -0
  580. package/src/db/tables/fundingIdempotency.test.ts +182 -0
  581. package/src/db/tables/fundingIdempotency.ts +180 -0
  582. package/src/db/tables/fundingTransferEvents.ts +28 -0
  583. package/src/db/tables/fundingTransferTransactions.test.ts +69 -0
  584. package/src/db/tables/fundingTransferTransactions.ts +37 -0
  585. package/src/db/tables/fundingTransfers.test.ts +89 -0
  586. package/src/db/tables/fundingTransfers.ts +153 -0
  587. package/src/db/tables/organizations.test.ts +88 -0
  588. package/src/db/tables/organizations.ts +42 -1
  589. package/src/db/tables/projects.ts +16 -0
  590. package/src/db/tables/requestUsage.ts +23 -0
  591. package/src/db/tables/sponsoredTransactions.test.ts +52 -2
  592. package/src/db/tables/sponsoredTransactions.ts +18 -6
  593. package/src/db/tables/users.ts +16 -0
  594. package/src/db/tables/verifiedTokens.ts +19 -14
  595. package/src/db/tables/webhookSubscriptions.ts +58 -3
  596. package/src/handlers/relay.test.ts +2 -0
  597. package/src/handlers/relay.ts +95 -99
  598. package/src/internal/Auth.test.ts +48 -0
  599. package/src/internal/Auth.ts +61 -2
  600. package/src/internal/Cache.ts +13 -9
  601. package/src/internal/Cursor.test.ts +2 -2
  602. package/src/internal/Cursor.ts +1 -1
  603. package/src/internal/EarnRates.test.ts +125 -0
  604. package/src/internal/EarnRates.ts +409 -0
  605. package/src/internal/EarnVaults.test.ts +195 -0
  606. package/src/internal/EarnVaults.ts +876 -0
  607. package/src/internal/EdgeCache.test.ts +37 -5
  608. package/src/internal/EdgeCache.ts +39 -7
  609. package/src/internal/Id.ts +14 -0
  610. package/src/internal/Idempotency.test.ts +109 -0
  611. package/src/internal/Idempotency.ts +237 -0
  612. package/src/internal/Log.test-d.ts +41 -8
  613. package/src/internal/Log.test.ts +189 -8
  614. package/src/internal/Log.ts +124 -17
  615. package/src/internal/MetricSink.test.ts +244 -4
  616. package/src/internal/MetricSink.ts +51 -1
  617. package/src/internal/OpenApi.ts +3 -0
  618. package/src/internal/Provider.test.ts +17 -0
  619. package/src/internal/Provider.ts +17 -0
  620. package/src/internal/Schema.test.ts +13 -0
  621. package/src/internal/Schema.ts +41 -24
  622. package/src/internal/Store.test.ts +25 -0
  623. package/src/internal/Store.ts +46 -0
  624. package/src/internal/Tidx.local.test.ts +78 -3
  625. package/src/internal/Tidx.test.ts +212 -18
  626. package/src/internal/Tidx.ts +112 -32
  627. package/src/internal/VerifiedTokens.test.ts +15 -0
  628. package/src/internal/Viem.test.ts +170 -17
  629. package/src/internal/Viem.ts +111 -28
  630. package/src/internal/WebhookDestination.ts +25 -23
  631. package/src/internal/Webhooks.test.ts +59 -0
  632. package/src/internal/Webhooks.ts +34 -10
  633. package/src/internal/exchange/Provider.test.ts +216 -0
  634. package/src/internal/exchange/Provider.ts +326 -0
  635. package/src/internal/exchange/index.test.ts +6 -0
  636. package/src/internal/exchange/index.ts +1 -0
  637. package/src/internal/exchange/providers/uniswap.test.ts +122 -0
  638. package/src/internal/exchange/providers/uniswap.ts +682 -0
  639. package/src/internal/funding/Action.test.ts +138 -0
  640. package/src/internal/funding/Action.ts +117 -0
  641. package/src/internal/funding/Catalog.test.ts +311 -0
  642. package/src/internal/funding/Catalog.ts +347 -0
  643. package/src/internal/funding/Chain.test.ts +79 -0
  644. package/src/internal/funding/Chain.ts +120 -0
  645. package/src/internal/funding/Deposit.test.ts +26 -0
  646. package/src/internal/funding/Deposit.ts +453 -0
  647. package/src/internal/funding/DepositAddress.test.ts +18 -0
  648. package/src/internal/funding/DepositAddress.ts +312 -0
  649. package/src/internal/funding/Evidence.test.ts +297 -0
  650. package/src/internal/funding/Evidence.ts +430 -0
  651. package/src/internal/funding/Provider.test.ts +1328 -0
  652. package/src/internal/funding/Provider.ts +1796 -0
  653. package/src/internal/funding/Reconciliation.local.test.ts +504 -0
  654. package/src/internal/funding/Reconciliation.test.ts +2020 -0
  655. package/src/internal/funding/Reconciliation.ts +930 -0
  656. package/src/internal/funding/Route.test.ts +66 -0
  657. package/src/internal/funding/Route.ts +29 -0
  658. package/src/internal/funding/Subsidy.local.test.ts +152 -0
  659. package/src/internal/funding/Subsidy.test.ts +75 -0
  660. package/src/internal/funding/Subsidy.ts +260 -0
  661. package/src/internal/funding/Token.test.ts +47 -0
  662. package/src/internal/funding/Token.ts +59 -0
  663. package/src/internal/funding/Transfer.test.ts +369 -0
  664. package/src/internal/funding/Transfer.ts +522 -0
  665. package/src/internal/funding/index.test.ts +20 -0
  666. package/src/internal/funding/index.ts +12 -0
  667. package/src/internal/funding/providers/across.ts +149 -0
  668. package/src/{apps/data/FundingQuoteProviders.test.ts → internal/funding/providers/index.test.ts} +178 -157
  669. package/src/internal/funding/providers/relay.test.ts +474 -0
  670. package/src/internal/funding/providers/relay.ts +559 -0
  671. package/src/internal/funding/providers/rhino.ts +139 -0
  672. package/src/internal/funding/providers/squid.ts +151 -0
  673. package/src/internal/funding/providers/symbiosis.ts +221 -0
  674. package/src/internal/index.test.ts +5 -1
  675. package/src/internal/index.ts +5 -1
  676. package/src/internal/webhookDestinations/url.ts +49 -45
  677. package/src/providers/index.test.ts +14 -0
  678. package/src/providers/index.ts +54 -0
  679. package/dist/apps/data/FundingQuoteProviders.d.ts +0 -174
  680. package/dist/apps/data/FundingQuoteProviders.d.ts.map +0 -1
  681. package/dist/apps/data/FundingQuoteProviders.js +0 -775
  682. package/dist/apps/data/FundingQuoteProviders.js.map +0 -1
  683. package/dist/internal/FundingQuotes.d.ts +0 -864
  684. package/dist/internal/FundingQuotes.d.ts.map +0 -1
  685. package/dist/internal/FundingQuotes.js +0 -672
  686. package/dist/internal/FundingQuotes.js.map +0 -1
  687. package/src/apps/data/FundingQuoteProviders.ts +0 -1043
  688. package/src/internal/FundingQuotes.test.ts +0 -456
  689. package/src/internal/FundingQuotes.ts +0 -944
@@ -1,13 +1,89 @@
1
+ import { Hash, Hex, Value } from 'ox'
1
2
  import { Schema } from 'tapimo'
3
+ import { decodeFunctionData, erc20Abi, maxUint256 } from 'viem'
4
+ import { base } from 'viem/chains'
5
+ import { tempoMainnet } from 'viem/tempo/chains'
2
6
 
3
- import type * as FundingQuotes from '../../../internal/FundingQuotes.js'
7
+ import * as Anvil from '../../../../test/Anvil.js'
8
+ import * as FundingDepositAddresses from '../../../db/tables/fundingDepositAddresses.js'
9
+ import * as FundingIdempotencyTable from '../../../db/tables/fundingIdempotency.js'
10
+ import * as Cache from '../../../internal/Cache.js'
11
+ import * as Cursor from '../../../internal/Cursor.js'
12
+ import * as FundingCatalog from '../../../internal/funding/Catalog.js'
13
+ import * as FundingDeposit from '../../../internal/funding/Deposit.js'
14
+ import * as FundingDepositAddress from '../../../internal/funding/DepositAddress.js'
15
+ import * as FundingProvider from '../../../internal/funding/Provider.js'
16
+ import * as FundingTransfer from '../../../internal/funding/Transfer.js'
17
+ import * as Idempotency from '../../../internal/Idempotency.js'
18
+ import * as Across from '../../../internal/funding/providers/across.js'
19
+ import * as Relay from '../../../internal/funding/providers/relay.js'
20
+ import * as Rhino from '../../../internal/funding/providers/rhino.js'
21
+ import * as Squid from '../../../internal/funding/providers/squid.js'
22
+ import * as Symbiosis from '../../../internal/funding/providers/symbiosis.js'
4
23
  import type * as Log from '../../../internal/Log.js'
24
+ import * as Store from '../../../internal/Store.js'
5
25
  import * as TestApp from '../../../../test/App.js'
26
+ import * as TestFunding from '../../../../test/Funding.js'
6
27
  import * as Funding from './funding.js'
7
28
 
29
+ const db = TestApp.database()
30
+ const relayApiKey = process.env.RELAY_API_KEY
31
+
32
+ const providers = [
33
+ Across.across({
34
+ apiKey: 'across-api-key',
35
+ integratorId: '0xdead',
36
+ userAddress: '0x1111111111111111111111111111111111111111',
37
+ }),
38
+ Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
39
+ Rhino.rhino(),
40
+ Squid.squid({
41
+ integratorId: 'squid-integrator-id',
42
+ userAddress: '0x1111111111111111111111111111111111111111',
43
+ }),
44
+ Symbiosis.symbiosis({ userAddress: '0x1111111111111111111111111111111111111111' }),
45
+ ] as const
46
+
47
+ /**
48
+ * Providers with executable coverage on the static Base USDC → pathUSD
49
+ * corridor. Each entry gets a fork execution suite (live quote, calls executed
50
+ * on an anvil Base fork); covering another provider is one entry here.
51
+ */
52
+ const executableProviders = [
53
+ {
54
+ create: (sender: string) =>
55
+ Relay.relay({
56
+ ...(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
57
+ userAddress: sender,
58
+ }),
59
+ id: 'relay',
60
+ },
61
+ ] as const
62
+
63
+ beforeAll(() => TestFunding.publish(db))
64
+
65
+ function create(options: TestApp.create.Options = {}) {
66
+ return TestApp.create({ ...options, db })
67
+ }
68
+
69
+ describe('funding OpenAPI', () => {
70
+ test('publishes get operation ids for funding inventory', async () => {
71
+ const app = create({ auth: false })
72
+ const spec = await (await app.request('/openapi.json')).json()
73
+
74
+ expect({
75
+ chains: spec.paths['/v1/funding/chains'].get.operationId,
76
+ providers: spec.paths['/v1/funding/providers'].get.operationId,
77
+ }).toEqual({
78
+ chains: 'getFundingChains',
79
+ providers: 'getFundingProviders',
80
+ })
81
+ })
82
+ })
83
+
8
84
  describe('GET /funding/quotes', () => {
9
85
  test('publishes alphabetically ordered quote example fields', async () => {
10
- const app = TestApp.create({ auth: false })
86
+ const app = create({ auth: false })
11
87
  const spec = await (await app.request('/openapi.json')).json()
12
88
  const operation = spec.paths['/v1/funding/quotes'].get
13
89
  const response = operation.responses['200'].content['application/json']
@@ -17,7 +93,7 @@ describe('GET /funding/quotes', () => {
17
93
  })
18
94
 
19
95
  test('returns successful live quotes with output amounts', async () => {
20
- const app = TestApp.create({ funding: { adapters: { relay: relayAdapter() } } })
96
+ const app = create({ providers: [relayProvider()] })
21
97
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
22
98
  expect(response.status).toBe(200)
23
99
  expect(response.headers.get('cache-control')).toBe('no-store')
@@ -47,20 +123,84 @@ describe('GET /funding/quotes', () => {
47
123
  expect(body.data[0]?.destinationToken).not.toHaveProperty('amount')
48
124
  })
49
125
 
126
+ test('resolves token identifiers through the composed route', async () => {
127
+ const app = create({ providers: [relayProvider()] })
128
+ const identifiers = [
129
+ { destinationToken: 'usdc.e', name: 'symbol', sourceToken: 'USDC' },
130
+ { destinationToken: 'usdce', name: 'slug', sourceToken: 'usdc' },
131
+ {
132
+ destinationToken: '0x20c000000000000000000000b9537d11c60e8b50',
133
+ name: 'address',
134
+ sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
135
+ },
136
+ {
137
+ destinationToken: 'eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50',
138
+ name: 'token key',
139
+ sourceToken: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
140
+ },
141
+ ] as const
142
+ const results = []
143
+ for (const identifier of identifiers) {
144
+ const response = await app.request(
145
+ quoteUrl({
146
+ destinationToken: identifier.destinationToken,
147
+ sourceAmount: '1000000',
148
+ sourceChain: 'base',
149
+ sourceToken: identifier.sourceToken,
150
+ }),
151
+ { headers: TestApp.auth.headers },
152
+ )
153
+ const body = await TestApp.json(response, Funding.schema.quoteFunding.Response)
154
+ results.push({
155
+ destinationToken: body.data[0]?.destinationToken.symbol,
156
+ name: identifier.name,
157
+ sourceToken: body.data[0]?.sourceToken.symbol,
158
+ status: response.status,
159
+ })
160
+ }
161
+
162
+ expect(results).toMatchInlineSnapshot(`
163
+ [
164
+ {
165
+ "destinationToken": "USDC.e",
166
+ "name": "symbol",
167
+ "sourceToken": "USDC",
168
+ "status": 200,
169
+ },
170
+ {
171
+ "destinationToken": "USDC.e",
172
+ "name": "slug",
173
+ "sourceToken": "USDC",
174
+ "status": 200,
175
+ },
176
+ {
177
+ "destinationToken": "USDC.e",
178
+ "name": "address",
179
+ "sourceToken": "USDC",
180
+ "status": 200,
181
+ },
182
+ {
183
+ "destinationToken": "USDC.e",
184
+ "name": "token key",
185
+ "sourceToken": "USDC",
186
+ "status": 200,
187
+ },
188
+ ]
189
+ `)
190
+ })
191
+
50
192
  test('queries only the requested provider', async () => {
51
193
  let acrossCalls = 0
52
194
  let relayCalls = 0
53
- const app = TestApp.create({
54
- funding: {
55
- adapters: {
56
- across: acrossAdapter(() => {
57
- acrossCalls += 1
58
- }),
59
- relay: relayAdapter(() => {
60
- relayCalls += 1
61
- }),
62
- },
63
- },
195
+ const app = create({
196
+ providers: [
197
+ acrossProvider(() => {
198
+ acrossCalls += 1
199
+ }),
200
+ relayProvider(() => {
201
+ relayCalls += 1
202
+ }),
203
+ ],
64
204
  })
65
205
  const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
66
206
  headers: TestApp.auth.headers,
@@ -74,7 +214,7 @@ describe('GET /funding/quotes', () => {
74
214
  })
75
215
 
76
216
  test('returns non-EVM Rhino quotes with stable chain and token ids', async () => {
77
- const app = TestApp.create({ funding: { adapters: { rhino: rhinoAdapter() } } })
217
+ const app = create({ providers: [rhinoProvider()] })
78
218
  const response = await app.request(
79
219
  quoteUrl({
80
220
  destinationToken: 'USDC.e',
@@ -103,8 +243,8 @@ describe('GET /funding/quotes', () => {
103
243
  })
104
244
  })
105
245
 
106
- test('returns an empty list when no provider is configured', async () => {
107
- const response = await TestApp.create().request(quoteUrl(), {
246
+ test('returns an empty list when no provider is available', async () => {
247
+ const response = await create().request(quoteUrl(), {
108
248
  headers: TestApp.auth.headers,
109
249
  })
110
250
  expect(response.status).toBe(200)
@@ -114,19 +254,16 @@ describe('GET /funding/quotes', () => {
114
254
 
115
255
  test('returns an empty list when the provider reports no quote', async () => {
116
256
  const entries: Log.Entry[] = []
117
- const app = TestApp.create({
118
- funding: {
119
- adapters: {
120
- relay: {
121
- async probeQuote() {
122
- return { sampledAt: '2026-01-01T00:00:00Z', status: 'unavailable' }
123
- },
124
- },
125
- },
126
- },
257
+ const app = create({
127
258
  logger: (entry) => {
128
259
  entries.push(entry)
129
260
  },
261
+ providers: [
262
+ withGetQuote('relay', async () => ({
263
+ sampledAt: '2026-01-01T00:00:00Z',
264
+ status: 'unavailable',
265
+ })),
266
+ ],
130
267
  })
131
268
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
132
269
  expect(response.status).toBe(200)
@@ -136,23 +273,19 @@ describe('GET /funding/quotes', () => {
136
273
  {
137
274
  durationMs: expect.any(Number),
138
275
  id: 'relay',
139
- operation: 'quote/v2',
276
+ operation: 'getQuote',
140
277
  outcome: 'unavailable',
141
278
  },
142
279
  ])
143
280
  })
144
281
 
145
282
  test('returns 502 when every matching provider fails', async () => {
146
- const app = TestApp.create({
147
- funding: {
148
- adapters: {
149
- relay: {
150
- async probeQuote() {
151
- throw new Error('upstream unavailable')
152
- },
153
- },
154
- },
155
- },
283
+ const app = create({
284
+ providers: [
285
+ withGetQuote('relay', async () => {
286
+ throw new Error('upstream unavailable')
287
+ }),
288
+ ],
156
289
  })
157
290
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
158
291
  expect(response.status).toBe(502)
@@ -161,64 +294,58 @@ describe('GET /funding/quotes', () => {
161
294
  })
162
295
 
163
296
  test('logs provider quote deadlines as timeouts', async () => {
297
+ vi.useFakeTimers()
164
298
  const entries: Log.Entry[] = []
165
- const app = TestApp.create({
166
- funding: {
167
- adapterTimeoutMs: 1,
168
- adapters: {
169
- across: {
170
- probeQuote() {
171
- return new Promise<FundingQuotes.QuoteProbeResult>(() => {})
172
- },
173
- },
174
- },
175
- },
299
+ const app = create({
176
300
  logger: (entry) => {
177
301
  entries.push(entry)
178
302
  },
303
+ providers: [
304
+ withGetQuote('across', () => new Promise<FundingProvider.getQuote.ReturnType>(() => {})),
305
+ ],
179
306
  })
180
307
 
181
- const response = await app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
182
- headers: TestApp.auth.headers,
183
- })
308
+ try {
309
+ const response_promise = app.request(quoteUrl({ ...baseUsdcQuery, provider: 'across' }), {
310
+ headers: TestApp.auth.headers,
311
+ })
312
+ await vi.advanceTimersByTimeAsync(2_000)
313
+ const response = await response_promise
184
314
 
185
- expect(response.status).toBe(502)
186
- expect(entries[0]?.provider).toEqual({
187
- failure: 'timeout',
188
- id: 'across',
189
- operation: 'swap/approval',
190
- })
191
- expect(entries[0]?.fundingProviderAttempts).toEqual([
192
- {
193
- durationMs: expect.any(Number),
315
+ expect(response.status).toBe(502)
316
+ expect(entries[0]?.provider).toEqual({
194
317
  failure: 'timeout',
195
318
  id: 'across',
196
- operation: 'swap/approval',
197
- outcome: 'failed',
198
- },
199
- ])
319
+ operation: 'getQuote',
320
+ })
321
+ expect(entries[0]?.fundingProviderAttempts).toEqual([
322
+ {
323
+ durationMs: expect.any(Number),
324
+ failure: 'timeout',
325
+ id: 'across',
326
+ operation: 'getQuote',
327
+ outcome: 'failed',
328
+ },
329
+ ])
330
+ } finally {
331
+ vi.useRealTimers()
332
+ }
200
333
  })
201
334
 
202
335
  test('logs every failure when multiple providers fail', async () => {
203
336
  const entries: Log.Entry[] = []
204
- const app = TestApp.create({
205
- funding: {
206
- adapters: {
207
- across: {
208
- async probeQuote() {
209
- throw new TypeError('Across network failure')
210
- },
211
- },
212
- relay: {
213
- async probeQuote() {
214
- throw new Error('Relay failure')
215
- },
216
- },
217
- },
218
- },
337
+ const app = create({
219
338
  logger: (entry) => {
220
339
  entries.push(entry)
221
340
  },
341
+ providers: [
342
+ withGetQuote('across', async () => {
343
+ throw new TypeError('Across network failure')
344
+ }),
345
+ withGetQuote('relay', async () => {
346
+ throw new Error('Relay failure')
347
+ }),
348
+ ],
222
349
  })
223
350
 
224
351
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
@@ -227,25 +354,25 @@ describe('GET /funding/quotes', () => {
227
354
  expect(entries[0]?.provider).toEqual({
228
355
  failure: 'network',
229
356
  id: 'across',
230
- operation: 'swap/approval',
357
+ operation: 'getQuote',
231
358
  })
232
359
  expect(entries[0]?.providerFailures).toEqual([
233
- { failure: 'network', id: 'across', operation: 'swap/approval' },
234
- { failure: 'unknown', id: 'relay', operation: 'quote/v2' },
360
+ { failure: 'network', id: 'across', operation: 'getQuote' },
361
+ { failure: 'unknown', id: 'relay', operation: 'getQuote' },
235
362
  ])
236
363
  expect(entries[0]?.fundingProviderAttempts).toEqual([
237
364
  {
238
365
  durationMs: expect.any(Number),
239
366
  failure: 'network',
240
367
  id: 'across',
241
- operation: 'swap/approval',
368
+ operation: 'getQuote',
242
369
  outcome: 'failed',
243
370
  },
244
371
  {
245
372
  durationMs: expect.any(Number),
246
373
  failure: 'unknown',
247
374
  id: 'relay',
248
- operation: 'quote/v2',
375
+ operation: 'getQuote',
249
376
  outcome: 'failed',
250
377
  },
251
378
  ])
@@ -253,20 +380,16 @@ describe('GET /funding/quotes', () => {
253
380
 
254
381
  test('logs failed providers when another provider returns a quote', async () => {
255
382
  const entries: Log.Entry[] = []
256
- const app = TestApp.create({
257
- funding: {
258
- adapters: {
259
- across: {
260
- async probeQuote() {
261
- throw new TypeError(`wallet ${'0xaa'.repeat(20)} could not reach secret-provider`)
262
- },
263
- },
264
- relay: relayAdapter(),
265
- },
266
- },
383
+ const app = create({
267
384
  logger: (entry) => {
268
385
  entries.push(entry)
269
386
  },
387
+ providers: [
388
+ withGetQuote('across', async () => {
389
+ throw new TypeError(`wallet ${'0xaa'.repeat(20)} could not reach secret-provider`)
390
+ }),
391
+ relayProvider(),
392
+ ],
270
393
  })
271
394
 
272
395
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
@@ -277,13 +400,13 @@ describe('GET /funding/quotes', () => {
277
400
  durationMs: expect.any(Number),
278
401
  failure: 'network',
279
402
  id: 'across',
280
- operation: 'swap/approval',
403
+ operation: 'getQuote',
281
404
  outcome: 'failed',
282
405
  },
283
406
  {
284
407
  durationMs: expect.any(Number),
285
408
  id: 'relay',
286
- operation: 'quote/v2',
409
+ operation: 'getQuote',
287
410
  outcome: 'available',
288
411
  },
289
412
  ])
@@ -292,23 +415,20 @@ describe('GET /funding/quotes', () => {
292
415
  })
293
416
 
294
417
  test('rejects successful provider results without output amounts', async () => {
295
- const app = TestApp.create({
296
- funding: {
297
- adapters: {
298
- relay: {
299
- async probeQuote() {
300
- return { sampledAt: '2026-01-01T00:00:00Z', status: 'available' }
301
- },
302
- },
303
- },
304
- },
418
+ const app = create({
419
+ providers: [
420
+ withGetQuote('relay', async () => ({
421
+ sampledAt: '2026-01-01T00:00:00Z',
422
+ status: 'available',
423
+ })),
424
+ ],
305
425
  })
306
426
  const response = await app.request(quoteUrl(), { headers: TestApp.auth.headers })
307
427
  expect(response.status).toBe(502)
308
428
  })
309
429
 
310
430
  test('rejects invalid query parameters', async () => {
311
- const response = await TestApp.create().request(
431
+ const response = await create().request(
312
432
  quoteUrl({ sourceAmount: '1000000', sourceChain: 'base' }),
313
433
  { headers: TestApp.auth.headers },
314
434
  )
@@ -318,19 +438,20 @@ describe('GET /funding/quotes', () => {
318
438
  })
319
439
 
320
440
  test('rejects unsupported providers', async () => {
321
- const response = await TestApp.create().request(
322
- quoteUrl({ ...baseUsdcQuery, provider: 'bridge' }),
323
- { headers: TestApp.auth.headers },
324
- )
441
+ const response = await create().request(quoteUrl({ ...baseUsdcQuery, provider: 'bridge' }), {
442
+ headers: TestApp.auth.headers,
443
+ })
325
444
  expect(response.status).toBe(400)
326
445
  const body = await TestApp.json(response, Schema.ErrorResponse)
327
446
  expect(body.error.code).toBe('query_invalid')
328
447
  })
329
448
 
330
449
  test('rejects unsupported source chains', async () => {
331
- const response = await TestApp.create().request(
450
+ const response = await create().request(
332
451
  quoteUrl({ ...baseUsdcQuery, sourceChain: 'bitcoin' }),
333
- { headers: TestApp.auth.headers },
452
+ {
453
+ headers: TestApp.auth.headers,
454
+ },
334
455
  )
335
456
  expect(response.status).toBe(400)
336
457
  const body = await TestApp.json(response, Schema.ErrorResponse)
@@ -338,7 +459,7 @@ describe('GET /funding/quotes', () => {
338
459
  })
339
460
 
340
461
  test('does not expose the removed routes endpoint', async () => {
341
- const response = await TestApp.create().request(
462
+ const response = await create().request(
342
463
  '/v1/funding/routes?sourceChain=base&sourceToken=USDC&sourceAmount=1000000',
343
464
  { headers: TestApp.auth.headers },
344
465
  )
@@ -348,14 +469,17 @@ describe('GET /funding/quotes', () => {
348
469
 
349
470
  describe('GET /funding/chains', () => {
350
471
  test('lists only inputs supported by quote providers', async () => {
351
- const response = await TestApp.client().v1.funding.chains.$get(undefined, TestApp.auth)
472
+ const response = await create({ providers }).request('/v1/funding/chains', {
473
+ headers: TestApp.auth.headers,
474
+ })
352
475
  expect(response.status).toBe(200)
353
- const body = await TestApp.json(response, Funding.schema.listFundingChains.Response)
476
+ const body = await TestApp.json(response, Funding.schema.getFundingChains.Response)
354
477
 
355
478
  expect(body.data.map((chain) => chain.id)).toEqual([
356
479
  'eip155:1',
357
- 'eip155:8453',
358
480
  'eip155:56',
481
+ 'eip155:8453',
482
+ 'eip155:137',
359
483
  'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
360
484
  'tron:0x2b6653dc',
361
485
  ])
@@ -369,135 +493,2502 @@ describe('GET /funding/chains', () => {
369
493
  })
370
494
 
371
495
  describe('GET /funding/providers', () => {
372
- test('lists quote providers and their configuration status', async () => {
373
- const app = TestApp.create({ funding: { adapters: { relay: relayAdapter() } } })
496
+ test('lists supplied quote providers', async () => {
497
+ const app = create({ providers: [relayProvider()] })
374
498
  const response = await app.request('/v1/funding/providers', {
375
499
  headers: TestApp.auth.headers,
376
500
  })
377
501
  expect(response.status).toBe(200)
378
- const body = await TestApp.json(response, Funding.schema.listProviders.Response)
502
+ const body = await TestApp.json(response, Funding.schema.getFundingProviders.Response)
379
503
 
380
504
  expect(body.data).toEqual([
381
- {
382
- id: 'across',
383
- kind: 'intentBridge',
384
- name: 'Across',
385
- requiresApiKey: true,
386
- status: 'unconfigured',
387
- },
388
505
  {
389
506
  id: 'relay',
390
- kind: 'solverNetwork',
391
507
  name: 'Relay',
392
- requiresApiKey: false,
393
- status: 'configured',
394
508
  },
509
+ ])
510
+ })
511
+
512
+ test('isolates cached inventories between apps', async () => {
513
+ const store = Store.memory()
514
+ const relayApp = create({ cache: { store }, providers: [relayProvider()] })
515
+ const acrossApp = create({ cache: { store }, providers: [acrossProvider()] })
516
+
517
+ const relayResponse = await relayApp.request('/v1/funding/providers', {
518
+ headers: TestApp.auth.headers,
519
+ })
520
+ const acrossResponse = await acrossApp.request('/v1/funding/providers', {
521
+ headers: TestApp.auth.headers,
522
+ })
523
+ const relayBody = await TestApp.json(relayResponse, Funding.schema.getFundingProviders.Response)
524
+ const acrossBody = await TestApp.json(
525
+ acrossResponse,
526
+ Funding.schema.getFundingProviders.Response,
527
+ )
528
+
529
+ expect(relayBody.data.map((provider) => provider.id)).toEqual(['relay'])
530
+ expect(acrossBody.data.map((provider) => provider.id)).toEqual(['across'])
531
+ })
532
+
533
+ test('isolates cached inventories between database sources', async () => {
534
+ const first = TestApp.database()
535
+ const second = TestApp.database()
536
+ const store = Store.memory()
537
+ await TestFunding.publish(first)
538
+ await FundingCatalog.publish(second, { ...TestFunding.data, routes: [] })
539
+ const firstApp = TestApp.create({ cache: { store }, db: first, providers: [relayProvider()] })
540
+ const secondApp = TestApp.create({ cache: { store }, db: second, providers: [relayProvider()] })
541
+
542
+ const firstResponse = await firstApp.request('/v1/funding/providers', {
543
+ headers: TestApp.auth.headers,
544
+ })
545
+ const secondResponse = await secondApp.request('/v1/funding/providers', {
546
+ headers: TestApp.auth.headers,
547
+ })
548
+ const firstBody = await TestApp.json(firstResponse, Funding.schema.getFundingProviders.Response)
549
+ const secondBody = await TestApp.json(
550
+ secondResponse,
551
+ Funding.schema.getFundingProviders.Response,
552
+ )
553
+
554
+ expect(firstBody.data.map((provider) => provider.id)).toEqual(['relay'])
555
+ expect(secondBody.data).toEqual([])
556
+ })
557
+
558
+ test('does not expose the removed generic provider endpoint', async () => {
559
+ const response = await create().request('/v1/providers', {
560
+ headers: TestApp.auth.headers,
561
+ })
562
+ expect(response.status).toBe(404)
563
+ })
564
+ })
565
+
566
+ describe('POST /funding/deposit-addresses', () => {
567
+ test('requires funding:write and an idempotency key', async () => {
568
+ const app = transfersApp({ providers: [depositAddressProvider()] })
569
+ expect((await postDepositAddress(app, { headers: {} })).status).toBe(401)
570
+ expect((await postDepositAddress(app, { key: fundingReader })).status).toBe(403)
571
+ const response = await postDepositAddress(app, {
572
+ headers: { authorization: `Bearer ${fundingWriter.token}` },
573
+ })
574
+ expect(response.status).toBe(400)
575
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
576
+ 'idempotency_key_required',
577
+ )
578
+ })
579
+
580
+ test('requires a source amount', async () => {
581
+ const { amount: _amount, ...body } = depositAddressBody
582
+ const response = await postDepositAddress(
583
+ transfersApp({ providers: [depositAddressProvider()] }),
584
+ { body },
585
+ )
586
+
587
+ expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
588
+ {
589
+ body: { requestId: expect.any(String) },
590
+ },
591
+ `
592
+ {
593
+ "body": {
594
+ "error": {
595
+ "code": "body_invalid",
596
+ "details": [
597
+ {
598
+ "message": "Invalid input: expected string, received undefined",
599
+ "path": [
600
+ "amount",
601
+ ],
602
+ },
603
+ ],
604
+ "message": "Check the request body and try again.",
605
+ },
606
+ "requestId": Any<String>,
607
+ },
608
+ "status": 400,
609
+ }
610
+ `,
611
+ )
612
+ })
613
+
614
+ test('creates a reusable address and persists private provider correlation', async () => {
615
+ let seen: FundingProvider.createDepositAddress.Parameters | undefined
616
+ const app = transfersApp({
617
+ providers: [
618
+ depositAddressProvider('TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV', (input) => {
619
+ seen = input
620
+ }),
621
+ ],
622
+ })
623
+ const response = await postDepositAddress(app, {
624
+ body: { ...depositAddressBody, destinationToken: 'usdt0', sourceToken: 'usdt' },
625
+ })
626
+ expect(response.status).toBe(200)
627
+ expect(response.headers.get('cache-control')).toBe('no-store')
628
+ const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
629
+ expect(body).toMatchObject({
630
+ address: 'TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV',
631
+ destinationToken: { symbol: 'USDT0' },
632
+ provider: { id: 'relay', name: 'Relay' },
633
+ recipient: depositAddressBody.recipient,
634
+ refundAddress: depositAddressBody.refundAddress,
635
+ sourceChain: { id: 'tron:0x2b6653dc' },
636
+ sourceToken: { symbol: 'USDT' },
637
+ status: 'active',
638
+ })
639
+ expect(seen).toMatchObject({
640
+ recipient: depositAddressBody.recipient,
641
+ refundAddress: depositAddressBody.refundAddress,
642
+ })
643
+ const record = await FundingDepositAddresses.get(db, body.id)
644
+ expect(record).toMatchObject({
645
+ deliveryStrategy: 'provider',
646
+ providerOutputToken: body.destinationToken,
647
+ providerRequestIds: ['relay_request_1'],
648
+ providerState: { checkEndpoint: '/intents/status/v3?requestId=relay_request_1' },
649
+ })
650
+ })
651
+
652
+ test('resolves token identifiers through the composed route', async () => {
653
+ const app = transfersApp({
654
+ providers: [depositAddressProvider('T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb')],
655
+ })
656
+ const identifiers = [
657
+ { destinationToken: 'usdt0', name: 'alias', sourceToken: 'usdt' },
395
658
  {
396
- id: 'rhino',
397
- kind: 'bridge',
398
- name: 'Rhino',
399
- requiresApiKey: false,
400
- status: 'unconfigured',
659
+ destinationToken: depositAddressBody.destinationToken,
660
+ name: 'address',
661
+ sourceToken: depositAddressBody.sourceToken,
401
662
  },
402
663
  {
403
- id: 'squid',
404
- kind: 'solverNetwork',
405
- name: 'Squid',
406
- requiresApiKey: true,
407
- status: 'unconfigured',
664
+ destinationToken: 'eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73',
665
+ name: 'token key',
666
+ sourceToken: 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
408
667
  },
668
+ ] as const
669
+ const results = []
670
+ for (const identifier of identifiers) {
671
+ const response = await postDepositAddress(app, {
672
+ body: {
673
+ ...depositAddressBody,
674
+ destinationToken: identifier.destinationToken,
675
+ recipient: `0x${'55'.repeat(20)}`,
676
+ refundAddress: 'TD5gsCwxykWsLN9aPrq2TAfNjByuZKYp4E',
677
+ sourceToken: identifier.sourceToken,
678
+ },
679
+ })
680
+ const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
681
+ results.push({
682
+ destinationToken: body.destinationToken.symbol,
683
+ name: identifier.name,
684
+ sourceToken: body.sourceToken.symbol,
685
+ status: response.status,
686
+ })
687
+ }
688
+
689
+ expect(results).toMatchInlineSnapshot(`
690
+ [
691
+ {
692
+ "destinationToken": "USDT0",
693
+ "name": "alias",
694
+ "sourceToken": "USDT",
695
+ "status": 200,
696
+ },
697
+ {
698
+ "destinationToken": "USDT0",
699
+ "name": "address",
700
+ "sourceToken": "USDT",
701
+ "status": 200,
702
+ },
703
+ {
704
+ "destinationToken": "USDT0",
705
+ "name": "token key",
706
+ "sourceToken": "USDT",
707
+ "status": 200,
708
+ },
709
+ ]
710
+ `)
711
+ })
712
+
713
+ test('omits newly quoted terms from a matching reusable address', async () => {
714
+ let calls = 0
715
+ const body = { ...depositAddressBody, refundAddress: 'TD5gsCwxykWsLN9aPrq2TAfNjByuZKYp4E' }
716
+ const app = transfersApp({
717
+ providers: [
718
+ depositAddressProvider('TEdvoHEatmDKvTh3o9vBRB9Vdtbhn4QFhy', () => {
719
+ calls += 1
720
+ }),
721
+ ],
722
+ })
723
+ const first = await TestApp.json(
724
+ await postDepositAddress(app, { body, idempotencyKey: nextDepositAddressKey() }),
725
+ Funding.schema.createFundingDepositAddress.Response,
726
+ )
727
+ const second = await TestApp.json(
728
+ await postDepositAddress(app, {
729
+ body: { ...body, amount: '200000000' },
730
+ idempotencyKey: nextDepositAddressKey(),
731
+ }),
732
+ Funding.schema.createFundingDepositAddress.Response,
733
+ )
734
+ if (!('sourceAmount' in first)) throw new Error('Expected newly provisioned terms.')
735
+
736
+ expect({
737
+ calls,
738
+ firstHasQuote: 'quote' in first,
739
+ firstId: first.id,
740
+ firstSourceAmount: first.sourceAmount.baseUnits,
741
+ secondHasQuote: 'quote' in second,
742
+ secondId: second.id,
743
+ }).toMatchInlineSnapshot(
409
744
  {
410
- id: 'symbiosis',
411
- kind: 'crossChainDex',
412
- name: 'Symbiosis',
413
- requiresApiKey: false,
414
- status: 'unconfigured',
745
+ firstId: expect.any(String),
746
+ secondId: expect.any(String),
415
747
  },
416
- ])
748
+ `
749
+ {
750
+ "calls": 2,
751
+ "firstHasQuote": true,
752
+ "firstId": Any<String>,
753
+ "firstSourceAmount": "100000000",
754
+ "secondHasQuote": false,
755
+ "secondId": Any<String>,
756
+ }
757
+ `,
758
+ )
759
+ expect(second.id).toBe(first.id)
417
760
  })
418
761
 
419
- test('does not expose the removed generic provider endpoint', async () => {
420
- const response = await TestApp.create().request('/v1/providers', {
421
- headers: TestApp.auth.headers,
762
+ test('stores subsidized addresses as a separate reusable identity', async () => {
763
+ const inputs: FundingProvider.createDepositAddress.Parameters[] = []
764
+ const app = transfersApp({
765
+ providers: [
766
+ withCreateDepositAddress('relay', async (input) => {
767
+ inputs.push(input)
768
+ return {
769
+ address: inputs.length === 1 ? `0x${'31'.repeat(20)}` : `0x${'32'.repeat(20)}`,
770
+ correlation: { requestId: `relay_request_${inputs.length}` },
771
+ destinationAmount: '1000000',
772
+ destinationAmountMin: '1000000',
773
+ fees: [],
774
+ sampledAt: '2026-08-04T00:00:00.000Z',
775
+ }
776
+ }),
777
+ ],
422
778
  })
423
- expect(response.status).toBe(404)
779
+ const body = {
780
+ ...depositAddressBody,
781
+ refundAddress: `0x${'33'.repeat(20)}`,
782
+ sourceChain: 'base',
783
+ sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
784
+ }
785
+ const standard = await TestApp.json(
786
+ await postDepositAddress(app, { body }),
787
+ Funding.schema.createFundingDepositAddress.Response,
788
+ )
789
+ const subsidized = await TestApp.json(
790
+ await postDepositAddress(app, { body: { ...body, subsidize: true } }),
791
+ Funding.schema.createFundingDepositAddress.Response,
792
+ )
793
+
794
+ expect({
795
+ distinct: standard.id !== subsidized.id,
796
+ inputs: inputs.map(({ subsidize }) => subsidize),
797
+ standard: standard.subsidize,
798
+ subsidized: subsidized.subsidize,
799
+ }).toMatchInlineSnapshot(`
800
+ {
801
+ "distinct": true,
802
+ "inputs": [
803
+ false,
804
+ true,
805
+ ],
806
+ "standard": false,
807
+ "subsidized": true,
808
+ }
809
+ `)
424
810
  })
425
- })
426
811
 
427
- const baseUsdcQuery = {
428
- sourceAmount: '1000000',
429
- sourceChain: 'base',
430
- sourceToken: 'USDC',
431
- } as const
812
+ test('normalizes EVM accounts before provisioning and matching', async () => {
813
+ const inputs: FundingProvider.createDepositAddress.Parameters[] = []
814
+ const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
815
+ const refundAddress = '0x1234567890AbcdEF1234567890aBcdef12345678'
816
+ const body = {
817
+ ...depositAddressBody,
818
+ recipient,
819
+ refundAddress,
820
+ sourceChain: 'base',
821
+ sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
822
+ }
823
+ const app = transfersApp({
824
+ providers: [
825
+ depositAddressProvider(`0x${'99'.repeat(20)}`, (input) => {
826
+ inputs.push(input)
827
+ }),
828
+ ],
829
+ })
830
+ const first = await TestApp.json(
831
+ await postDepositAddress(app, { body, idempotencyKey: nextDepositAddressKey() }),
832
+ Funding.schema.createFundingDepositAddress.Response,
833
+ )
834
+ const second = await TestApp.json(
835
+ await postDepositAddress(app, {
836
+ body: {
837
+ ...body,
838
+ recipient: recipient.toLowerCase(),
839
+ refundAddress: refundAddress.toLowerCase(),
840
+ },
841
+ idempotencyKey: nextDepositAddressKey(),
842
+ }),
843
+ Funding.schema.createFundingDepositAddress.Response,
844
+ )
432
845
 
433
- type QuoteQuery = Record<string, string>
846
+ expect({
847
+ first: { recipient: first.recipient, refundAddress: first.refundAddress },
848
+ inputs: inputs.map(({ recipient, refundAddress }) => ({ recipient, refundAddress })),
849
+ reused: first.id === second.id,
850
+ second: { recipient: second.recipient, refundAddress: second.refundAddress },
851
+ }).toMatchInlineSnapshot(`
852
+ {
853
+ "first": {
854
+ "recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
855
+ "refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
856
+ },
857
+ "inputs": [
858
+ {
859
+ "recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
860
+ "refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
861
+ },
862
+ {
863
+ "recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
864
+ "refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
865
+ },
866
+ ],
867
+ "reused": true,
868
+ "second": {
869
+ "recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
870
+ "refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
871
+ },
872
+ }
873
+ `)
874
+ })
434
875
 
435
- function quoteUrl(query: QuoteQuery = baseUsdcQuery) {
436
- return `/v1/funding/quotes?${new URLSearchParams(
437
- Object.entries(query).map(([key, value]) => [key, String(value)]),
438
- )}`
439
- }
876
+ test('keeps the stored reusable address when the provider rotates addresses', async () => {
877
+ const body = { ...depositAddressBody, recipient: `0x${'22'.repeat(20)}` }
878
+ const first = await TestApp.json(
879
+ await postDepositAddress(
880
+ transfersApp({
881
+ providers: [depositAddressProvider('TGCAjMXComunWZEXCT1LPBdcYbDVuyexBv')],
882
+ }),
883
+ { body, idempotencyKey: nextDepositAddressKey() },
884
+ ),
885
+ Funding.schema.createFundingDepositAddress.Response,
886
+ )
440
887
 
441
- function relayAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAdapter {
442
- return {
443
- async probeQuote(input, signal) {
444
- onProbe()
445
- expect(signal.aborted).toBe(false)
446
- expect(input.candidate.id).toBe('base-usdc-tempo-usdce-relay')
447
- return {
448
- minimumOutputAmount: '980000',
449
- outputAmount: '990000',
450
- quality: {
451
- estimatedSeconds: 4,
452
- liquiditySource: 'providerQuote',
453
- sourceDetail: 'relay:quote-v2',
454
- tier: 'liquid',
455
- },
456
- sampledAt: '2026-01-01T00:00:00Z',
457
- status: 'available',
888
+ let calls = 0
889
+ const app = transfersApp({
890
+ providers: [
891
+ depositAddressProvider('THkQfRopincF6emzbk6VMC7jTHqJ8MP8g7', () => {
892
+ calls += 1
893
+ }),
894
+ ],
895
+ })
896
+ const idempotencyKey = nextDepositAddressKey()
897
+ const response = await postDepositAddress(app, {
898
+ body: { ...body, amount: '200000000' },
899
+ idempotencyKey,
900
+ })
901
+ const second = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
902
+
903
+ expect({
904
+ address: second.address,
905
+ calls,
906
+ hasQuote: 'quote' in second,
907
+ reused: second.id === first.id,
908
+ status: response.status,
909
+ }).toMatchInlineSnapshot(
910
+ {
911
+ address: expect.any(String),
912
+ },
913
+ `
914
+ {
915
+ "address": Any<String>,
916
+ "calls": 1,
917
+ "hasQuote": false,
918
+ "reused": true,
919
+ "status": 200,
458
920
  }
459
- },
460
- }
461
- }
921
+ `,
922
+ )
923
+ expect(second.address).toBe(first.address)
924
+ expect(
925
+ await TestApp.json(
926
+ await postDepositAddress(app, {
927
+ body: { ...body, amount: '200000000' },
928
+ idempotencyKey,
929
+ }),
930
+ Funding.schema.createFundingDepositAddress.Response,
931
+ ),
932
+ ).toEqual(second)
933
+ expect(calls).toBe(1)
934
+ })
462
935
 
463
- function acrossAdapter(onProbe: () => void = () => {}): FundingQuotes.ProviderAdapter {
464
- return {
465
- async probeQuote(input, signal) {
466
- onProbe()
467
- expect(signal.aborted).toBe(false)
468
- expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across')
469
- return {
470
- expiresAt: '2026-01-01T00:01:00Z',
471
- minimumOutputAmount: '988000',
472
- outputAmount: '990000',
473
- quality: {
474
- estimatedSeconds: 2,
475
- liquiditySource: 'providerQuote',
476
- sourceDetail: 'across:swap-approval',
477
- tier: 'liquid',
478
- },
479
- sampledAt: '2026-01-01T00:00:00Z',
480
- status: 'available',
936
+ test('rejects a refund address from another chain', async () => {
937
+ const response = await postDepositAddress(
938
+ transfersApp({ providers: [depositAddressProvider()] }),
939
+ { body: { ...depositAddressBody, refundAddress: `0x${'11'.repeat(20)}` } },
940
+ )
941
+
942
+ expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
943
+ {
944
+ body: { requestId: expect.any(String) },
945
+ },
946
+ `
947
+ {
948
+ "body": {
949
+ "error": {
950
+ "code": "body_invalid",
951
+ "message": "Invalid \`refundAddress\`.",
952
+ },
953
+ "requestId": Any<String>,
954
+ },
955
+ "status": 400,
481
956
  }
482
- },
483
- }
484
- }
957
+ `,
958
+ )
959
+ })
485
960
 
486
- function rhinoAdapter(): FundingQuotes.ProviderAdapter {
487
- return {
488
- async probeQuote(input, signal) {
489
- expect(signal.aborted).toBe(false)
490
- expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino')
491
- return {
492
- outputAmount: '983000',
493
- quality: {
494
- liquiditySource: 'providerQuote',
495
- sourceDetail: 'rhino:bridge-swap-public',
496
- tier: 'liquid',
497
- },
498
- sampledAt: '2026-01-01T00:00:00Z',
499
- status: 'available',
961
+ test('replays matching idempotent requests and rejects changed input', async () => {
962
+ const body = { ...depositAddressBody, refundAddress: 'TKJebW6SdoJhgkKU13BeKCbrMzT6HeQhJ6' }
963
+ const app = transfersApp({
964
+ providers: [depositAddressProvider('TLrtXaP4Yp1AGqrwQLGoHD5yGh4tTAvPbe')],
965
+ })
966
+ const idempotencyKey = nextDepositAddressKey()
967
+ const first = await postDepositAddress(app, { body, idempotencyKey })
968
+ const replay = await postDepositAddress(app, { body, idempotencyKey })
969
+ expect(replay.status).toBe(200)
970
+ expect(await replay.json()).toEqual(await first.json())
971
+
972
+ const conflict = await postDepositAddress(app, {
973
+ body: { ...depositAddressBody, refundAddress: 'TNR8TefgTphcrwQQodMxFDa6BPggc2CZeE' },
974
+ idempotencyKey,
975
+ })
976
+ expect(conflict.status).toBe(409)
977
+ expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe(
978
+ 'idempotency_conflict',
979
+ )
980
+ })
981
+
982
+ test('defaults subsidize for legacy idempotency replays', async () => {
983
+ const app = transfersApp({
984
+ providers: [depositAddressProvider(`0x${'45'.repeat(20)}`)],
985
+ })
986
+ const requestBody = {
987
+ ...depositAddressBody,
988
+ refundAddress: `0x${'44'.repeat(20)}`,
989
+ sourceChain: 'base',
990
+ sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
991
+ }
992
+ const idempotencyKey = nextDepositAddressKey()
993
+ const first = await postDepositAddress(app, { body: requestBody, idempotencyKey })
994
+ const response = await TestApp.json(first, Funding.schema.createFundingDepositAddress.Response)
995
+ const { subsidize: _, ...legacy } = response
996
+ await db.kysely
997
+ .updateTable('funding_idempotency_requests')
998
+ .set({ response: JSON.stringify(legacy) })
999
+ .where('apiKeyId', '=', fundingWriter.id)
1000
+ .where('keyHash', '=', Idempotency.inputHash(idempotencyKey))
1001
+ .returningAll()
1002
+ .executeTakeFirstOrThrow()
1003
+
1004
+ const replay = await postDepositAddress(app, { body: requestBody, idempotencyKey })
1005
+ const response_replay = await TestApp.json(
1006
+ replay,
1007
+ Funding.schema.createFundingDepositAddress.Response,
1008
+ )
1009
+
1010
+ expect({ status: replay.status, subsidize: response_replay.subsidize }).toMatchInlineSnapshot(`
1011
+ {
1012
+ "status": 200,
1013
+ "subsidize": false,
500
1014
  }
501
- },
502
- }
1015
+ `)
1016
+ })
1017
+
1018
+ test('preserves the idempotency fingerprint when subsidize is omitted', async () => {
1019
+ const idempotencyKey = nextDepositAddressKey()
1020
+ await FundingIdempotencyTable.claim(db, {
1021
+ apiKeyId: fundingWriter.id,
1022
+ keyHash: Idempotency.inputHash(idempotencyKey),
1023
+ requestHash: Idempotency.inputHash({
1024
+ body: depositAddressBody,
1025
+ endpoint: 'POST /v1/funding/deposit-addresses',
1026
+ }),
1027
+ ttlMs: 60_000,
1028
+ })
1029
+
1030
+ const response = await postDepositAddress(transfersApp(), { idempotencyKey })
1031
+
1032
+ expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
1033
+ {
1034
+ body: { requestId: expect.any(String) },
1035
+ },
1036
+ `
1037
+ {
1038
+ "body": {
1039
+ "error": {
1040
+ "code": "idempotency_in_progress",
1041
+ "message": "An equivalent funding deposit address creation is still running.",
1042
+ },
1043
+ "requestId": Any<String>,
1044
+ },
1045
+ "status": 409,
1046
+ }
1047
+ `,
1048
+ )
1049
+ })
1050
+
1051
+ test('includes the creation endpoint in the idempotency fingerprint', async () => {
1052
+ const idempotencyKey = nextDepositAddressKey()
1053
+ await FundingIdempotencyTable.claim(db, {
1054
+ apiKeyId: fundingWriter.id,
1055
+ keyHash: Idempotency.inputHash(idempotencyKey),
1056
+ requestHash: Idempotency.inputHash(depositAddressBody),
1057
+ ttlMs: 60_000,
1058
+ })
1059
+
1060
+ const response = await postDepositAddress(
1061
+ transfersApp({ providers: [depositAddressProvider()] }),
1062
+ { idempotencyKey },
1063
+ )
1064
+ expect(response.status).toBe(409)
1065
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
1066
+ 'idempotency_conflict',
1067
+ )
1068
+ })
1069
+
1070
+ test('returns not available without a registered provider', async () => {
1071
+ const response = await postDepositAddress(transfersApp({ providers: [] }))
1072
+ expect(response.status).toBe(404)
1073
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
1074
+ 'deposit_address_not_available',
1075
+ )
1076
+ })
1077
+
1078
+ test('reports unexpected provider failures', async () => {
1079
+ const entries: Log.Entry[] = []
1080
+ const app = transfersApp({
1081
+ logger: (entry) => {
1082
+ entries.push(entry)
1083
+ },
1084
+ providers: [
1085
+ withCreateDepositAddress('relay', async () => {
1086
+ throw new Error('upstream failed')
1087
+ }),
1088
+ ],
1089
+ })
1090
+ const response = await postDepositAddress(app, {
1091
+ body: { ...depositAddressBody, refundAddress: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb' },
1092
+ })
1093
+ expect(response.status).toBe(502)
1094
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
1095
+ 'upstream_error',
1096
+ )
1097
+ expect(entries.at(-1)).toMatchObject({
1098
+ provider: {
1099
+ failure: 'unknown',
1100
+ id: 'relay',
1101
+ operation: 'createDepositAddress',
1102
+ },
1103
+ status: 502,
1104
+ })
1105
+ })
1106
+ })
1107
+
1108
+ describe('POST /webhooks/funding/relay', () => {
1109
+ test('authenticates and enqueues a bounded reconciliation hint', async () => {
1110
+ const secret = 'relay-webhook-secret'
1111
+ const address = await seedDepositAddress({
1112
+ snapshot: TestFunding.depositAddressSnapshot({
1113
+ provider: { id: 'relay', name: 'Relay' },
1114
+ recipient: `0x${'99'.repeat(20)}`,
1115
+ }),
1116
+ })
1117
+ const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = []
1118
+ const app = transfersApp({
1119
+ providers: [
1120
+ Relay.relay({
1121
+ apiKey: secret,
1122
+ webhook: {
1123
+ db,
1124
+ dispatch: async (message) => {
1125
+ dispatched.push(message)
1126
+ },
1127
+ },
1128
+ }),
1129
+ ],
1130
+ })
1131
+ const body = JSON.stringify({
1132
+ data: {
1133
+ depositAddress: { address: address.address },
1134
+ requestId: 'relay-child-request',
1135
+ },
1136
+ event: 'request.status.updated',
1137
+ })
1138
+ const timestamp = String(Math.floor(Date.now() / 1_000))
1139
+ const signature = Hash.hmac256(
1140
+ Hex.fromString(secret),
1141
+ Hex.fromString(`${timestamp}.${body}`),
1142
+ ).slice(2)
1143
+ const response = await app.request('/webhooks/funding/relay', {
1144
+ body,
1145
+ headers: {
1146
+ 'x-signature-sha256': signature,
1147
+ 'x-signature-timestamp': timestamp,
1148
+ },
1149
+ method: 'POST',
1150
+ })
1151
+
1152
+ expect({ dispatched, status: response.status }).toMatchInlineSnapshot(`
1153
+ {
1154
+ "dispatched": [
1155
+ {
1156
+ "addressId": "${address.id}",
1157
+ "requestId": "relay-child-request",
1158
+ "type": "funding:deposit-address:reconcile",
1159
+ },
1160
+ ],
1161
+ "status": 204,
1162
+ }
1163
+ `)
1164
+ })
1165
+
1166
+ test('rejects unauthenticated events', async () => {
1167
+ const app = transfersApp({
1168
+ providers: [
1169
+ Relay.relay({
1170
+ apiKey: 'relay-webhook-secret',
1171
+ webhook: { db, dispatch: async () => {} },
1172
+ }),
1173
+ ],
1174
+ })
1175
+ const response = await app.request('/webhooks/funding/relay', {
1176
+ body: '{}',
1177
+ method: 'POST',
1178
+ })
1179
+
1180
+ expect(response.status).toMatchInlineSnapshot('401')
1181
+ })
1182
+ })
1183
+
1184
+ describe.skipIf(!relayApiKey)('POST /funding/deposit-addresses (relay)', () => {
1185
+ test('provisions and persists a reusable address through Relay', async () => {
1186
+ const app = transfersApp({
1187
+ providers: [Relay.relay({ apiKey: relayApiKey })],
1188
+ })
1189
+ const response = await postDepositAddress(app, {
1190
+ body: { ...depositAddressBody, recipient: `0x${'77'.repeat(20)}` },
1191
+ })
1192
+
1193
+ expect(response.status).toBe(200)
1194
+ const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
1195
+ expect(body).toMatchInlineSnapshot(
1196
+ {
1197
+ address: expect.any(String),
1198
+ createdAt: expect.any(String),
1199
+ destinationAmount: {
1200
+ baseUnits: expect.any(String),
1201
+ formatted: expect.any(String),
1202
+ },
1203
+ destinationAmountMin: {
1204
+ baseUnits: expect.any(String),
1205
+ formatted: expect.any(String),
1206
+ },
1207
+ fees: expect.any(Array),
1208
+ id: expect.any(String),
1209
+ quote: { sampledAt: expect.any(String) },
1210
+ updatedAt: expect.any(String),
1211
+ },
1212
+ `
1213
+ {
1214
+ "address": Any<String>,
1215
+ "createdAt": Any<String>,
1216
+ "destinationAmount": {
1217
+ "baseUnits": Any<String>,
1218
+ "currency": "USD",
1219
+ "decimals": 6,
1220
+ "formatted": Any<String>,
1221
+ },
1222
+ "destinationAmountMin": {
1223
+ "baseUnits": Any<String>,
1224
+ "currency": "USD",
1225
+ "decimals": 6,
1226
+ "formatted": Any<String>,
1227
+ },
1228
+ "destinationChain": {
1229
+ "addressFormat": "hex",
1230
+ "id": "eip155:4217",
1231
+ "kind": "evm",
1232
+ "name": "Tempo",
1233
+ },
1234
+ "destinationToken": {
1235
+ "address": "0x20c00000000000000000000014f22ca97301eb73",
1236
+ "currency": "USD",
1237
+ "decimals": 6,
1238
+ "name": "USDT0",
1239
+ "standard": "TIP-20",
1240
+ "symbol": "USDT0",
1241
+ "tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
1242
+ "verified": true,
1243
+ },
1244
+ "fees": Any<Array>,
1245
+ "id": Any<String>,
1246
+ "provider": {
1247
+ "id": "relay",
1248
+ "name": "Relay",
1249
+ },
1250
+ "quote": {
1251
+ "sampledAt": Any<String>,
1252
+ },
1253
+ "recipient": "0x7777777777777777777777777777777777777777",
1254
+ "refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
1255
+ "sourceAmount": {
1256
+ "baseUnits": "100000000",
1257
+ "currency": "USD",
1258
+ "decimals": 6,
1259
+ "formatted": "100",
1260
+ },
1261
+ "sourceChain": {
1262
+ "addressFormat": "base58check",
1263
+ "id": "tron:0x2b6653dc",
1264
+ "kind": "tron",
1265
+ "name": "Tron",
1266
+ },
1267
+ "sourceToken": {
1268
+ "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1269
+ "currency": "USD",
1270
+ "decimals": 6,
1271
+ "name": "Tether USD",
1272
+ "standard": "TRC-20",
1273
+ "symbol": "USDT",
1274
+ "tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1275
+ "verified": true,
1276
+ },
1277
+ "status": "active",
1278
+ "subsidize": false,
1279
+ "updatedAt": Any<String>,
1280
+ }
1281
+ `,
1282
+ )
1283
+ const record = await FundingDepositAddresses.get(db, body.id)
1284
+ expect(record).toMatchInlineSnapshot(
1285
+ {
1286
+ address: expect.any(String),
1287
+ createdAt: expect.any(String),
1288
+ id: expect.any(String),
1289
+ nextPollAt: expect.any(String),
1290
+ providerRequestIds: [expect.any(String)],
1291
+ providerState: expect.any(Object),
1292
+ snapshot: { address: expect.any(String) },
1293
+ updatedAt: expect.any(String),
1294
+ },
1295
+ `
1296
+ {
1297
+ "address": Any<String>,
1298
+ "apiKeyId": "key_funding_writer",
1299
+ "createdAt": Any<String>,
1300
+ "deliveryStrategy": "provider",
1301
+ "destinationTokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
1302
+ "environment": "production",
1303
+ "id": Any<String>,
1304
+ "lastPolledAt": null,
1305
+ "nextPollAt": Any<String>,
1306
+ "orgId": "org_funding",
1307
+ "pollFailureCount": 0,
1308
+ "pollLeaseUntil": null,
1309
+ "pollLeaseVersion": 0,
1310
+ "projectId": null,
1311
+ "providerId": "relay",
1312
+ "providerOutputToken": {
1313
+ "address": "0x20c00000000000000000000014f22ca97301eb73",
1314
+ "currency": "USD",
1315
+ "decimals": 6,
1316
+ "name": "USDT0",
1317
+ "standard": "TIP-20",
1318
+ "symbol": "USDT0",
1319
+ "tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
1320
+ "verified": true,
1321
+ },
1322
+ "providerRequestIds": [
1323
+ Any<String>,
1324
+ ],
1325
+ "providerState": Any<Object>,
1326
+ "recipient": "0x7777777777777777777777777777777777777777",
1327
+ "refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
1328
+ "snapshot": {
1329
+ "address": Any<String>,
1330
+ "destinationChain": {
1331
+ "addressFormat": "hex",
1332
+ "id": "eip155:4217",
1333
+ "kind": "evm",
1334
+ "name": "Tempo",
1335
+ },
1336
+ "destinationToken": {
1337
+ "address": "0x20c00000000000000000000014f22ca97301eb73",
1338
+ "currency": "USD",
1339
+ "decimals": 6,
1340
+ "name": "USDT0",
1341
+ "standard": "TIP-20",
1342
+ "symbol": "USDT0",
1343
+ "tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
1344
+ "verified": true,
1345
+ },
1346
+ "provider": {
1347
+ "id": "relay",
1348
+ "name": "Relay",
1349
+ },
1350
+ "recipient": "0x7777777777777777777777777777777777777777",
1351
+ "refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
1352
+ "sourceChain": {
1353
+ "addressFormat": "base58check",
1354
+ "id": "tron:0x2b6653dc",
1355
+ "kind": "tron",
1356
+ "name": "Tron",
1357
+ },
1358
+ "sourceToken": {
1359
+ "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1360
+ "currency": "USD",
1361
+ "decimals": 6,
1362
+ "name": "Tether USD",
1363
+ "standard": "TRC-20",
1364
+ "symbol": "USDT",
1365
+ "tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1366
+ "verified": true,
1367
+ },
1368
+ "subsidize": false,
1369
+ },
1370
+ "sourceChainId": "tron:0x2b6653dc",
1371
+ "sourceTokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1372
+ "status": "active",
1373
+ "subsidize": false,
1374
+ "updatedAt": Any<String>,
1375
+ "version": 1,
1376
+ }
1377
+ `,
1378
+ )
1379
+ })
1380
+ })
1381
+
1382
+ describe('GET /funding/deposit-addresses/:id', () => {
1383
+ test('requires funding:read and returns the owned address', async () => {
1384
+ const app = transfersApp()
1385
+ const record = await seedDepositAddress()
1386
+ const path = `/v1/funding/deposit-addresses/${record.id}`
1387
+ expect((await app.request(path)).status).toBe(401)
1388
+ expect((await app.request(path, as(TestApp.key))).status).toBe(403)
1389
+
1390
+ const response = await app.request(path, as(fundingReader))
1391
+ expect(response.status).toBe(200)
1392
+ expect({
1393
+ cacheControl: response.headers.get('cache-control'),
1394
+ etag: response.headers.get('etag'),
1395
+ }).toMatchInlineSnapshot(`
1396
+ {
1397
+ "cacheControl": "no-store",
1398
+ "etag": null,
1399
+ }
1400
+ `)
1401
+ expect(response.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
1402
+ const body = await TestApp.json(response, Funding.schema.getFundingDepositAddress.Response)
1403
+ expect(body).toEqual(FundingDepositAddress.toPublic(record))
1404
+ expect(body).not.toHaveProperty('providerOutputToken')
1405
+ expect(body).not.toHaveProperty('providerRequestIds')
1406
+ })
1407
+
1408
+ test('hides foreign, project-inaccessible, and absent addresses', async () => {
1409
+ const app = transfersApp()
1410
+ const record = await seedDepositAddress()
1411
+ const path = `/v1/funding/deposit-addresses/${record.id}`
1412
+
1413
+ for (const key of [fundingForeign, fundingProjectReader]) {
1414
+ const response = await app.request(path, as(key))
1415
+ expect(response.status).toBe(404)
1416
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
1417
+ 'funding_deposit_address_not_found',
1418
+ )
1419
+ }
1420
+
1421
+ const absent = await app.request(
1422
+ '/v1/funding/deposit-addresses/fda_000000000000000_000000000000000000000000',
1423
+ as(fundingReader),
1424
+ )
1425
+ expect(absent.status).toBe(404)
1426
+ })
1427
+ })
1428
+
1429
+ describe('GET /funding/deposit-addresses/:id/deposits', () => {
1430
+ test('pages deposits newest-first under an owned address', async () => {
1431
+ const app = transfersApp()
1432
+ const address = await seedDepositAddress()
1433
+ const first = await seedDeposit(address)
1434
+ const second = await seedDeposit(address)
1435
+ const third = await seedDeposit(address)
1436
+ const fourth = await seedDeposit(address)
1437
+ const fifth = await seedDeposit(address)
1438
+ const sixth = await seedDeposit(address)
1439
+
1440
+ const response = await app.request(
1441
+ `/v1/funding/deposit-addresses/${address.id}/deposits?limit=5`,
1442
+ as(fundingReader),
1443
+ )
1444
+ expect(response.status).toBe(200)
1445
+ expect({
1446
+ cacheControl: response.headers.get('cache-control'),
1447
+ etag: response.headers.get('etag'),
1448
+ }).toMatchInlineSnapshot(`
1449
+ {
1450
+ "cacheControl": "no-store",
1451
+ "etag": null,
1452
+ }
1453
+ `)
1454
+ const body = await TestApp.json(
1455
+ response,
1456
+ Funding.schema.listFundingDepositAddressDeposits.Response,
1457
+ )
1458
+ expect(body.data.map((deposit) => deposit.id)).toEqual([
1459
+ sixth.id,
1460
+ fifth.id,
1461
+ fourth.id,
1462
+ third.id,
1463
+ second.id,
1464
+ ])
1465
+ expect(body.nextCursor).toBe(Cursor.encode([second.createdAt, second.id]))
1466
+
1467
+ const next = await TestApp.json(
1468
+ await app.request(
1469
+ `/v1/funding/deposit-addresses/${address.id}/deposits?limit=5&cursor=${body.nextCursor}`,
1470
+ as(fundingReader),
1471
+ ),
1472
+ Funding.schema.listFundingDepositAddressDeposits.Response,
1473
+ )
1474
+ expect(next.data.map((deposit) => deposit.id)).toEqual([first.id])
1475
+ expect(next.nextCursor).toBeNull()
1476
+ })
1477
+
1478
+ test('returns a provider-observed deposit before source verification', async () => {
1479
+ const app = transfersApp()
1480
+ const address = await seedDepositAddress({
1481
+ snapshot: TestFunding.depositAddressSnapshot({
1482
+ provider: { id: 'relay_observed', name: 'Relay' },
1483
+ recipient: `0x${'31'.repeat(20)}`,
1484
+ }),
1485
+ })
1486
+ const sourceTransactionHash = `0x${'ab'.repeat(32)}`
1487
+ await FundingDeposit.create(db, {
1488
+ now: new Date('2026-01-04T00:00:00.000Z'),
1489
+ providerRequestId: 'relay-observed-request',
1490
+ snapshot: {
1491
+ depositAddressId: address.id,
1492
+ destinationChain: address.snapshot.destinationChain,
1493
+ destinationToken: address.snapshot.destinationToken,
1494
+ provider: address.snapshot.provider,
1495
+ recipient: address.recipient,
1496
+ refundAddress: address.refundAddress,
1497
+ sourceChain: address.snapshot.sourceChain,
1498
+ sourceToken: address.snapshot.sourceToken,
1499
+ sourceTransactionHashes: [sourceTransactionHash],
1500
+ },
1501
+ sourceTransactionHash,
1502
+ })
1503
+
1504
+ const response = await app.request(
1505
+ `/v1/funding/deposit-addresses/${address.id}/deposits`,
1506
+ as(fundingReader),
1507
+ )
1508
+ expect(response.status).toBe(200)
1509
+ const body = await TestApp.json(
1510
+ response,
1511
+ Funding.schema.listFundingDepositAddressDeposits.Response,
1512
+ )
1513
+ expect(body).toMatchInlineSnapshot(
1514
+ {
1515
+ data: [
1516
+ {
1517
+ createdAt: expect.any(String),
1518
+ depositAddressId: expect.any(String),
1519
+ id: expect.any(String),
1520
+ updatedAt: expect.any(String),
1521
+ },
1522
+ ],
1523
+ },
1524
+ `
1525
+ {
1526
+ "data": [
1527
+ {
1528
+ "createdAt": Any<String>,
1529
+ "depositAddressId": Any<String>,
1530
+ "destinationChain": {
1531
+ "addressFormat": "hex",
1532
+ "id": "eip155:4217",
1533
+ "kind": "evm",
1534
+ "name": "Tempo",
1535
+ },
1536
+ "destinationToken": {
1537
+ "address": "0x20c00000000000000000000014f22ca97301eb73",
1538
+ "currency": "USD",
1539
+ "decimals": 6,
1540
+ "name": "USDT0",
1541
+ "standard": "TIP-20",
1542
+ "symbol": "USDT0",
1543
+ "tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
1544
+ "verified": true,
1545
+ },
1546
+ "id": Any<String>,
1547
+ "provider": {
1548
+ "id": "relay_observed",
1549
+ "name": "Relay",
1550
+ },
1551
+ "recipient": "0x3131313131313131313131313131313131313131",
1552
+ "refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
1553
+ "sourceChain": {
1554
+ "addressFormat": "base58check",
1555
+ "id": "tron:0x2b6653dc",
1556
+ "kind": "tron",
1557
+ "name": "Tron",
1558
+ },
1559
+ "sourceToken": {
1560
+ "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1561
+ "currency": "USD",
1562
+ "decimals": 6,
1563
+ "name": "Tether USD",
1564
+ "standard": "TRC-20",
1565
+ "symbol": "USDT",
1566
+ "tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
1567
+ "verified": true,
1568
+ },
1569
+ "sourceTransactionHashes": [
1570
+ "0xabababababababababababababababababababababababababababababababab",
1571
+ ],
1572
+ "status": "detected",
1573
+ "updatedAt": Any<String>,
1574
+ },
1575
+ ],
1576
+ "nextCursor": null,
1577
+ }
1578
+ `,
1579
+ )
1580
+ })
1581
+
1582
+ test('returns the parent not-found error before listing deposits', async () => {
1583
+ const app = transfersApp()
1584
+ const address = await seedDepositAddress({ orgId: fundingForeign.orgId })
1585
+ const response = await app.request(
1586
+ `/v1/funding/deposit-addresses/${address.id}/deposits`,
1587
+ as(fundingReader),
1588
+ )
1589
+ expect(response.status).toBe(404)
1590
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
1591
+ 'funding_deposit_address_not_found',
1592
+ )
1593
+ })
1594
+ })
1595
+
1596
+ describe('GET /funding/deposits/:id', () => {
1597
+ test('returns the owned deposit without private reconciliation state', async () => {
1598
+ const app = transfersApp()
1599
+ const address = await seedDepositAddress()
1600
+ const record = await seedDeposit(address, {
1601
+ providerRequestIds: ['relay-private'],
1602
+ providerState: { request: 'private' },
1603
+ })
1604
+ const response = await app.request(`/v1/funding/deposits/${record.id}`, as(fundingReader))
1605
+ expect(response.status).toBe(200)
1606
+ expect(response.headers.get('cache-control')).toBe(Cache.policies.state)
1607
+ const body = await TestApp.json(response, Funding.schema.getFundingDeposit.Response)
1608
+ expect(body).toEqual(FundingDeposit.toPublic(record))
1609
+ expect(body).not.toHaveProperty('providerRequestIds')
1610
+ expect(body).not.toHaveProperty('providerState')
1611
+ })
1612
+
1613
+ test('hides foreign and absent deposits', async () => {
1614
+ const app = transfersApp()
1615
+ const address = await seedDepositAddress()
1616
+ const record = await seedDeposit(address)
1617
+ expect(
1618
+ (await app.request(`/v1/funding/deposits/${record.id}`, as(fundingForeign))).status,
1619
+ ).toBe(404)
1620
+ expect(
1621
+ (
1622
+ await app.request(
1623
+ '/v1/funding/deposits/fdp_000000000000000_000000000000000000000000',
1624
+ as(fundingReader),
1625
+ )
1626
+ ).status,
1627
+ ).toBe(404)
1628
+ })
1629
+ })
1630
+
1631
+ describe('GET /funding/transfers', () => {
1632
+ test('requires the funding:read scope', async () => {
1633
+ const app = transfersApp()
1634
+ expect((await app.request('/v1/funding/transfers')).status).toBe(401)
1635
+ const forbidden = await app.request('/v1/funding/transfers', as(TestApp.key))
1636
+ expect(forbidden.status).toBe(403)
1637
+ })
1638
+
1639
+ test('pages own transfers newest-first', async () => {
1640
+ const app = transfersApp()
1641
+ const first = await seedTransfer()
1642
+ const second = await seedTransfer()
1643
+ const third = await seedTransfer()
1644
+ await seedTransfer({ orgId: fundingForeign.orgId })
1645
+
1646
+ const response = await app.request('/v1/funding/transfers?limit=5', {
1647
+ headers: { 'tempo-api-key': fundingReader.token },
1648
+ })
1649
+ expect(response.status).toBe(200)
1650
+ expect(response.headers.get('cache-control')).toBe(Cache.policies.feed)
1651
+ const etag = response.headers.get('etag') ?? ''
1652
+ expect(etag).not.toBe('')
1653
+ expect(response.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
1654
+ const body = await TestApp.json(response, Funding.schema.listFundingTransfers.Response)
1655
+ expect(body.data.map((transfer) => transfer.id)).toEqual([third.id, second.id, first.id])
1656
+ expect(body.nextCursor).toBeNull()
1657
+ expect(body.meta).toBeUndefined()
1658
+
1659
+ const unchanged = await app.request('/v1/funding/transfers?limit=5', {
1660
+ headers: { 'if-none-match': etag, 'tempo-api-key': fundingReader.token },
1661
+ })
1662
+ expect(unchanged.status).toBe(304)
1663
+ expect(unchanged.headers.get('cache-control')).toBe(Cache.policies.feed)
1664
+ expect(unchanged.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
1665
+
1666
+ const paged = await TestApp.json(
1667
+ await app.request(`/v1/funding/transfers?limit=5&cursor=${second.id}`, as(fundingReader)),
1668
+ Funding.schema.listFundingTransfers.Response,
1669
+ )
1670
+ expect(paged.data.map((transfer) => transfer.id)).toEqual([first.id])
1671
+ })
1672
+
1673
+ test('filters by status and embeds the capped count on request', async () => {
1674
+ const app = transfersApp()
1675
+ const expired = await seedTransfer()
1676
+ await FundingTransfer.transition(db, {
1677
+ expectedVersion: 1,
1678
+ id: expired.id,
1679
+ status: 'expired',
1680
+ })
1681
+
1682
+ const response = await app.request(
1683
+ '/v1/funding/transfers?status=expired&include=totalCount',
1684
+ as(fundingReader),
1685
+ )
1686
+ const body = await TestApp.json(response, Funding.schema.listFundingTransfers.Response)
1687
+ expect(body.data.map((transfer) => transfer.id)).toEqual([expired.id])
1688
+ expect(body.data[0]?.status).toBe('expired')
1689
+ expect(body.meta).toEqual({ totalCount: 1, totalCountCapped: false })
1690
+ })
1691
+
1692
+ test('narrows project-attributed keys to their project', async () => {
1693
+ const app = transfersApp()
1694
+ const scoped = await seedTransfer({ projectId: fundingProjectReader.projectId })
1695
+
1696
+ const body = await TestApp.json(
1697
+ await app.request('/v1/funding/transfers?limit=200', as(fundingProjectReader)),
1698
+ Funding.schema.listFundingTransfers.Response,
1699
+ )
1700
+ expect(body.data.map((transfer) => transfer.id)).toEqual([scoped.id])
1701
+
1702
+ // The organization-attributed key sees the project row too.
1703
+ const orgBody = await TestApp.json(
1704
+ await app.request('/v1/funding/transfers?limit=200', as(fundingReader)),
1705
+ Funding.schema.listFundingTransfers.Response,
1706
+ )
1707
+ expect(orgBody.data.map((transfer) => transfer.id)).toContain(scoped.id)
1708
+ })
1709
+
1710
+ test('rejects invalid query parameters', async () => {
1711
+ const app = transfersApp()
1712
+ const response = await app.request('/v1/funding/transfers?status=bogus', as(fundingReader))
1713
+ expect(response.status).toBe(400)
1714
+ })
1715
+ })
1716
+
1717
+ describe('GET /funding/transfers/:id', () => {
1718
+ test('returns the owned transfer directly, without an action', async () => {
1719
+ const app = transfersApp()
1720
+ const record = await seedTransfer()
1721
+ const response = await app.request(`/v1/funding/transfers/${record.id}`, {
1722
+ headers: { 'tempo-api-key': fundingReader.token },
1723
+ })
1724
+ expect(response.status).toBe(200)
1725
+ expect({
1726
+ cacheControl: response.headers.get('cache-control'),
1727
+ etag: response.headers.get('etag'),
1728
+ }).toMatchInlineSnapshot(`
1729
+ {
1730
+ "cacheControl": "no-store",
1731
+ "etag": null,
1732
+ }
1733
+ `)
1734
+ expect(response.headers.get('vary')).toBe('Accept-Encoding, Authorization, Tempo-API-Key')
1735
+ const body = await TestApp.json(response, Funding.schema.getFundingTransfer.Response)
1736
+ expect(body).toEqual(FundingTransfer.toPublic(record))
1737
+ expect('action' in body).toBe(false)
1738
+
1739
+ const repeated = await app.request(`/v1/funding/transfers/${record.id}`, {
1740
+ headers: { 'if-none-match': '"stale"', 'tempo-api-key': fundingReader.token },
1741
+ })
1742
+ expect({
1743
+ cacheControl: repeated.headers.get('cache-control'),
1744
+ etag: repeated.headers.get('etag'),
1745
+ status: repeated.status,
1746
+ }).toMatchInlineSnapshot(`
1747
+ {
1748
+ "cacheControl": "no-store",
1749
+ "etag": null,
1750
+ "status": 200,
1751
+ }
1752
+ `)
1753
+ })
1754
+
1755
+ test('hides foreign and absent transfers behind the same 404', async () => {
1756
+ const app = transfersApp()
1757
+ const record = await seedTransfer()
1758
+
1759
+ const foreign = await app.request(`/v1/funding/transfers/${record.id}`, as(fundingForeign))
1760
+ expect(foreign.status).toBe(404)
1761
+ expect(((await foreign.json()) as { error: { code: string } }).error.code).toBe(
1762
+ 'funding_transfer_not_found',
1763
+ )
1764
+
1765
+ const project = await app.request(
1766
+ `/v1/funding/transfers/${record.id}`,
1767
+ as(fundingProjectReader),
1768
+ )
1769
+ expect(project.status).toBe(404)
1770
+
1771
+ const absent = await app.request(
1772
+ '/v1/funding/transfers/ftr_000000000000000_000000000000000000000000',
1773
+ as(fundingReader),
1774
+ )
1775
+ expect(absent.status).toBe(404)
1776
+ })
1777
+ })
1778
+
1779
+ const createBody = {
1780
+ amount: '1000000',
1781
+ destinationToken: '0x20c0000000000000000000000000000000000000',
1782
+ mode: 'exactSource',
1783
+ recipient: `0x${'11'.repeat(20)}`,
1784
+ sender: `0x${'22'.repeat(20)}`,
1785
+ slippageBps: 300,
1786
+ sourceChain: 'base',
1787
+ sourceToken: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
1788
+ }
1789
+
1790
+ const vaultTransferBody = {
1791
+ amount: createBody.amount,
1792
+ mode: createBody.mode,
1793
+ recipient: createBody.recipient,
1794
+ sender: createBody.sender,
1795
+ slippageBps: createBody.slippageBps,
1796
+ sourceChain: createBody.sourceChain,
1797
+ sourceToken: createBody.sourceToken,
1798
+ vaultAddress: `0x${'f4'.repeat(20)}`,
1799
+ }
1800
+
1801
+ const zoneTransferBody = {
1802
+ amount: createBody.amount,
1803
+ destinationChain: 'eip155:421700001',
1804
+ destinationToken: createBody.destinationToken,
1805
+ mode: createBody.mode,
1806
+ recipient: createBody.recipient,
1807
+ recipientFallback: `0x${'33'.repeat(20)}`,
1808
+ sender: createBody.sender,
1809
+ slippageBps: createBody.slippageBps,
1810
+ sourceChain: createBody.sourceChain,
1811
+ sourceToken: createBody.sourceToken,
1812
+ }
1813
+
1814
+ const depositAddressBody = {
1815
+ amount: '100000000',
1816
+ destinationToken: '0x20c00000000000000000000014f22ca97301eb73',
1817
+ recipient: `0x${'11'.repeat(20)}`,
1818
+ refundAddress: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
1819
+ sourceChain: 'tron',
1820
+ sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
1821
+ }
1822
+
1823
+ let depositAddressIdempotencySequence = 0
1824
+ function nextDepositAddressKey() {
1825
+ return `funding_deposit_address_${depositAddressIdempotencySequence++}`
1826
+ }
1827
+
1828
+ function postDepositAddress(
1829
+ app: ReturnType<typeof create>,
1830
+ options: {
1831
+ body?: unknown
1832
+ headers?: Record<string, string> | undefined
1833
+ idempotencyKey?: string | undefined
1834
+ key?: { token: string } | undefined
1835
+ } = {},
1836
+ ) {
1837
+ return app.request('/v1/funding/deposit-addresses', {
1838
+ body: JSON.stringify(options.body ?? depositAddressBody),
1839
+ headers: options.headers ?? {
1840
+ authorization: `Bearer ${(options.key ?? fundingWriter).token}`,
1841
+ 'content-type': 'application/json',
1842
+ 'idempotency-key': options.idempotencyKey ?? nextDepositAddressKey(),
1843
+ },
1844
+ method: 'POST',
1845
+ })
1846
+ }
1847
+
1848
+ function postTransfer(
1849
+ app: ReturnType<typeof create>,
1850
+ options: { body?: unknown; idempotencyKey?: string | undefined; key?: { token: string } } = {},
1851
+ ) {
1852
+ return app.request('/v1/funding/transfers', {
1853
+ body: JSON.stringify(options.body ?? createBody),
1854
+ headers: {
1855
+ authorization: `Bearer ${(options.key ?? fundingWriter).token}`,
1856
+ 'content-type': 'application/json',
1857
+ ...(options.idempotencyKey === undefined
1858
+ ? {}
1859
+ : { 'idempotency-key': options.idempotencyKey }),
1860
+ },
1861
+ method: 'POST',
1862
+ })
1863
+ }
1864
+
1865
+ describe('POST /funding/transfers', () => {
1866
+ function providerResponse(): FundingProvider.prepareTransfer.ReturnType {
1867
+ const spender = `0x${'44'.repeat(20)}`
1868
+ return {
1869
+ action: {
1870
+ calls: [
1871
+ {
1872
+ data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${(1_000_000).toString(16).padStart(64, '0')}`,
1873
+ to: createBody.sourceToken,
1874
+ value: '0x0',
1875
+ },
1876
+ { data: '0xe8017952', to: spender, value: '0x0' },
1877
+ ],
1878
+ type: 'evm:calls',
1879
+ },
1880
+ correlation: { requestId: `0x${'ab'.repeat(32)}` },
1881
+ fees: [{ amount: '21050', side: 'source' }],
1882
+ destinationAmountMin: '980000',
1883
+ destinationAmount: '990000',
1884
+ quality: {
1885
+ estimatedSeconds: 4,
1886
+ liquiditySource: 'providerQuote',
1887
+ sourceDetail: 'relay:quote-v2',
1888
+ tier: 'liquid',
1889
+ },
1890
+ sampledAt: '2099-01-01T00:00:00.000Z',
1891
+ }
1892
+ }
1893
+
1894
+ function provider(
1895
+ prepareTransfer: NonNullable<FundingProvider.Provider['prepareTransfer']> = async () =>
1896
+ providerResponse(),
1897
+ ) {
1898
+ return FundingProvider.from({
1899
+ ...Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
1900
+ prepareTransfer,
1901
+ })
1902
+ }
1903
+
1904
+ let idempotencySequence = 0
1905
+ function nextIdempotencyKey() {
1906
+ return `funding_test_${idempotencySequence++}`
1907
+ }
1908
+
1909
+ test('requires funding:write and an Idempotency-Key', async () => {
1910
+ const app = transfersApp({
1911
+ providers: [provider()],
1912
+ })
1913
+ const forbidden = await postTransfer(app, {
1914
+ idempotencyKey: nextIdempotencyKey(),
1915
+ key: fundingReader,
1916
+ })
1917
+ expect(forbidden.status).toBe(403)
1918
+
1919
+ const missing = await postTransfer(app)
1920
+ expect(missing.status).toBe(400)
1921
+ expect(((await missing.json()) as { error: { code: string } }).error.code).toBe(
1922
+ 'idempotency_key_required',
1923
+ )
1924
+ })
1925
+
1926
+ test('creates a transfer with the selected executable quote', async () => {
1927
+ const inputs: FundingProvider.prepareTransfer.Parameters[] = []
1928
+ const app = transfersApp({
1929
+ providers: [
1930
+ provider(async (input) => {
1931
+ inputs.push(input)
1932
+ return providerResponse()
1933
+ }),
1934
+ ],
1935
+ })
1936
+ const response = await postTransfer(app, {
1937
+ body: { ...createBody, destinationToken: 'pathusd', sourceToken: 'usdc' },
1938
+ idempotencyKey: nextIdempotencyKey(),
1939
+ })
1940
+ expect(response.status).toBe(200)
1941
+ expect(response.headers.get('cache-control')).toBe('no-store')
1942
+ const body = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
1943
+
1944
+ expect(inputs[0]).toMatchObject({
1945
+ method: 'transaction',
1946
+ mode: 'exactSource',
1947
+ recipient: createBody.recipient,
1948
+ sender: createBody.sender,
1949
+ slippageBps: 300,
1950
+ })
1951
+ expect(body).toMatchObject({
1952
+ action: { type: 'evm:calls' },
1953
+ destinationAmountMin: { baseUnits: '980000' },
1954
+ method: 'transaction',
1955
+ mode: 'exactSource',
1956
+ provider: { id: 'relay' },
1957
+ // The provider returned no expiry, so the default validity window applies.
1958
+ quote: { expiresAt: '2099-01-01T00:01:00.000Z', sampledAt: '2099-01-01T00:00:00.000Z' },
1959
+ sourceAmount: { baseUnits: '1000000' },
1960
+ status: 'awaiting-source',
1961
+ version: 1,
1962
+ })
1963
+ expect(body.fees[0]).toMatchObject({ side: 'source', token: { symbol: 'USDC' } })
1964
+
1965
+ // Reads return the same transfer without the action; the private
1966
+ // correlation never leaves the row.
1967
+ const read = await app.request(`/v1/funding/transfers/${body.id}`, as(fundingWriter))
1968
+ const readBody = await TestApp.json(read, Funding.schema.getFundingTransfer.Response)
1969
+ expect('action' in readBody).toBe(false)
1970
+ expect(JSON.stringify(readBody)).not.toContain('ab'.repeat(32))
1971
+ const row = await db.kysely
1972
+ .selectFrom('funding_transfers')
1973
+ .select(['providerState'])
1974
+ .where('id', '=', body.id)
1975
+ .executeTakeFirstOrThrow()
1976
+ expect(row.providerState).toEqual({ requestId: `0x${'ab'.repeat(32)}` })
1977
+ })
1978
+
1979
+ test('resolves token identifiers through the composed route', async () => {
1980
+ const app = transfersApp({ providers: [provider()] })
1981
+ const identifiers = [
1982
+ { destinationToken: 'pathusd', name: 'alias', sourceToken: 'usdc' },
1983
+ {
1984
+ destinationToken: createBody.destinationToken,
1985
+ name: 'address',
1986
+ sourceToken: createBody.sourceToken,
1987
+ },
1988
+ {
1989
+ destinationToken: 'eip155:4217/erc20:0x20c0000000000000000000000000000000000000',
1990
+ name: 'token key',
1991
+ sourceToken: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
1992
+ },
1993
+ ] as const
1994
+ const results = []
1995
+ for (const identifier of identifiers) {
1996
+ const response = await postTransfer(app, {
1997
+ body: {
1998
+ ...createBody,
1999
+ destinationToken: identifier.destinationToken,
2000
+ sourceToken: identifier.sourceToken,
2001
+ },
2002
+ idempotencyKey: nextIdempotencyKey(),
2003
+ })
2004
+ const body = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
2005
+ results.push({
2006
+ destinationToken: body.destinationToken.symbol,
2007
+ name: identifier.name,
2008
+ sourceToken: body.sourceToken.symbol,
2009
+ status: response.status,
2010
+ })
2011
+ }
2012
+
2013
+ expect(results).toMatchInlineSnapshot(`
2014
+ [
2015
+ {
2016
+ "destinationToken": "pathUSD",
2017
+ "name": "alias",
2018
+ "sourceToken": "USDC",
2019
+ "status": 200,
2020
+ },
2021
+ {
2022
+ "destinationToken": "pathUSD",
2023
+ "name": "address",
2024
+ "sourceToken": "USDC",
2025
+ "status": 200,
2026
+ },
2027
+ {
2028
+ "destinationToken": "pathUSD",
2029
+ "name": "token key",
2030
+ "sourceToken": "USDC",
2031
+ "status": 200,
2032
+ },
2033
+ ]
2034
+ `)
2035
+ })
2036
+
2037
+ test('replays the exact response for the same key and body', async () => {
2038
+ const app = transfersApp({
2039
+ providers: [provider()],
2040
+ })
2041
+ const idempotencyKey = nextIdempotencyKey()
2042
+ const first = await postTransfer(app, { idempotencyKey })
2043
+ const second = await postTransfer(app, { idempotencyKey })
2044
+ expect(second.status).toBe(200)
2045
+ expect(await second.json()).toEqual(await first.json())
2046
+ })
2047
+
2048
+ test('rejects a reused key with different input', async () => {
2049
+ const app = transfersApp({
2050
+ providers: [provider()],
2051
+ })
2052
+ const idempotencyKey = nextIdempotencyKey()
2053
+ await postTransfer(app, { idempotencyKey })
2054
+ const conflict = await postTransfer(app, {
2055
+ body: { ...createBody, amount: '2000000' },
2056
+ idempotencyKey,
2057
+ })
2058
+ expect(conflict.status).toBe(409)
2059
+ expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe(
2060
+ 'idempotency_conflict',
2061
+ )
2062
+ })
2063
+
2064
+ test('rejects an equivalent request while the first is running', async () => {
2065
+ const app = transfersApp({
2066
+ providers: [provider()],
2067
+ })
2068
+ const idempotencyKey = nextIdempotencyKey()
2069
+ await FundingIdempotencyTable.claim(db, {
2070
+ apiKeyId: fundingWriter.id,
2071
+ keyHash: Idempotency.inputHash(idempotencyKey),
2072
+ requestHash: Idempotency.inputHash(createBody),
2073
+ ttlMs: 60_000,
2074
+ })
2075
+ const pending = await postTransfer(app, { idempotencyKey })
2076
+ expect(pending.status).toBe(409)
2077
+ expect(((await pending.json()) as { error: { code: string } }).error.code).toBe(
2078
+ 'idempotency_in_progress',
2079
+ )
2080
+ })
2081
+
2082
+ test('rolls back creation after losing the idempotency lease', async () => {
2083
+ const idempotencyKey = nextIdempotencyKey()
2084
+ const keyHash = Idempotency.inputHash(idempotencyKey)
2085
+ const before = await db.kysely
2086
+ .selectFrom('funding_transfers')
2087
+ .select(({ fn }) => fn.countAll<number>().as('count'))
2088
+ .executeTakeFirstOrThrow()
2089
+ const app = transfersApp({
2090
+ providers: [
2091
+ provider(async () => {
2092
+ await db.kysely
2093
+ .updateTable('funding_idempotency_requests')
2094
+ .set({ expiresAt: new Date(0).toISOString() })
2095
+ .where('apiKeyId', '=', fundingWriter.id)
2096
+ .where('keyHash', '=', keyHash)
2097
+ .execute()
2098
+ await FundingIdempotencyTable.claim(db, {
2099
+ apiKeyId: fundingWriter.id,
2100
+ keyHash,
2101
+ requestHash: Idempotency.inputHash(createBody),
2102
+ ttlMs: 60_000,
2103
+ })
2104
+ return providerResponse()
2105
+ }),
2106
+ ],
2107
+ })
2108
+
2109
+ const response = await postTransfer(app, { idempotencyKey })
2110
+ expect(response.status).toBe(500)
2111
+ const after = await db.kysely
2112
+ .selectFrom('funding_transfers')
2113
+ .select(({ fn }) => fn.countAll<number>().as('count'))
2114
+ .executeTakeFirstOrThrow()
2115
+ expect(after.count).toBe(before.count)
2116
+ })
2117
+
2118
+ test('rejects non-account fields and gates disabled corridors and methods', async () => {
2119
+ const app = transfersApp({
2120
+ providers: [provider()],
2121
+ })
2122
+
2123
+ const ignoredRefund = await postTransfer(app, {
2124
+ body: { ...createBody, refundAddress: createBody.sender },
2125
+ idempotencyKey: nextIdempotencyKey(),
2126
+ })
2127
+ expect(ignoredRefund.status).toBe(400)
2128
+ expect(((await ignoredRefund.json()) as { error: { code: string } }).error.code).toBe(
2129
+ 'body_invalid',
2130
+ )
2131
+
2132
+ const unsupportedDestination = await postTransfer(app, {
2133
+ body: { ...createBody, destinationVault: `0x${'f4'.repeat(20)}`, type: 'vault' },
2134
+ idempotencyKey: nextIdempotencyKey(),
2135
+ })
2136
+ expect(unsupportedDestination.status).toBe(400)
2137
+ expect(((await unsupportedDestination.json()) as { error: { code: string } }).error.code).toBe(
2138
+ 'body_invalid',
2139
+ )
2140
+
2141
+ const unknownToken = await postTransfer(app, {
2142
+ body: { ...createBody, destinationToken: `0x${'99'.repeat(20)}` },
2143
+ idempotencyKey: nextIdempotencyKey(),
2144
+ })
2145
+ expect(unknownToken.status).toBe(404)
2146
+ })
2147
+
2148
+ test('releases the claim after a provider failure', async () => {
2149
+ const failing = transfersApp({
2150
+ providers: [
2151
+ provider(async () => {
2152
+ throw new Error('upstream unavailable')
2153
+ }),
2154
+ ],
2155
+ })
2156
+ const idempotencyKey = nextIdempotencyKey()
2157
+ const failed = await postTransfer(failing, { idempotencyKey })
2158
+ expect(failed.status).toBe(502)
2159
+ expect(((await failed.json()) as { error: { code: string } }).error.code).toBe('upstream_error')
2160
+
2161
+ // The key was released, so the same request retries successfully.
2162
+ const working = transfersApp({
2163
+ providers: [provider()],
2164
+ })
2165
+ expect((await postTransfer(working, { idempotencyKey })).status).toBe(200)
2166
+ })
2167
+
2168
+ test('rejects preparations with unbounded approvals', async () => {
2169
+ const app = transfersApp({
2170
+ providers: [
2171
+ provider(async () => {
2172
+ const result = providerResponse()
2173
+ const spender = `0x${'44'.repeat(20)}`
2174
+ return {
2175
+ ...result,
2176
+ action: {
2177
+ calls: [
2178
+ {
2179
+ data: `0x095ea7b3${spender.slice(2).padStart(64, '0')}${'f'.repeat(64)}`,
2180
+ to: createBody.sourceToken,
2181
+ value: '0x0',
2182
+ },
2183
+ ],
2184
+ type: 'evm:calls',
2185
+ },
2186
+ }
2187
+ }),
2188
+ ],
2189
+ })
2190
+ const response = await postTransfer(app, { idempotencyKey: nextIdempotencyKey() })
2191
+ expect(response.status).toBe(502)
2192
+ })
2193
+ })
2194
+
2195
+ // Quotes come from the live provider API; the returned calls execute on a
2196
+ // disposable anvil fork of Base, proving the action without funding a real
2197
+ // account. Skips without the `anvil` binary.
2198
+ describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', () => {
2199
+ /**
2200
+ * Storage slot of Circle FiatToken's `balanceAndBlacklistStates` mapping.
2201
+ * `anvil_dealERC20` cannot probe it because balances pack a blacklist bit,
2202
+ * so the fork suite writes the slot directly. Every curated USDC is a
2203
+ * FiatToken.
2204
+ */
2205
+ const fiatTokenBalanceSlot = 9n
2206
+
2207
+ for (const executable of executableProviders) {
2208
+ describe(executable.id, () => {
2209
+ const route = TestFunding.snapshot.routes.find(
2210
+ (entry) =>
2211
+ entry.route.source.chain.id === `eip155:${base.id}` &&
2212
+ entry.route.source.symbol === 'USDC' &&
2213
+ entry.route.destination.slug === 'pathusd',
2214
+ )!
2215
+ const usdc = route.route.source
2216
+ const amount = Value.from('10', usdc.decimals)
2217
+ const sender = Anvil.getAccount(0)
2218
+
2219
+ let anvil: Anvil.start.ReturnType
2220
+
2221
+ beforeAll(async () => {
2222
+ anvil = await Anvil.start({ chain: base, forkUrl: base.rpcUrls.default.http[0]! })
2223
+ })
2224
+
2225
+ afterAll(async () => {
2226
+ await anvil?.stop()
2227
+ })
2228
+
2229
+ function createTransfer(idempotencyKey: string) {
2230
+ const app = transfersApp({ providers: [executable.create(sender.address)] })
2231
+ return postTransfer(app, {
2232
+ body: { ...createBody, amount: amount.toString(), sender: sender.address },
2233
+ idempotencyKey,
2234
+ })
2235
+ }
2236
+
2237
+ test('executes the prepared calls and returns successful receipts', async () => {
2238
+ const { client } = anvil
2239
+ await Anvil.setErc20Balance(client, {
2240
+ address: sender.address,
2241
+ amount,
2242
+ slot: fiatTokenBalanceSlot,
2243
+ token: usdc.address as `0x${string}`,
2244
+ })
2245
+
2246
+ const response = await createTransfer(`bridge_${executable.id}_${sender.address}`)
2247
+ expect(response.status).toBe(200)
2248
+ const transfer = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
2249
+ expect(transfer.action.type).toBe('evm:calls')
2250
+ if (transfer.action.type !== 'evm:calls') return
2251
+ expect(transfer.action.calls.length).toBeGreaterThan(0)
2252
+
2253
+ const receipts = []
2254
+ for (const call of transfer.action.calls) {
2255
+ receipts.push(
2256
+ await client.sendTransactionSync({
2257
+ account: sender,
2258
+ data: call.data as `0x${string}`,
2259
+ to: call.to as `0x${string}`,
2260
+ value: BigInt(call.value),
2261
+ }),
2262
+ )
2263
+ }
2264
+
2265
+ expect(receipts.map((receipt) => receipt.status)).toEqual(
2266
+ transfer.action.calls.map(() => 'success'),
2267
+ )
2268
+ // The deposit is the last call and must emit; an approval alone proves nothing.
2269
+ expect(receipts.at(-1)!.logs.length).toBeGreaterThan(0)
2270
+ })
2271
+
2272
+ test('bounds the approval to the quoted source spend', async () => {
2273
+ const response = await createTransfer(`bridge_bounds_${executable.id}_${sender.address}`)
2274
+ expect(response.status).toBe(200)
2275
+ const transfer = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
2276
+ if (transfer.action.type !== 'evm:calls') return
2277
+
2278
+ const approval = transfer.action.calls.find((call) =>
2279
+ call.data.toLowerCase().startsWith('0x095ea7b3'),
2280
+ )
2281
+ expect(approval).toBeDefined()
2282
+ if (!approval) return
2283
+
2284
+ // Decoded from the live action, so a provider that starts returning
2285
+ // an unlimited allowance fails here.
2286
+ const decoded = decodeFunctionData({ abi: erc20Abi, data: approval.data as `0x${string}` })
2287
+ expect(decoded.functionName).toBe('approve')
2288
+ if (decoded.functionName !== 'approve') return
2289
+ const [, approved] = decoded.args
2290
+ expect(approved).toBeLessThanOrEqual(BigInt(transfer.sourceAmount.baseUnits))
2291
+ expect(approved).toBeLessThan(maxUint256)
2292
+ })
2293
+ })
2294
+ }
2295
+ })
2296
+
2297
+ describe('POST /funding/transfers/{destination}', () => {
2298
+ test.each([
2299
+ { body: vaultTransferBody, destination: 'vault' },
2300
+ { body: zoneTransferBody, destination: 'zone' },
2301
+ ] as const)('validates and returns 501 for $destination transfers', async (options) => {
2302
+ const app = transfersApp()
2303
+ const before = await db.kysely
2304
+ .selectFrom('funding_transfers')
2305
+ .select(({ fn }) => fn.countAll<number>().as('count'))
2306
+ .executeTakeFirstOrThrow()
2307
+ const response = await app.request(`/v1/funding/transfers/${options.destination}`, {
2308
+ body: JSON.stringify(options.body),
2309
+ headers: {
2310
+ authorization: `Bearer ${fundingWriter.token}`,
2311
+ 'content-type': 'application/json',
2312
+ 'idempotency-key': `funding_${options.destination}_unimplemented`,
2313
+ },
2314
+ method: 'POST',
2315
+ })
2316
+
2317
+ expect(response.status).toBe(501)
2318
+ expect(response.headers.get('cache-control')).toBe('no-store')
2319
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
2320
+ 'not_implemented',
2321
+ )
2322
+ const after = await db.kysely
2323
+ .selectFrom('funding_transfers')
2324
+ .select(({ fn }) => fn.countAll<number>().as('count'))
2325
+ .executeTakeFirstOrThrow()
2326
+ expect(after.count).toBe(before.count)
2327
+ })
2328
+
2329
+ test('requires the header and validates destination fields', async () => {
2330
+ const app = transfersApp()
2331
+ const missingHeader = await app.request('/v1/funding/transfers/vault', {
2332
+ body: JSON.stringify(vaultTransferBody),
2333
+ headers: {
2334
+ authorization: `Bearer ${fundingWriter.token}`,
2335
+ 'content-type': 'application/json',
2336
+ },
2337
+ method: 'POST',
2338
+ })
2339
+ expect(missingHeader.status).toBe(400)
2340
+ expect(((await missingHeader.json()) as { error: { code: string } }).error.code).toBe(
2341
+ 'idempotency_key_required',
2342
+ )
2343
+
2344
+ const invalidZone = await app.request('/v1/funding/transfers/zone', {
2345
+ body: JSON.stringify({ ...zoneTransferBody, destinationChain: '421700001' }),
2346
+ headers: {
2347
+ authorization: `Bearer ${fundingWriter.token}`,
2348
+ 'content-type': 'application/json',
2349
+ 'idempotency-key': 'funding_zone_invalid',
2350
+ },
2351
+ method: 'POST',
2352
+ })
2353
+ expect(invalidZone.status).toBe(400)
2354
+ expect(((await invalidZone.json()) as { error: { code: string } }).error.code).toBe(
2355
+ 'body_invalid',
2356
+ )
2357
+ })
2358
+ })
2359
+
2360
+ describe('funding transfers OpenAPI', () => {
2361
+ test('publishes an executable account transfer example', async () => {
2362
+ const app = create({ auth: false })
2363
+ type TokenSchema = { examples: string[] }
2364
+ type TransferExample = { destinationToken: { address: string } }
2365
+ type Operation = {
2366
+ requestBody: {
2367
+ content: Record<string, { schema: { properties: { destinationToken: TokenSchema } } }>
2368
+ }
2369
+ responses: Record<string, { content: Record<string, { example: TransferExample }> }>
2370
+ }
2371
+ type Spec = { paths: Record<string, { post: Operation }> }
2372
+ const spec = (await (await app.request('/openapi.json')).json()) as Spec
2373
+ const operation = spec.paths['/v1/funding/transfers']!.post
2374
+
2375
+ expect({
2376
+ request:
2377
+ operation.requestBody.content['application/json']!.schema.properties.destinationToken
2378
+ .examples,
2379
+ response:
2380
+ operation.responses['200']!.content['application/json']!.example.destinationToken.address,
2381
+ }).toEqual({
2382
+ request: ['pathusd'],
2383
+ response: '0x20c0000000000000000000000000000000000000',
2384
+ })
2385
+ })
2386
+
2387
+ test('publishes human-readable deposit address route examples', async () => {
2388
+ const app = create({ auth: false })
2389
+ type Property = { examples: string[] }
2390
+ type Operation = {
2391
+ requestBody: {
2392
+ content: Record<
2393
+ string,
2394
+ {
2395
+ schema: {
2396
+ properties: {
2397
+ destinationToken: Property
2398
+ sourceChain: Property
2399
+ sourceToken: Property
2400
+ }
2401
+ }
2402
+ }
2403
+ >
2404
+ }
2405
+ }
2406
+ type Spec = { paths: Record<string, { post: Operation }> }
2407
+ const spec = (await (await app.request('/openapi.json')).json()) as Spec
2408
+ const properties =
2409
+ spec.paths['/v1/funding/deposit-addresses']!.post.requestBody.content['application/json']!
2410
+ .schema.properties
2411
+
2412
+ expect({
2413
+ destinationToken: properties.destinationToken.examples,
2414
+ sourceChain: properties.sourceChain.examples,
2415
+ sourceToken: properties.sourceToken.examples,
2416
+ }).toMatchInlineSnapshot(`
2417
+ {
2418
+ "destinationToken": [
2419
+ "usdt0",
2420
+ ],
2421
+ "sourceChain": [
2422
+ "tron",
2423
+ "tron:0x2b6653dc",
2424
+ ],
2425
+ "sourceToken": [
2426
+ "usdt",
2427
+ ],
2428
+ }
2429
+ `)
2430
+ })
2431
+
2432
+ test('publishes concise operation documentation', async () => {
2433
+ const app = create({ auth: false })
2434
+ type Operation = { description: string; summary: string; tags: string[] }
2435
+ type Spec = {
2436
+ paths: Record<string, { get?: Operation; post?: Operation }>
2437
+ tags: { description?: string; name: string }[]
2438
+ }
2439
+ const spec = (await (await app.request('/openapi.json')).json()) as Spec
2440
+ const detailPath = Object.keys(spec.paths).find((path) =>
2441
+ path.includes('/v1/funding/transfers/{'),
2442
+ )!
2443
+ const depositAddressPath = Object.keys(spec.paths).find(
2444
+ (path) => path.includes('/v1/funding/deposit-addresses/{') && !path.endsWith('/deposits'),
2445
+ )!
2446
+ const depositAddressDepositsPath = Object.keys(spec.paths).find((path) =>
2447
+ path.endsWith('/deposits'),
2448
+ )!
2449
+ const depositPath = Object.keys(spec.paths).find((path) =>
2450
+ path.includes('/v1/funding/deposits/{'),
2451
+ )!
2452
+ const summarize = (operation: Operation) => ({
2453
+ description: operation.description,
2454
+ summary: operation.summary,
2455
+ })
2456
+ const operations = {
2457
+ account: summarize(spec.paths['/v1/funding/transfers']!.post!),
2458
+ chains: summarize(spec.paths['/v1/funding/chains']!.get!),
2459
+ deposit: summarize(spec.paths[depositPath]!.get!),
2460
+ depositAddress: summarize(spec.paths[depositAddressPath]!.get!),
2461
+ depositAddressCreate: summarize(spec.paths['/v1/funding/deposit-addresses']!.post!),
2462
+ depositAddressDeposits: summarize(spec.paths[depositAddressDepositsPath]!.get!),
2463
+ detail: summarize(spec.paths[detailPath]!.get!),
2464
+ list: summarize(spec.paths['/v1/funding/transfers']!.get!),
2465
+ providers: summarize(spec.paths['/v1/funding/providers']!.get!),
2466
+ quotes: summarize(spec.paths['/v1/funding/quotes']!.get!),
2467
+ vault: summarize(spec.paths['/v1/funding/transfers/vault']!.post!),
2468
+ zone: summarize(spec.paths['/v1/funding/transfers/zone']!.post!),
2469
+ }
2470
+ const sidebar = Object.values(spec.paths).flatMap((path) =>
2471
+ [path.get, path.post].flatMap((operation) =>
2472
+ operation?.tags.includes('Funding') ? [operation.summary] : [],
2473
+ ),
2474
+ )
2475
+
2476
+ for (const operation of Object.values(operations))
2477
+ expect(operation.description.split(/\s+/).length).toBeLessThan(15)
2478
+
2479
+ expect({
2480
+ category: spec.tags.find((tag) => tag.name === 'Funding')?.description,
2481
+ operations,
2482
+ sidebar,
2483
+ }).toMatchInlineSnapshot(`
2484
+ {
2485
+ "category": "Quote, transfer, and deposit management for funding Tempo from other chains.",
2486
+ "operations": {
2487
+ "account": {
2488
+ "description": "Creates a transfer to a Tempo account.",
2489
+ "summary": "Create transfer",
2490
+ },
2491
+ "chains": {
2492
+ "description": "Lists supported source chains and tokens.",
2493
+ "summary": "Get chains",
2494
+ },
2495
+ "deposit": {
2496
+ "description": "Returns one detected deposit and its delivery status.",
2497
+ "summary": "Get deposit",
2498
+ },
2499
+ "depositAddress": {
2500
+ "description": "Returns one reusable funding deposit address.",
2501
+ "summary": "Get deposit address",
2502
+ },
2503
+ "depositAddressCreate": {
2504
+ "description": "Creates or returns a reusable deposit address for funding a Tempo account.",
2505
+ "summary": "Create deposit address",
2506
+ },
2507
+ "depositAddressDeposits": {
2508
+ "description": "Lists deposits detected for one funding deposit address, newest first.",
2509
+ "summary": "List deposits",
2510
+ },
2511
+ "detail": {
2512
+ "description": "Returns a transfer by ID.",
2513
+ "summary": "Get transfer",
2514
+ },
2515
+ "list": {
2516
+ "description": "Lists transfers from newest to oldest.",
2517
+ "summary": "List transfers",
2518
+ },
2519
+ "providers": {
2520
+ "description": "Lists available transfer providers.",
2521
+ "summary": "Get providers",
2522
+ },
2523
+ "quotes": {
2524
+ "description": "Returns live quotes for transferring stablecoins to Tempo.",
2525
+ "summary": "Get quotes",
2526
+ },
2527
+ "vault": {
2528
+ "description": "Creates a transfer into a Tempo Earn vault.",
2529
+ "summary": "Create transfer into vault",
2530
+ },
2531
+ "zone": {
2532
+ "description": "Creates a transfer into a Tempo Zone.",
2533
+ "summary": "Create transfer into zone",
2534
+ },
2535
+ },
2536
+ "sidebar": [
2537
+ "Get quotes",
2538
+ "Get chains",
2539
+ "Get providers",
2540
+ "Get deposit address",
2541
+ "Get deposit",
2542
+ "List deposits",
2543
+ "Create deposit address",
2544
+ "List transfers",
2545
+ "Create transfer",
2546
+ "Create transfer into vault",
2547
+ "Create transfer into zone",
2548
+ "Get transfer",
2549
+ ],
2550
+ }
2551
+ `)
2552
+ })
2553
+
2554
+ test('publishes concrete creation schemas', async () => {
2555
+ const app = create({ auth: false })
2556
+ const spec = (await (await app.request('/openapi.json')).json()) as {
2557
+ paths: Record<
2558
+ string,
2559
+ {
2560
+ post?: {
2561
+ requestBody: {
2562
+ content: Record<
2563
+ string,
2564
+ {
2565
+ schema: {
2566
+ additionalProperties?: boolean
2567
+ properties: object
2568
+ required?: readonly string[] | undefined
2569
+ }
2570
+ }
2571
+ >
2572
+ }
2573
+ responses: Record<string, unknown>
2574
+ }
2575
+ }
2576
+ >
2577
+ }
2578
+ const creationSchema = (path: string) =>
2579
+ spec.paths[path]!.post!.requestBody.content['application/json']!.schema
2580
+ expect(
2581
+ creationSchema('/v1/funding/deposit-addresses').required?.includes('subsidize') ?? false,
2582
+ ).toMatchInlineSnapshot(`false`)
2583
+ const summarize = (path: string) => {
2584
+ const schema = creationSchema(path)
2585
+ return {
2586
+ additionalProperties: schema.additionalProperties,
2587
+ properties: Object.keys(schema.properties),
2588
+ }
2589
+ }
2590
+
2591
+ expect({
2592
+ account: summarize('/v1/funding/transfers'),
2593
+ depositAddress: summarize('/v1/funding/deposit-addresses'),
2594
+ depositAddressResponses: Object.keys(
2595
+ spec.paths['/v1/funding/deposit-addresses']!.post!.responses,
2596
+ ),
2597
+ vault: summarize('/v1/funding/transfers/vault'),
2598
+ vaultResponses: Object.keys(spec.paths['/v1/funding/transfers/vault']!.post!.responses),
2599
+ zone: summarize('/v1/funding/transfers/zone'),
2600
+ zoneResponses: Object.keys(spec.paths['/v1/funding/transfers/zone']!.post!.responses),
2601
+ }).toMatchInlineSnapshot(`
2602
+ {
2603
+ "account": {
2604
+ "additionalProperties": false,
2605
+ "properties": [
2606
+ "amount",
2607
+ "destinationToken",
2608
+ "mode",
2609
+ "recipient",
2610
+ "sender",
2611
+ "slippageBps",
2612
+ "sourceChain",
2613
+ "sourceToken",
2614
+ ],
2615
+ },
2616
+ "depositAddress": {
2617
+ "additionalProperties": false,
2618
+ "properties": [
2619
+ "amount",
2620
+ "destinationToken",
2621
+ "recipient",
2622
+ "refundAddress",
2623
+ "sourceChain",
2624
+ "sourceToken",
2625
+ "subsidize",
2626
+ ],
2627
+ },
2628
+ "depositAddressResponses": [
2629
+ "200",
2630
+ "400",
2631
+ "401",
2632
+ "404",
2633
+ "409",
2634
+ "429",
2635
+ "500",
2636
+ "502",
2637
+ ],
2638
+ "vault": {
2639
+ "additionalProperties": false,
2640
+ "properties": [
2641
+ "amount",
2642
+ "mode",
2643
+ "recipient",
2644
+ "sender",
2645
+ "slippageBps",
2646
+ "sourceChain",
2647
+ "sourceToken",
2648
+ "vaultAddress",
2649
+ ],
2650
+ },
2651
+ "vaultResponses": [
2652
+ "400",
2653
+ "401",
2654
+ "429",
2655
+ "500",
2656
+ "501",
2657
+ ],
2658
+ "zone": {
2659
+ "additionalProperties": false,
2660
+ "properties": [
2661
+ "amount",
2662
+ "destinationChain",
2663
+ "destinationToken",
2664
+ "mode",
2665
+ "recipient",
2666
+ "recipientFallback",
2667
+ "sender",
2668
+ "slippageBps",
2669
+ "sourceChain",
2670
+ "sourceToken",
2671
+ ],
2672
+ },
2673
+ "zoneResponses": [
2674
+ "400",
2675
+ "401",
2676
+ "429",
2677
+ "500",
2678
+ "501",
2679
+ ],
2680
+ }
2681
+ `)
2682
+ })
2683
+
2684
+ test('publishes alphabetically ordered example fields and parameter examples', async () => {
2685
+ const app = create({ auth: false })
2686
+ const spec = (await (await app.request('/openapi.json')).json()) as {
2687
+ paths: Record<
2688
+ string,
2689
+ {
2690
+ get: {
2691
+ parameters?: { example?: unknown; name: string; schema?: { examples?: unknown } }[]
2692
+ responses: Record<string, { content: Record<string, { example: { data: {}[] } }> }>
2693
+ }
2694
+ }
2695
+ >
2696
+ }
2697
+ const operation = spec.paths['/v1/funding/transfers']!.get
2698
+ const fields = Object.keys(operation.responses['200']!.content['application/json']!.example.data[0]!) // prettier-ignore
2699
+ expect(fields).toEqual([...fields].sort())
2700
+ for (const parameter of operation.parameters ?? [])
2701
+ expect(parameter.example ?? parameter.schema?.examples, parameter.name).toBeDefined()
2702
+ })
2703
+
2704
+ test('publishes operation ids and required scopes', async () => {
2705
+ const app = create({ auth: false })
2706
+ const spec = (await (await app.request('/openapi.json')).json()) as {
2707
+ paths: Record<
2708
+ string,
2709
+ {
2710
+ get: { operationId: string; 'x-required-scopes': string[] }
2711
+ post?: {
2712
+ operationId: string
2713
+ parameters: { in: string; name: string; required: boolean }[]
2714
+ 'x-required-scopes': string[]
2715
+ }
2716
+ }
2717
+ >
2718
+ }
2719
+ const detailPath = Object.keys(spec.paths).find((path) =>
2720
+ path.includes('/v1/funding/transfers/{'),
2721
+ )
2722
+ const depositAddressPath = Object.keys(spec.paths).find(
2723
+ (path) => path.includes('/v1/funding/deposit-addresses/{') && !path.endsWith('/deposits'),
2724
+ )
2725
+ const depositAddressDepositsPath = Object.keys(spec.paths).find((path) =>
2726
+ path.endsWith('/deposits'),
2727
+ )
2728
+ const depositPath = Object.keys(spec.paths).find((path) =>
2729
+ path.includes('/v1/funding/deposits/{'),
2730
+ )
2731
+ expect({
2732
+ deposit: depositPath && spec.paths[depositPath]?.get.operationId,
2733
+ depositAddress: depositAddressPath && spec.paths[depositAddressPath]?.get.operationId,
2734
+ depositAddressCreate: spec.paths['/v1/funding/deposit-addresses']?.post?.operationId,
2735
+ depositAddressCreateScopes:
2736
+ spec.paths['/v1/funding/deposit-addresses']?.post?.['x-required-scopes'],
2737
+ depositAddressDeposits:
2738
+ depositAddressDepositsPath && spec.paths[depositAddressDepositsPath]?.get.operationId,
2739
+ depositAddressDepositsScopes:
2740
+ depositAddressDepositsPath &&
2741
+ spec.paths[depositAddressDepositsPath]?.get['x-required-scopes'],
2742
+ depositAddressScopes:
2743
+ depositAddressPath && spec.paths[depositAddressPath]?.get['x-required-scopes'],
2744
+ depositScopes: depositPath && spec.paths[depositPath]?.get['x-required-scopes'],
2745
+ detail: detailPath && spec.paths[detailPath]?.get.operationId,
2746
+ detailScopes: detailPath && spec.paths[detailPath]?.get['x-required-scopes'],
2747
+ list: spec.paths['/v1/funding/transfers']?.get.operationId,
2748
+ listScopes: spec.paths['/v1/funding/transfers']?.get['x-required-scopes'],
2749
+ requiredCreateHeader: spec.paths['/v1/funding/transfers']?.post?.parameters.find(
2750
+ (parameter) => parameter.name === 'idempotency-key',
2751
+ ),
2752
+ vault: spec.paths['/v1/funding/transfers/vault']?.post?.operationId,
2753
+ vaultScopes: spec.paths['/v1/funding/transfers/vault']?.post?.['x-required-scopes'],
2754
+ zone: spec.paths['/v1/funding/transfers/zone']?.post?.operationId,
2755
+ zoneScopes: spec.paths['/v1/funding/transfers/zone']?.post?.['x-required-scopes'],
2756
+ }).toEqual({
2757
+ deposit: 'getFundingDeposit',
2758
+ depositAddress: 'getFundingDepositAddress',
2759
+ depositAddressCreate: 'createFundingDepositAddress',
2760
+ depositAddressCreateScopes: ['funding:write'],
2761
+ depositAddressDeposits: 'listFundingDepositAddressDeposits',
2762
+ depositAddressDepositsScopes: ['funding:read'],
2763
+ depositAddressScopes: ['funding:read'],
2764
+ depositScopes: ['funding:read'],
2765
+ detail: 'getFundingTransfer',
2766
+ detailScopes: ['funding:read'],
2767
+ list: 'listFundingTransfers',
2768
+ listScopes: ['funding:read'],
2769
+ requiredCreateHeader: expect.objectContaining({
2770
+ description: expect.stringContaining('24 hours'),
2771
+ in: 'header',
2772
+ name: 'idempotency-key',
2773
+ required: true,
2774
+ schema: expect.any(Object),
2775
+ }),
2776
+ vault: 'createFundingTransferVault',
2777
+ vaultScopes: ['funding:write'],
2778
+ zone: 'createFundingTransferZone',
2779
+ zoneScopes: ['funding:write'],
2780
+ })
2781
+ })
2782
+ })
2783
+
2784
+ const baseUsdcQuery = {
2785
+ sourceAmount: '1000000',
2786
+ sourceChain: 'base',
2787
+ sourceToken: 'USDC',
2788
+ } as const
2789
+
2790
+ type QuoteQuery = Record<string, string>
2791
+
2792
+ function quoteUrl(query: QuoteQuery = baseUsdcQuery) {
2793
+ return `/v1/funding/quotes?${new URLSearchParams(
2794
+ Object.entries(query).map(([key, value]) => [key, String(value)]),
2795
+ )}`
2796
+ }
2797
+
2798
+ function relayProvider(onGetQuote: () => void = () => {}) {
2799
+ return withGetQuote('relay', async (input, signal) => {
2800
+ onGetQuote()
2801
+ expect(signal.aborted).toBe(false)
2802
+ expect(input.candidate.id).toBe('base-usdc-tempo-usdce-relay')
2803
+ return {
2804
+ destinationAmountMin: '980000',
2805
+ destinationAmount: '990000',
2806
+ quality: {
2807
+ estimatedSeconds: 4,
2808
+ liquiditySource: 'providerQuote',
2809
+ sourceDetail: 'relay:quote-v2',
2810
+ tier: 'liquid',
2811
+ },
2812
+ sampledAt: '2026-01-01T00:00:00Z',
2813
+ status: 'available',
2814
+ }
2815
+ })
2816
+ }
2817
+
2818
+ function acrossProvider(onGetQuote: () => void = () => {}) {
2819
+ return withGetQuote('across', async (input, signal) => {
2820
+ onGetQuote()
2821
+ expect(signal.aborted).toBe(false)
2822
+ expect(input.candidate.id).toBe('base-usdc-tempo-usdce-across')
2823
+ return {
2824
+ expiresAt: '2026-01-01T00:01:00Z',
2825
+ destinationAmountMin: '988000',
2826
+ destinationAmount: '990000',
2827
+ quality: {
2828
+ estimatedSeconds: 2,
2829
+ liquiditySource: 'providerQuote',
2830
+ sourceDetail: 'across:swap-approval',
2831
+ tier: 'liquid',
2832
+ },
2833
+ sampledAt: '2026-01-01T00:00:00Z',
2834
+ status: 'available',
2835
+ }
2836
+ })
2837
+ }
2838
+
2839
+ function rhinoProvider() {
2840
+ return withGetQuote('rhino', async (input, signal) => {
2841
+ expect(signal.aborted).toBe(false)
2842
+ expect(input.candidate.id).toBe('solana-usdc-tempo-usdce-rhino')
2843
+ return {
2844
+ destinationAmount: '983000',
2845
+ quality: {
2846
+ liquiditySource: 'providerQuote',
2847
+ sourceDetail: 'rhino:bridge-swap-public',
2848
+ tier: 'liquid',
2849
+ },
2850
+ sampledAt: '2026-01-01T00:00:00Z',
2851
+ status: 'available',
2852
+ }
2853
+ })
2854
+ }
2855
+
2856
+ function withGetQuote(
2857
+ id: FundingProvider.ProviderId,
2858
+ getQuote: FundingProvider.Provider['getQuote'],
2859
+ ) {
2860
+ const provider = providers.find((entry) => entry.id === id)
2861
+ if (!provider) throw new Error(`Missing ${id} provider`)
2862
+ return FundingProvider.from({ ...provider, getQuote })
2863
+ }
2864
+
2865
+ function withCreateDepositAddress(
2866
+ id: FundingProvider.ProviderId,
2867
+ createDepositAddress: NonNullable<FundingProvider.Provider['createDepositAddress']>,
2868
+ ) {
2869
+ const provider = providers.find((entry) => entry.id === id)
2870
+ if (!provider) throw new Error(`Missing ${id} provider`)
2871
+ return FundingProvider.from({ ...provider, createDepositAddress })
2872
+ }
2873
+
2874
+ function depositAddressProvider(
2875
+ address = 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
2876
+ onCreate: (input: FundingProvider.createDepositAddress.Parameters) => void = () => {},
2877
+ ) {
2878
+ return withCreateDepositAddress('relay', async (input) => {
2879
+ onCreate(input)
2880
+ return {
2881
+ address,
2882
+ correlation: {
2883
+ checkEndpoint: '/intents/status/v3?requestId=relay_request_1',
2884
+ requestId: 'relay_request_1',
2885
+ },
2886
+ destinationAmount: '1000000',
2887
+ destinationAmountMin: '1000000',
2888
+ fees: [],
2889
+ sampledAt: '2026-08-04T00:00:00.000Z',
2890
+ }
2891
+ })
2892
+ }
2893
+
2894
+ /** API key with funding read access, owned by `org_funding`. */
2895
+ const fundingReader = {
2896
+ id: 'key_funding',
2897
+ orgId: 'org_funding',
2898
+ scopes: ['funding:read'],
2899
+ token: 'secret_funding_reader',
2900
+ } satisfies TestApp.kvStore.Key
2901
+
2902
+ /** Project-attributed key in the same org, used to prove project narrowing. */
2903
+ const fundingProjectReader = {
2904
+ id: 'key_funding_project',
2905
+ orgId: 'org_funding',
2906
+ projectId: 'prj_funding',
2907
+ scopes: ['funding:read'],
2908
+ token: 'secret_funding_project',
2909
+ } satisfies TestApp.kvStore.Key
2910
+
2911
+ /** Key in a different org, used to prove owner isolation. */
2912
+ const fundingForeign = {
2913
+ id: 'key_funding_other',
2914
+ orgId: 'org_funding_other',
2915
+ scopes: ['funding:read'],
2916
+ token: 'secret_funding_other',
2917
+ } satisfies TestApp.kvStore.Key
2918
+
2919
+ /** Key with funding write access, owned by `org_funding`. */
2920
+ const fundingWriter = {
2921
+ id: 'key_funding_writer',
2922
+ orgId: 'org_funding',
2923
+ scopes: ['funding:read', 'funding:write'],
2924
+ token: 'secret_funding_writer',
2925
+ } satisfies TestApp.kvStore.Key
2926
+
2927
+ function transfersApp(options: TestApp.create.Options = {}) {
2928
+ return create({
2929
+ ...options,
2930
+ auth: {
2931
+ keys: [TestApp.key, fundingReader, fundingProjectReader, fundingForeign, fundingWriter],
2932
+ },
2933
+ defaultChainId: options.defaultChainId ?? tempoMainnet.id,
2934
+ })
2935
+ }
2936
+
2937
+ /** RequestInit carrying a bearer token for the given key. */
2938
+ function as(key: { token: string }) {
2939
+ return { headers: { authorization: `Bearer ${key.token}` } } as const
2940
+ }
2941
+
2942
+ let seedDepositAddressSecond = 0
2943
+ function seedDepositAddress(input: Partial<FundingDepositAddress.create.Input> = {}) {
2944
+ const index = seedDepositAddressSecond++
2945
+ const now = new Date(Date.UTC(2026, 0, 3, 0, 0, index))
2946
+ return FundingDepositAddress.create(db, {
2947
+ apiKeyId: fundingReader.id,
2948
+ deliveryStrategy: 'provider',
2949
+ environment: 'production',
2950
+ id: FundingDepositAddress.generateId(now),
2951
+ now,
2952
+ orgId: fundingReader.orgId,
2953
+ providerOutputToken: TestFunding.providerOutputToken(),
2954
+ snapshot: TestFunding.depositAddressSnapshot({
2955
+ provider: { id: `relay_route_${index}`, name: 'Relay' },
2956
+ recipient: `0x${index.toString(16).padStart(40, '0')}`,
2957
+ }),
2958
+ ...input,
2959
+ })
2960
+ }
2961
+
2962
+ let seedDepositSecond = 0
2963
+ function seedDeposit(
2964
+ address: Awaited<ReturnType<typeof seedDepositAddress>>,
2965
+ input: Partial<FundingDeposit.create.Input> = {},
2966
+ ) {
2967
+ const index = seedDepositSecond++
2968
+ const now = new Date(Date.UTC(2026, 0, 4, 0, 0, index))
2969
+ const sourceTransactionHash = index.toString(16).padStart(64, '0')
2970
+ return FundingDeposit.create(db, {
2971
+ now,
2972
+ providerRequestId: `relay_request_${index}`,
2973
+ snapshot: TestFunding.depositSnapshot(address.id, {
2974
+ provider: address.snapshot.provider,
2975
+ sourceTransactionHashes: [sourceTransactionHash],
2976
+ }),
2977
+ sourceTransactionHash,
2978
+ sourceTransferIndex: 0,
2979
+ ...input,
2980
+ })
2981
+ }
2982
+
2983
+ let seedSecond = 0
2984
+ function seedTransfer(input: Partial<FundingTransfer.create.Input> = {}) {
2985
+ // Distinct create times keep ids strictly ordered for paging assertions.
2986
+ return FundingTransfer.create(db, {
2987
+ apiKeyId: fundingReader.id,
2988
+ environment: 'production',
2989
+ id: FundingTransfer.generateId(new Date(Date.UTC(2026, 0, 2, 0, 0, seedSecond++))),
2990
+ orgId: fundingReader.orgId,
2991
+ snapshot: TestFunding.transferSnapshot(),
2992
+ ...input,
2993
+ })
503
2994
  }