tapimo 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. package/dist/App.d.ts +11333 -9563
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +39 -3
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +2 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +17 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/Webhooks.js +2 -2
  10. package/dist/Webhooks.js.map +1 -1
  11. package/dist/admin/App.d.ts +1064 -924
  12. package/dist/admin/App.d.ts.map +1 -1
  13. package/dist/admin/apps/api-keys.d.ts +80 -0
  14. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  15. package/dist/admin/apps/api-keys.js +54 -0
  16. package/dist/admin/apps/api-keys.js.map +1 -1
  17. package/dist/admin/apps/earn-vaults.d.ts +63 -15
  18. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  19. package/dist/admin/apps/earn-vaults.js +2 -1
  20. package/dist/admin/apps/earn-vaults.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +92 -3
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/apps/organizations.js +69 -7
  24. package/dist/admin/apps/organizations.js.map +1 -1
  25. package/dist/admin/apps/verified-tokens.d.ts +7 -7
  26. package/dist/admin/ui.generated.d.ts +1 -1
  27. package/dist/admin/ui.generated.d.ts.map +1 -1
  28. package/dist/admin/ui.generated.js +151 -151
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +28 -28
  31. package/dist/apps/Zones.js +7 -7
  32. package/dist/apps/data/App.d.ts +2909 -2523
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/App.js +26 -21
  35. package/dist/apps/data/App.js.map +1 -1
  36. package/dist/apps/data/ZoneSelection.d.ts +8 -8
  37. package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
  38. package/dist/apps/data/ZoneSelection.js +1 -1
  39. package/dist/apps/data/ZoneSelection.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +588 -584
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +1561 -1239
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +22 -22
  45. package/dist/apps/data/routes/balances.d.ts +26 -26
  46. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.js +43 -16
  48. package/dist/apps/data/routes/balances.js.map +1 -1
  49. package/dist/apps/data/routes/blocks.d.ts +44 -44
  50. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  51. package/dist/apps/data/routes/blocks.js +21 -11
  52. package/dist/apps/data/routes/blocks.js.map +1 -1
  53. package/dist/apps/data/routes/coingecko.d.ts +3 -3
  54. package/dist/apps/data/routes/earn.d.ts +201 -25
  55. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  56. package/dist/apps/data/routes/earn.js +434 -75
  57. package/dist/apps/data/routes/earn.js.map +1 -1
  58. package/dist/apps/data/routes/exchanges.d.ts +160 -145
  59. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  60. package/dist/apps/data/routes/exchanges.js +180 -114
  61. package/dist/apps/data/routes/exchanges.js.map +1 -1
  62. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  63. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  64. package/dist/apps/data/routes/fee-amm.js +18 -19
  65. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  66. package/dist/apps/data/routes/receipts.d.ts +155 -55
  67. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  68. package/dist/apps/data/routes/receipts.js +28 -10
  69. package/dist/apps/data/routes/receipts.js.map +1 -1
  70. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  71. package/dist/apps/data/routes/tokens.d.ts +5 -5
  72. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  73. package/dist/apps/data/routes/tokens.js +51 -20
  74. package/dist/apps/data/routes/tokens.js.map +1 -1
  75. package/dist/apps/data/routes/transactions.d.ts +7 -2
  76. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  77. package/dist/apps/data/routes/transactions.js +37 -17
  78. package/dist/apps/data/routes/transactions.js.map +1 -1
  79. package/dist/apps/data/routes/transfers.d.ts +13 -13
  80. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  81. package/dist/apps/data/routes/transfers.js +18 -12
  82. package/dist/apps/data/routes/transfers.js.map +1 -1
  83. package/dist/apps/data/routes/valuation.d.ts +31 -31
  84. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  85. package/dist/apps/data/routes/valuation.js +12 -7
  86. package/dist/apps/data/routes/valuation.js.map +1 -1
  87. package/dist/apps/data/routes/verified-tokens.d.ts +8 -8
  88. package/dist/apps/data/routes/webhooks.d.ts +816 -113
  89. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  90. package/dist/apps/data/routes/webhooks.js +47 -5
  91. package/dist/apps/data/routes/webhooks.js.map +1 -1
  92. package/dist/apps/data/routes/zones.d.ts +14 -14
  93. package/dist/apps/data/routes/zones.js +1 -1
  94. package/dist/apps/funding/App.d.ts +466 -44
  95. package/dist/apps/funding/App.d.ts.map +1 -1
  96. package/dist/apps/funding/App.js +1 -1
  97. package/dist/apps/funding/App.js.map +1 -1
  98. package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
  99. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  100. package/dist/apps/funding/routes/deposit-addresses.js +17 -12
  101. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  102. package/dist/apps/funding/routes/transfers.d.ts +650 -57
  103. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  104. package/dist/apps/funding/routes/transfers.js +238 -24
  105. package/dist/apps/funding/routes/transfers.js.map +1 -1
  106. package/dist/apps/management/App.d.ts +5120 -4152
  107. package/dist/apps/management/App.d.ts.map +1 -1
  108. package/dist/apps/management/Billing.d.ts +1 -1
  109. package/dist/apps/management/Billing.d.ts.map +1 -1
  110. package/dist/apps/management/Billing.js +17 -1
  111. package/dist/apps/management/Billing.js.map +1 -1
  112. package/dist/apps/management/routes/api-keys.d.ts +632 -81
  113. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  114. package/dist/apps/management/routes/api-keys.js +236 -4
  115. package/dist/apps/management/routes/api-keys.js.map +1 -1
  116. package/dist/apps/management/routes/billing.d.ts +76 -76
  117. package/dist/apps/management/routes/faucet.d.ts +6 -6
  118. package/dist/apps/management/routes/invitations.d.ts +24 -24
  119. package/dist/apps/management/routes/invite-links.d.ts +4 -4
  120. package/dist/apps/management/routes/members.d.ts +16 -16
  121. package/dist/apps/management/routes/orgs.d.ts +20 -20
  122. package/dist/apps/management/routes/projects.d.ts +4 -4
  123. package/dist/apps/management/routes/usage.d.ts +8 -8
  124. package/dist/apps/management/routes/verified-token-requests.d.ts +17 -17
  125. package/dist/apps/management/routes/webhooks.d.ts +675 -25
  126. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  127. package/dist/apps/management/routes/webhooks.js +21 -21
  128. package/dist/apps/management/routes/webhooks.js.map +1 -1
  129. package/dist/apps/relay/App.d.ts.map +1 -1
  130. package/dist/apps/relay/App.js +115 -23
  131. package/dist/apps/relay/App.js.map +1 -1
  132. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  133. package/dist/apps/relay/Sponsorships.js +27 -1
  134. package/dist/apps/relay/Sponsorships.js.map +1 -1
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +34 -1
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +11 -5
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +21 -3
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  143. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  144. package/dist/db/tables/fundingCatalog.js +5 -1
  145. package/dist/db/tables/fundingCatalog.js.map +1 -1
  146. package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
  147. package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
  148. package/dist/db/tables/fundingTransferTransactions.js +44 -3
  149. package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
  150. package/dist/db/tables/fundingTransfers.d.ts +17 -0
  151. package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
  152. package/dist/db/tables/fundingTransfers.js +21 -0
  153. package/dist/db/tables/fundingTransfers.js.map +1 -1
  154. package/dist/db/tables/organizations.d.ts +24 -0
  155. package/dist/db/tables/organizations.d.ts.map +1 -1
  156. package/dist/db/tables/organizations.js +27 -0
  157. package/dist/db/tables/organizations.js.map +1 -1
  158. package/dist/db/tables/projects.d.ts +25 -1
  159. package/dist/db/tables/projects.d.ts.map +1 -1
  160. package/dist/db/tables/projects.js +26 -0
  161. package/dist/db/tables/projects.js.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
  163. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  164. package/dist/db/tables/sponsoredTransactions.js +146 -13
  165. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  166. package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
  167. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  168. package/dist/db/tables/webhookSubscriptions.js +9 -6
  169. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  170. package/dist/handlers/internal/utils.d.ts.map +1 -1
  171. package/dist/handlers/internal/utils.js +9 -0
  172. package/dist/handlers/internal/utils.js.map +1 -1
  173. package/dist/internal/Auth.d.ts +3 -1
  174. package/dist/internal/Auth.d.ts.map +1 -1
  175. package/dist/internal/Auth.js +6 -3
  176. package/dist/internal/Auth.js.map +1 -1
  177. package/dist/internal/EarnVaults.d.ts +3 -3
  178. package/dist/internal/EarnVaults.js +5 -5
  179. package/dist/internal/EarnVaults.js.map +1 -1
  180. package/dist/internal/Fetch.d.ts +23 -0
  181. package/dist/internal/Fetch.d.ts.map +1 -0
  182. package/dist/internal/Fetch.js +26 -0
  183. package/dist/internal/Fetch.js.map +1 -0
  184. package/dist/internal/Log.d.ts +4 -2
  185. package/dist/internal/Log.d.ts.map +1 -1
  186. package/dist/internal/Log.js +16 -3
  187. package/dist/internal/Log.js.map +1 -1
  188. package/dist/internal/OpenApi.d.ts +5 -3
  189. package/dist/internal/OpenApi.d.ts.map +1 -1
  190. package/dist/internal/OpenApi.js +13 -3
  191. package/dist/internal/OpenApi.js.map +1 -1
  192. package/dist/internal/Response.d.ts +1 -1
  193. package/dist/internal/Response.d.ts.map +1 -1
  194. package/dist/internal/Schema.d.ts +6 -3
  195. package/dist/internal/Schema.d.ts.map +1 -1
  196. package/dist/internal/Schema.js +8 -2
  197. package/dist/internal/Schema.js.map +1 -1
  198. package/dist/internal/Store.d.ts +16 -3
  199. package/dist/internal/Store.d.ts.map +1 -1
  200. package/dist/internal/Store.js +82 -16
  201. package/dist/internal/Store.js.map +1 -1
  202. package/dist/internal/Tidx.d.ts.map +1 -1
  203. package/dist/internal/Tidx.js +7 -2
  204. package/dist/internal/Tidx.js.map +1 -1
  205. package/dist/internal/Timeout.d.ts +36 -0
  206. package/dist/internal/Timeout.d.ts.map +1 -0
  207. package/dist/internal/Timeout.js +37 -0
  208. package/dist/internal/Timeout.js.map +1 -0
  209. package/dist/internal/Timing.d.ts +9 -0
  210. package/dist/internal/Timing.d.ts.map +1 -1
  211. package/dist/internal/Timing.js +7 -0
  212. package/dist/internal/Timing.js.map +1 -1
  213. package/dist/internal/Viem.d.ts +16 -1
  214. package/dist/internal/Viem.d.ts.map +1 -1
  215. package/dist/internal/Viem.js +12 -2
  216. package/dist/internal/Viem.js.map +1 -1
  217. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  218. package/dist/internal/WebhookDestination.js +3 -0
  219. package/dist/internal/WebhookDestination.js.map +1 -1
  220. package/dist/internal/funding/Action.d.ts +9 -0
  221. package/dist/internal/funding/Action.d.ts.map +1 -1
  222. package/dist/internal/funding/Action.js +16 -8
  223. package/dist/internal/funding/Action.js.map +1 -1
  224. package/dist/internal/funding/Catalog.d.ts +9 -2
  225. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  226. package/dist/internal/funding/Catalog.js +19 -8
  227. package/dist/internal/funding/Catalog.js.map +1 -1
  228. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
  229. package/dist/internal/funding/DepositWebhook.js +2 -1
  230. package/dist/internal/funding/DepositWebhook.js.map +1 -1
  231. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  232. package/dist/internal/funding/Evidence.js +2 -10
  233. package/dist/internal/funding/Evidence.js.map +1 -1
  234. package/dist/internal/funding/Provider.d.ts +108 -13
  235. package/dist/internal/funding/Provider.d.ts.map +1 -1
  236. package/dist/internal/funding/Provider.js +34 -8
  237. package/dist/internal/funding/Provider.js.map +1 -1
  238. package/dist/internal/funding/SourceObservation.d.ts.map +1 -1
  239. package/dist/internal/funding/SourceObservation.js +5 -7
  240. package/dist/internal/funding/SourceObservation.js.map +1 -1
  241. package/dist/internal/funding/Transfer.d.ts +114 -18
  242. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  243. package/dist/internal/funding/Transfer.js +185 -55
  244. package/dist/internal/funding/Transfer.js.map +1 -1
  245. package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
  246. package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
  247. package/dist/internal/funding/TransferReconciliation.js +192 -0
  248. package/dist/internal/funding/TransferReconciliation.js.map +1 -0
  249. package/dist/internal/funding/TransferWebhook.d.ts +21 -0
  250. package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
  251. package/dist/internal/funding/TransferWebhook.js +43 -0
  252. package/dist/internal/funding/TransferWebhook.js.map +1 -0
  253. package/dist/internal/funding/index.d.ts +1 -0
  254. package/dist/internal/funding/index.d.ts.map +1 -1
  255. package/dist/internal/funding/index.js +1 -0
  256. package/dist/internal/funding/index.js.map +1 -1
  257. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  258. package/dist/internal/funding/providers/relay.js +3 -12
  259. package/dist/internal/funding/providers/relay.js.map +1 -1
  260. package/dist/internal/funding/providers/stargate.d.ts +127 -9
  261. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  262. package/dist/internal/funding/providers/stargate.js +548 -112
  263. package/dist/internal/funding/providers/stargate.js.map +1 -1
  264. package/dist/internal/index.d.ts +1 -0
  265. package/dist/internal/index.d.ts.map +1 -1
  266. package/dist/internal/index.js +1 -0
  267. package/dist/internal/index.js.map +1 -1
  268. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  269. package/dist/internal/webhookDestinations/slack.js +16 -0
  270. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  271. package/package.json +2 -2
  272. package/src/App.test.ts +100 -0
  273. package/src/App.ts +43 -3
  274. package/src/Client.test-d.ts +129 -2
  275. package/src/Scope.test.ts +20 -0
  276. package/src/Scope.ts +16 -0
  277. package/src/Webhooks.ts +2 -2
  278. package/src/admin/apps/api-keys.test.ts +75 -0
  279. package/src/admin/apps/api-keys.ts +64 -0
  280. package/src/admin/apps/earn-vaults.test.ts +1 -1
  281. package/src/admin/apps/earn-vaults.ts +2 -1
  282. package/src/admin/apps/organizations.test.ts +53 -1
  283. package/src/admin/apps/organizations.ts +90 -5
  284. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  285. package/src/admin/ui/src/lib/organizations.test.ts +112 -1
  286. package/src/admin/ui/src/lib/organizations.ts +55 -0
  287. package/src/admin/ui/src/routes/api-keys.tsx +89 -2
  288. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
  289. package/src/admin/ui.generated.ts +189 -151
  290. package/src/apps/Zones.test.ts +7 -7
  291. package/src/apps/Zones.ts +7 -7
  292. package/src/apps/composition.test.ts +2 -1
  293. package/src/apps/data/App.test.ts +41 -0
  294. package/src/apps/data/App.ts +30 -21
  295. package/src/apps/data/ZoneSelection.test.ts +2 -2
  296. package/src/apps/data/ZoneSelection.ts +8 -8
  297. package/src/apps/data/routes/activities.test.ts +1526 -12
  298. package/src/apps/data/routes/activities.ts +1774 -1356
  299. package/src/apps/data/routes/balances.ts +133 -97
  300. package/src/apps/data/routes/blocks.test.ts +533 -0
  301. package/src/apps/data/routes/blocks.ts +35 -16
  302. package/src/apps/data/routes/earn.test.ts +373 -16
  303. package/src/apps/data/routes/earn.ts +548 -79
  304. package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
  305. package/src/apps/data/routes/exchanges.test.ts +740 -15
  306. package/src/apps/data/routes/exchanges.ts +954 -753
  307. package/src/apps/data/routes/fee-amm.ts +17 -18
  308. package/src/apps/data/routes/receipts.ts +47 -16
  309. package/src/apps/data/routes/tokens.test.ts +578 -0
  310. package/src/apps/data/routes/tokens.ts +283 -225
  311. package/src/apps/data/routes/transactions.ts +68 -29
  312. package/src/apps/data/routes/transfers.test.ts +421 -0
  313. package/src/apps/data/routes/transfers.ts +45 -27
  314. package/src/apps/data/routes/valuation.ts +94 -78
  315. package/src/apps/data/routes/webhooks.test.ts +21 -5
  316. package/src/apps/data/routes/webhooks.ts +63 -8
  317. package/src/apps/data/routes/zones.test.ts +2 -2
  318. package/src/apps/data/routes/zones.ts +1 -1
  319. package/src/apps/funding/App.ts +12 -2
  320. package/src/apps/funding/routes/deposit-addresses.test.ts +29 -7
  321. package/src/apps/funding/routes/deposit-addresses.ts +19 -14
  322. package/src/apps/funding/routes/quotes.test.ts +5 -9
  323. package/src/apps/funding/routes/transfers.test.ts +901 -23
  324. package/src/apps/funding/routes/transfers.ts +347 -64
  325. package/src/apps/management/Billing.ts +18 -5
  326. package/src/apps/management/routes/api-keys.test.ts +220 -0
  327. package/src/apps/management/routes/api-keys.ts +280 -3
  328. package/src/apps/management/routes/webhooks.test.ts +3 -3
  329. package/src/apps/management/routes/webhooks.ts +24 -25
  330. package/src/apps/relay/App.test.ts +212 -1
  331. package/src/apps/relay/App.ts +158 -29
  332. package/src/apps/relay/Sponsorships.ts +24 -1
  333. package/src/db/Db.ts +48 -1
  334. package/src/db/Schema.ts +22 -4
  335. package/src/db/tables/earnVaults.test.ts +9 -9
  336. package/src/db/tables/fundingCatalog.test.ts +16 -2
  337. package/src/db/tables/fundingCatalog.ts +9 -3
  338. package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
  339. package/src/db/tables/fundingTransferTransactions.ts +79 -3
  340. package/src/db/tables/fundingTransfers.test.ts +26 -0
  341. package/src/db/tables/fundingTransfers.ts +36 -0
  342. package/src/db/tables/organizations.test.ts +41 -0
  343. package/src/db/tables/organizations.ts +49 -0
  344. package/src/db/tables/projects.test.ts +55 -0
  345. package/src/db/tables/projects.ts +46 -1
  346. package/src/db/tables/sponsoredTransactions.test.ts +338 -0
  347. package/src/db/tables/sponsoredTransactions.ts +197 -13
  348. package/src/db/tables/webhookSubscriptions.ts +18 -11
  349. package/src/handlers/internal/utils.test.ts +53 -0
  350. package/src/handlers/internal/utils.ts +15 -0
  351. package/src/internal/Auth.ts +12 -4
  352. package/src/internal/EarnVaults.capabilities.test.ts +2 -2
  353. package/src/internal/EarnVaults.test.ts +1 -1
  354. package/src/internal/EarnVaults.ts +5 -5
  355. package/src/internal/Fetch.test.ts +46 -0
  356. package/src/internal/Fetch.ts +56 -0
  357. package/src/internal/Log.test-d.ts +2 -0
  358. package/src/internal/Log.test.ts +21 -0
  359. package/src/internal/Log.ts +22 -5
  360. package/src/internal/OpenApi.ts +14 -4
  361. package/src/internal/Response.ts +1 -1
  362. package/src/internal/Schema.test.ts +2 -0
  363. package/src/internal/Schema.ts +12 -2
  364. package/src/internal/Store.test.ts +178 -0
  365. package/src/internal/Store.ts +104 -18
  366. package/src/internal/Tidx.test.ts +2 -1
  367. package/src/internal/Tidx.ts +7 -2
  368. package/src/internal/Timeout.test.ts +35 -0
  369. package/src/internal/Timeout.ts +73 -0
  370. package/src/internal/Timing.ts +16 -0
  371. package/src/internal/Viem.ts +39 -3
  372. package/src/internal/WebhookDestination.ts +3 -0
  373. package/src/internal/Webhooks.test.ts +45 -0
  374. package/src/internal/funding/Action.test.ts +41 -1
  375. package/src/internal/funding/Action.ts +69 -41
  376. package/src/internal/funding/Catalog.test.ts +34 -0
  377. package/src/internal/funding/Catalog.ts +28 -11
  378. package/src/internal/funding/DepositWebhook.ts +2 -1
  379. package/src/internal/funding/Evidence.ts +2 -13
  380. package/src/internal/funding/Provider.test.ts +73 -0
  381. package/src/internal/funding/Provider.ts +125 -28
  382. package/src/internal/funding/SourceObservation.ts +5 -9
  383. package/src/internal/funding/Transfer.test.ts +155 -0
  384. package/src/internal/funding/Transfer.ts +387 -147
  385. package/src/internal/funding/TransferReconciliation.test.ts +147 -0
  386. package/src/internal/funding/TransferReconciliation.ts +264 -0
  387. package/src/internal/funding/TransferWebhook.test.ts +128 -0
  388. package/src/internal/funding/TransferWebhook.ts +67 -0
  389. package/src/internal/funding/index.test.ts +1 -0
  390. package/src/internal/funding/index.ts +1 -0
  391. package/src/internal/funding/providers/index.test.ts +4 -167
  392. package/src/internal/funding/providers/relay.test.ts +2 -2
  393. package/src/internal/funding/providers/relay.ts +3 -15
  394. package/src/internal/funding/providers/stargate.test.ts +328 -0
  395. package/src/internal/funding/providers/stargate.ts +807 -135
  396. package/src/internal/index.test.ts +1 -0
  397. package/src/internal/index.ts +1 -0
  398. package/src/internal/webhookDestinations/slack.ts +15 -0
@@ -4,34 +4,42 @@ import { base } from 'viem/chains'
4
4
  import { tempoMainnet } from 'viem/tempo/chains'
5
5
 
6
6
  import * as Anvil from '../../../../test/Anvil.js'
7
+ import * as TestApp from '../../../../test/App.js'
8
+ import * as TestFunding from '../../../../test/Funding.js'
7
9
  import * as FundingIdempotencyTable from '../../../db/tables/fundingIdempotency.js'
8
-
9
10
  import * as Cache from '../../../internal/Cache.js'
10
-
11
+ import * as FundingCatalog from '../../../internal/funding/Catalog.js'
12
+ import * as FundingChain from '../../../internal/funding/Chain.js'
11
13
  import * as FundingProvider from '../../../internal/funding/Provider.js'
12
-
13
14
  import * as FundingTransfer from '../../../internal/funding/Transfer.js'
14
-
15
+ import * as FundingTransferReconciliation from '../../../internal/funding/TransferReconciliation.js'
15
16
  import * as Idempotency from '../../../internal/Idempotency.js'
16
-
17
17
  import * as Relay from '../../../internal/funding/providers/relay.js'
18
-
19
- import * as TestApp from '../../../../test/App.js'
20
-
21
- import * as TestFunding from '../../../../test/Funding.js'
22
-
18
+ import * as Stargate from '../../../internal/funding/providers/stargate.js'
19
+ import * as Webhooks from '../../../internal/Webhooks.js'
23
20
  import * as Funding from './transfers.js'
24
21
 
25
22
  const db = TestApp.database()
23
+ const destinationTransactionHash =
24
+ '0x3dfaba0a78b8f4bc09e55e05211c8120729e7b6879efe32613883befbca666da'
26
25
 
27
26
  /** Providers exercised against executable calls on a forked source chain. */
28
27
  const executableProviders = [
29
28
  {
30
29
  create: (_sender: string) =>
31
30
  Relay.relay(process.env.RELAY_API_KEY ? { apiKey: process.env.RELAY_API_KEY } : {}),
31
+ destinationToken: 'pathusd',
32
32
  id: 'relay',
33
33
  },
34
+ {
35
+ create: (_sender: string) => Stargate.stargate(),
36
+ destinationToken: 'usdce',
37
+ id: 'stargate',
38
+ },
34
39
  ] as const
40
+ const stargateConfiguration = TestFunding.snapshot.routesByProvider
41
+ .get('stargate')!
42
+ .find((candidate) => candidate.route.source.chain.id === 'eip155:8453')!.configuration!
35
43
 
36
44
  beforeAll(() => TestFunding.publish(db))
37
45
 
@@ -39,6 +47,11 @@ function create(options: TestApp.create.Options = {}) {
39
47
  return TestApp.create({ ...options, db })
40
48
  }
41
49
 
50
+ type ContractResponse = {
51
+ $ref?: string | undefined
52
+ content?: Record<string, { schema: unknown }> | undefined
53
+ }
54
+
42
55
  type JsonSchema = {
43
56
  $ref?: string | undefined
44
57
  additionalProperties?: boolean | undefined
@@ -211,6 +224,54 @@ describe('GET /funding/transfers/:id', () => {
211
224
  })
212
225
  })
213
226
 
227
+ describe('POST /funding/transfers/:id/source-transactions', () => {
228
+ test('requires funding write access and validates the transaction hash', async () => {
229
+ const app = transfersApp()
230
+ const transfer = await seedTransfer()
231
+ const path = `/v1/funding/transfers/${transfer.id}/source-transactions`
232
+ expect((await app.request(path, { method: 'POST' })).status).toBe(401)
233
+ expect((await app.request(path, { ...as(fundingReader), method: 'POST' })).status).toBe(403)
234
+ expect(
235
+ (
236
+ await app.request(path, {
237
+ body: JSON.stringify({ transactionHash: '0xinvalid' }),
238
+ headers: { ...as(fundingWriter).headers, 'content-type': 'application/json' },
239
+ method: 'POST',
240
+ })
241
+ ).status,
242
+ ).toBe(400)
243
+ })
244
+
245
+ test('reports when transfer reconciliation is not configured', async () => {
246
+ const app = transfersApp()
247
+ const transfer = await seedTransfer()
248
+ const response = await app.request(`/v1/funding/transfers/${transfer.id}/source-transactions`, {
249
+ body: JSON.stringify({ transactionHash: `0x${'aa'.repeat(32)}` }),
250
+ headers: { ...as(fundingWriter).headers, 'content-type': 'application/json' },
251
+ method: 'POST',
252
+ })
253
+ expect(response.status).toBe(501)
254
+ })
255
+
256
+ test('does not expose a transfer owned by another organization', async () => {
257
+ const tracker = FundingTransferReconciliation.createTracker({ db })
258
+ const app = transfersApp({
259
+ funding: {
260
+ async dispatchTransferReconciliation(message) {
261
+ await tracker.reconcile(message)
262
+ },
263
+ },
264
+ })
265
+ const transfer = await seedTransfer()
266
+ const response = await app.request(`/v1/funding/transfers/${transfer.id}/source-transactions`, {
267
+ body: JSON.stringify({ transactionHash: `0x${'aa'.repeat(32)}` }),
268
+ headers: { ...as(fundingForeign).headers, 'content-type': 'application/json' },
269
+ method: 'POST',
270
+ })
271
+ expect(response.status).toBe(404)
272
+ })
273
+ })
274
+
214
275
  const createBody = {
215
276
  amount: '1000000',
216
277
  destinationToken: '0x20c0000000000000000000000000000000000000',
@@ -377,6 +438,103 @@ describe('POST /funding/transfers', () => {
377
438
  expect(row.providerState).toEqual({ requestId: `0x${'ab'.repeat(32)}` })
378
439
  })
379
440
 
441
+ test('dispatches staged webhook updates after creation commits', async () => {
442
+ const subscription = await Webhooks.createSubscription(db, {
443
+ chainId: tempoMainnet.id,
444
+ destination: { type: 'url', url: 'https://example.com/webhooks/funding-transfers' },
445
+ environment: 'production',
446
+ eventType: 'funding:transfer.updated',
447
+ filters: { status: 'awaiting-source' },
448
+ owner: { orgId: fundingWriter.orgId, type: 'api_key' },
449
+ })
450
+ const references: Webhooks.QueueReference[] = []
451
+ const app = transfersApp({
452
+ funding: {
453
+ async dispatchTransferUpdates(staged) {
454
+ const reference = staged[0]!
455
+ const queued = await db.kysely
456
+ .selectFrom('webhook_queue_events')
457
+ .select('eventId')
458
+ .where('eventId', '=', reference.eventId)
459
+ .where('subscriptionId', '=', reference.subscriptionId)
460
+ .executeTakeFirst()
461
+ expect(queued).toBeDefined()
462
+ references.push(...staged)
463
+ return staged.length
464
+ },
465
+ },
466
+ providers: [provider()],
467
+ })
468
+
469
+ const response = await postTransfer(app, { idempotencyKey: nextIdempotencyKey() })
470
+
471
+ expect(response.status).toBe(200)
472
+ expect(references).toEqual([{ eventId: expect.any(String), subscriptionId: subscription.id }])
473
+ })
474
+
475
+ test('creates a transfer with the requested provider', async () => {
476
+ const app = transfersApp({ providers: [provider()] })
477
+ const response = await postTransfer(app, {
478
+ body: { ...createBody, provider: 'relay' },
479
+ idempotencyKey: nextIdempotencyKey(),
480
+ })
481
+
482
+ expect(response.status).toBe(200)
483
+ const body = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
484
+ expect(body.provider.id).toBe('relay')
485
+ })
486
+
487
+ test('rejects an unsupported provider', async () => {
488
+ const app = transfersApp({ providers: [provider()] })
489
+ const response = await postTransfer(app, {
490
+ body: { ...createBody, provider: 'bridge' },
491
+ idempotencyKey: nextIdempotencyKey(),
492
+ })
493
+
494
+ expect(response.status).toBe(400)
495
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe('body_invalid')
496
+ })
497
+
498
+ test('preserves a provider-specified native fee token', async () => {
499
+ const app = transfersApp({
500
+ providers: [
501
+ provider(async () => ({
502
+ ...providerResponse(),
503
+ fees: [
504
+ {
505
+ amount: '1000000000000000',
506
+ side: 'source',
507
+ token: {
508
+ address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
509
+ currency: 'ETH',
510
+ decimals: 18,
511
+ name: 'Ether',
512
+ standard: 'native',
513
+ symbol: 'ETH',
514
+ tokenKey: 'eip155:8453/slip44:60',
515
+ verified: true,
516
+ },
517
+ },
518
+ ],
519
+ })),
520
+ ],
521
+ })
522
+ const response = await postTransfer(app, {
523
+ body: { ...createBody, destinationToken: 'pathusd', sourceToken: 'usdc' },
524
+ idempotencyKey: nextIdempotencyKey(),
525
+ })
526
+ expect(response.status).toBe(200)
527
+ const body = await TestApp.json(response, Funding.schema.createFundingTransfer.Response)
528
+
529
+ expect(body.fees).toMatchObject([
530
+ {
531
+ amount: { baseUnits: '1000000000000000', formatted: '0.001' },
532
+ side: 'source',
533
+ token: { standard: 'native', symbol: 'ETH' },
534
+ },
535
+ ])
536
+ })
537
+
380
538
  test('resolves token identifiers through the composed route', async () => {
381
539
  const app = transfersApp({ providers: [provider()] })
382
540
  const identifiers = [
@@ -608,7 +766,7 @@ describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', (
608
766
  (entry) =>
609
767
  entry.route.source.chain.id === `eip155:${base.id}` &&
610
768
  entry.route.source.symbol === 'USDC' &&
611
- entry.route.destination.slug === 'pathusd',
769
+ entry.route.destination.slug === executable.destinationToken,
612
770
  )!
613
771
  const usdc = route.route.source
614
772
  const amount = Value.from('10', usdc.decimals)
@@ -627,7 +785,12 @@ describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', (
627
785
  function createTransfer(idempotencyKey: string) {
628
786
  const app = transfersApp({ providers: [executable.create(sender.address)] })
629
787
  return postTransfer(app, {
630
- body: { ...createBody, amount: amount.toString(), sender: sender.address },
788
+ body: {
789
+ ...createBody,
790
+ amount: amount.toString(),
791
+ destinationToken: executable.destinationToken,
792
+ sender: sender.address,
793
+ },
631
794
  idempotencyKey,
632
795
  })
633
796
  }
@@ -647,6 +810,24 @@ describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', (
647
810
  expect(transfer.action.type).toBe('evm:calls')
648
811
  if (transfer.action.type !== 'evm:calls') return
649
812
  expect(transfer.action.calls.length).toBeGreaterThan(0)
813
+ if (executable.id === 'stargate') {
814
+ expect(transfer.fees).toMatchObject([
815
+ {
816
+ amount: { baseUnits: BigInt(transfer.action.calls.at(-1)!.value).toString() },
817
+ side: 'source',
818
+ token: { standard: 'native', symbol: 'ETH' },
819
+ },
820
+ ])
821
+ const row = await db.kysely
822
+ .selectFrom('funding_transfers')
823
+ .select('providerState')
824
+ .where('id', '=', transfer.id)
825
+ .executeTakeFirstOrThrow()
826
+ expect(row.providerState).toEqual({
827
+ destinationBlockNumber: expect.stringMatching(/^\d+$/),
828
+ routeConfiguration: stargateConfiguration,
829
+ })
830
+ }
650
831
 
651
832
  const receipts = []
652
833
  for (const call of transfer.action.calls) {
@@ -665,8 +846,158 @@ describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', (
665
846
  )
666
847
  // The deposit is the last call and must emit; an approval alone proves nothing.
667
848
  expect(receipts.at(-1)!.logs.length).toBeGreaterThan(0)
849
+ if (executable.id === 'stargate') {
850
+ const receipt = receipts.at(-1)!
851
+ const finalized = await client.getBlock({ blockTag: 'finalized' })
852
+ expect(receipt.blockNumber).toBeGreaterThan(finalized.number)
853
+ await expect(
854
+ Stargate.verifySourceTransaction({
855
+ configuration: stargateConfiguration,
856
+ destinationTokenAddress: transfer.destinationToken.address,
857
+ recipient: transfer.recipient,
858
+ sender: sender.address,
859
+ sourceAmount: transfer.sourceAmount.baseUnits,
860
+ sourceChain: FundingChain.from({
861
+ id: 'eip155:8453',
862
+ name: 'Base',
863
+ rpcUrls: [anvil.rpcUrl],
864
+ slug: 'base',
865
+ }),
866
+ sourceTokenAddress: transfer.sourceToken.address,
867
+ transactionHash: receipt.transactionHash,
868
+ validAfter: transfer.quote.sampledAt,
869
+ }),
870
+ ).resolves.toMatchObject({ type: 'verified' })
871
+ }
668
872
  })
669
873
 
874
+ if (executable.id === 'stargate')
875
+ test('reopens registration when the source transaction is reorged out', async () => {
876
+ await Anvil.setErc20Balance(anvil.client, {
877
+ address: sender.address,
878
+ amount,
879
+ slot: fiatTokenBalanceSlot,
880
+ token: usdc.address as `0x${string}`,
881
+ })
882
+ const response = await createTransfer(`bridge_reorg_${sender.address}`)
883
+ expect(response.status).toBe(200)
884
+ const transfer = await TestApp.json(
885
+ response,
886
+ Funding.schema.createFundingTransfer.Response,
887
+ )
888
+ expect(transfer.action.type).toBe('evm:calls')
889
+ if (transfer.action.type !== 'evm:calls') return
890
+
891
+ const checkpoint = await anvil.client.snapshot()
892
+ // Anvil increments fork timestamps instead of following wall time between tests.
893
+ const block = await anvil.client.getBlock()
894
+ const sampledAt = BigInt(Math.floor(Date.parse(transfer.quote.sampledAt) / 1_000))
895
+ if (block.timestamp < sampledAt)
896
+ await anvil.client.setNextBlockTimestamp({ timestamp: sampledAt })
897
+ let reverted = false
898
+ try {
899
+ const receipts = []
900
+ for (const call of transfer.action.calls)
901
+ receipts.push(
902
+ await anvil.client.sendTransactionSync({
903
+ account: sender,
904
+ data: call.data as `0x${string}`,
905
+ to: call.to as `0x${string}`,
906
+ value: BigInt(call.value),
907
+ }),
908
+ )
909
+ const sourceReceipt = receipts.at(-1)!
910
+ const transactionHash = sourceReceipt.transactionHash
911
+ await FundingCatalog.publish(db, {
912
+ ...TestFunding.data,
913
+ chains: TestFunding.data.chains.map((chain) =>
914
+ chain.id === 'eip155:8453' ? { ...chain, rpcUrls: [anvil.rpcUrl] } : chain,
915
+ ),
916
+ })
917
+ const messages: FundingTransferReconciliation.Message[] = []
918
+ const app = transfersApp({
919
+ funding: {
920
+ async dispatchTransferReconciliation(message) {
921
+ messages.push(message)
922
+ },
923
+ },
924
+ providers: [Stargate.stargate()],
925
+ })
926
+ const registered = await app.request(
927
+ `/v1/funding/transfers/${transfer.id}/source-transactions`,
928
+ {
929
+ body: JSON.stringify({ transactionHash }),
930
+ headers: { ...as(fundingWriter).headers, 'content-type': 'application/json' },
931
+ method: 'POST',
932
+ },
933
+ )
934
+ expect(registered.status).toBe(200)
935
+ expect(messages).toEqual([
936
+ { transferId: transfer.id, type: 'funding:transfer:reconcile' },
937
+ ])
938
+
939
+ await anvil.client.revert({ id: checkpoint })
940
+ reverted = true
941
+ const tracker = FundingTransferReconciliation.createTracker({
942
+ db,
943
+ async dispatch(message) {
944
+ messages.push(message)
945
+ },
946
+ })
947
+ await expect(tracker.reconcile(messages[0]!)).resolves.toEqual({ type: 'pending' })
948
+ expect(messages[1]).toEqual({
949
+ attempt: 1,
950
+ transferId: transfer.id,
951
+ type: 'funding:transfer:reconcile',
952
+ })
953
+
954
+ const pending = await db.kysely
955
+ .selectFrom('funding_transfers')
956
+ .select(['snapshot', 'status', 'version'])
957
+ .where('id', '=', transfer.id)
958
+ .executeTakeFirstOrThrow()
959
+ expect({
960
+ sourceTransactionHashes: pending.snapshot.sourceTransactionHashes,
961
+ status: pending.status,
962
+ version: pending.version,
963
+ }).toEqual({
964
+ sourceTransactionHashes: [transactionHash],
965
+ status: 'processing',
966
+ version: 2,
967
+ })
968
+
969
+ await anvil.client.mine({ blocks: 128 })
970
+ const finalized = await anvil.client.getBlock({ blockTag: 'finalized' })
971
+ expect(finalized.number).toBeGreaterThanOrEqual(sourceReceipt.blockNumber)
972
+ await expect(tracker.reconcile(messages[1]!)).resolves.toEqual({ type: 'reopened' })
973
+
974
+ const record = await db.kysely
975
+ .selectFrom('funding_transfers')
976
+ .select(['snapshot', 'status', 'version'])
977
+ .where('id', '=', transfer.id)
978
+ .executeTakeFirstOrThrow()
979
+ expect({
980
+ sourceTransactionHashes: record.snapshot.sourceTransactionHashes,
981
+ status: record.status,
982
+ version: record.version,
983
+ }).toEqual({
984
+ sourceTransactionHashes: undefined,
985
+ status: 'awaiting-source',
986
+ version: 3,
987
+ })
988
+ expect(
989
+ await db.kysely
990
+ .selectFrom('funding_transfer_transactions')
991
+ .selectAll()
992
+ .where('transferId', '=', transfer.id)
993
+ .execute(),
994
+ ).toEqual([])
995
+ } finally {
996
+ if (!reverted) await anvil.client.revert({ id: checkpoint })
997
+ await TestFunding.publish(db)
998
+ }
999
+ })
1000
+
670
1001
  test('bounds the approval to the quoted source spend', async () => {
671
1002
  const response = await createTransfer(`bridge_bounds_${executable.id}_${sender.address}`)
672
1003
  expect(response.status).toBe(200)
@@ -692,6 +1023,195 @@ describe.skipIf(!Anvil.available)('POST /funding/transfers (forked localnet)', (
692
1023
  }
693
1024
  })
694
1025
 
1026
+ describe.skipIf(!Anvil.available)(
1027
+ 'POST /funding/transfers/:id/source-transactions (forked localnet)',
1028
+ () => {
1029
+ const trackingDb = TestApp.database()
1030
+ let destination: Anvil.start.ReturnType
1031
+ let source: Anvil.start.ReturnType
1032
+
1033
+ beforeAll(async () => {
1034
+ ;[destination, source] = await Promise.all([
1035
+ Anvil.start({ chain: tempoMainnet, forkUrl: tempoMainnet.rpcUrls.default.http[0]! }),
1036
+ Anvil.start({ chain: base, forkUrl: base.rpcUrls.default.http[0]! }),
1037
+ ])
1038
+ await FundingCatalog.publish(trackingDb, {
1039
+ ...TestFunding.data,
1040
+ chains: TestFunding.data.chains.map((chain) => {
1041
+ if (chain.id === 'eip155:4217') return { ...chain, rpcUrls: [destination.rpcUrl] }
1042
+ if (chain.id === 'eip155:8453') return { ...chain, rpcUrls: [source.rpcUrl] }
1043
+ return chain
1044
+ }),
1045
+ })
1046
+ })
1047
+
1048
+ afterAll(async () => {
1049
+ await Promise.all([destination?.stop(), source?.stop()])
1050
+ })
1051
+
1052
+ test('backfills legacy state and completes from destination evidence', async () => {
1053
+ const destinationReceipt = await destination.client.getTransactionReceipt({
1054
+ hash: destinationTransactionHash,
1055
+ })
1056
+ const sourceTransactionHash =
1057
+ '0xfd71359adf5095f2ab512a19e0a2df6c8cc0db14ac8a52286d61287daabdccd4'
1058
+ const sourceReceipt = await source.client.getTransactionReceipt({
1059
+ hash: sourceTransactionHash,
1060
+ })
1061
+ const sourceBlock = await source.client.getBlock({ blockNumber: sourceReceipt.blockNumber })
1062
+ const sampledAt = new Date(Number(sourceBlock.timestamp - 1n) * 1_000).toISOString()
1063
+ const amount = {
1064
+ baseUnits: '200000',
1065
+ currency: 'USD',
1066
+ decimals: 6,
1067
+ formatted: '0.2',
1068
+ } as const
1069
+ const snapshot = {
1070
+ ...TestFunding.transferSnapshot({
1071
+ destinationAmount: amount,
1072
+ destinationAmountMin: amount,
1073
+ provider: { id: 'stargate', name: 'Stargate' },
1074
+ recipient: '0x7212623e4cff9ad010d28aa065fad2bf58578783',
1075
+ sender: '0x52b650a62e384f68880d0a1dc2b84dfed47d5660',
1076
+ sourceAmount: amount,
1077
+ }),
1078
+ quote: {
1079
+ expiresAt: new Date(Date.parse(sampledAt) + 60_000).toISOString(),
1080
+ sampledAt,
1081
+ },
1082
+ }
1083
+ const transfer = await FundingTransfer.create(trackingDb, {
1084
+ apiKeyId: fundingWriter.id,
1085
+ environment: 'production',
1086
+ orgId: fundingWriter.orgId,
1087
+ snapshot,
1088
+ })
1089
+ const tracker = FundingTransferReconciliation.createTracker({ db: trackingDb })
1090
+ const messages: FundingTransferReconciliation.Message[] = []
1091
+ let dispatchEnabled = true
1092
+ const app = TestApp.create({
1093
+ auth: { keys: [fundingWriter] },
1094
+ db: trackingDb,
1095
+ defaultChainId: tempoMainnet.id,
1096
+ funding: {
1097
+ async dispatchTransferReconciliation(message) {
1098
+ if (!dispatchEnabled) throw new Error('reconciliation unavailable')
1099
+ messages.push(message)
1100
+ },
1101
+ },
1102
+ providers: [Stargate.stargate()],
1103
+ })
1104
+ const path = `/v1/funding/transfers/${transfer.id}/source-transactions`
1105
+ const registration = {
1106
+ body: JSON.stringify({
1107
+ transactionHash: sourceTransactionHash,
1108
+ }),
1109
+ headers: { ...as(fundingWriter).headers, 'content-type': 'application/json' },
1110
+ method: 'POST',
1111
+ }
1112
+ const response = await app.request(path, registration)
1113
+ expect(response.status).toBe(200)
1114
+ const registered = await TestApp.json(
1115
+ response,
1116
+ Funding.schema.registerFundingTransferSourceTransaction.Response,
1117
+ )
1118
+ expect({
1119
+ sourceTransactionHashes: registered.sourceTransactionHashes,
1120
+ status: registered.status,
1121
+ version: registered.version,
1122
+ }).toEqual({
1123
+ sourceTransactionHashes: [
1124
+ '0xfd71359adf5095f2ab512a19e0a2df6c8cc0db14ac8a52286d61287daabdccd4',
1125
+ ],
1126
+ status: 'processing',
1127
+ version: 2,
1128
+ })
1129
+ const stored = await trackingDb.kysely
1130
+ .selectFrom('funding_transfers')
1131
+ .select('providerState')
1132
+ .where('id', '=', transfer.id)
1133
+ .executeTakeFirstOrThrow()
1134
+ expect(stored).toEqual({
1135
+ providerState: {
1136
+ destinationBlockNumber: expect.stringMatching(/^\d+$/),
1137
+ routeConfiguration: stargateConfiguration,
1138
+ stargate: {
1139
+ guid: '0x5549d117c8b23f0ecdf05c2604ed989cf0034f378707bd81319e93834f2f85f2',
1140
+ sourceBlock: {
1141
+ hash: sourceReceipt.blockHash,
1142
+ number: sourceReceipt.blockNumber.toString(),
1143
+ },
1144
+ sourceEid: 30_184,
1145
+ },
1146
+ },
1147
+ })
1148
+ const destinationBlockNumber = stored.providerState?.['destinationBlockNumber']
1149
+ if (typeof destinationBlockNumber !== 'string')
1150
+ throw new Error('Expected a backfilled destination block number.')
1151
+ expect(BigInt(destinationBlockNumber)).toBeLessThanOrEqual(destinationReceipt.blockNumber)
1152
+ expect(messages).toEqual([{ transferId: transfer.id, type: 'funding:transfer:reconcile' }])
1153
+ await tracker.reconcile(messages[0]!)
1154
+
1155
+ const completed = await TestApp.json(
1156
+ await app.request(`/v1/funding/transfers/${transfer.id}`, as(fundingWriter)),
1157
+ Funding.schema.getFundingTransfer.Response,
1158
+ )
1159
+ expect({
1160
+ destinationTransactionHashes: completed.destinationTransactionHashes,
1161
+ status: completed.status,
1162
+ version: completed.version,
1163
+ }).toEqual({
1164
+ destinationTransactionHashes: [destinationTransactionHash],
1165
+ status: 'completed',
1166
+ version: 3,
1167
+ })
1168
+
1169
+ dispatchEnabled = false
1170
+ const replayed = await TestApp.json(
1171
+ await app.request(path, registration),
1172
+ Funding.schema.registerFundingTransferSourceTransaction.Response,
1173
+ )
1174
+ expect({ status: replayed.status, version: replayed.version }).toEqual({
1175
+ status: 'completed',
1176
+ version: 3,
1177
+ })
1178
+
1179
+ const other = await FundingTransfer.create(trackingDb, {
1180
+ apiKeyId: fundingWriter.id,
1181
+ environment: 'production',
1182
+ orgId: fundingWriter.orgId,
1183
+ snapshot,
1184
+ })
1185
+ const conflict = await app.request(
1186
+ `/v1/funding/transfers/${other.id}/source-transactions`,
1187
+ registration,
1188
+ )
1189
+ expect(conflict.status).toBe(409)
1190
+ expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe(
1191
+ 'source_transaction_conflict',
1192
+ )
1193
+
1194
+ const pending = await FundingTransfer.create(trackingDb, {
1195
+ apiKeyId: fundingWriter.id,
1196
+ environment: 'production',
1197
+ orgId: fundingWriter.orgId,
1198
+ snapshot,
1199
+ })
1200
+ const pendingResponse = await app.request(
1201
+ `/v1/funding/transfers/${pending.id}/source-transactions`,
1202
+ {
1203
+ ...registration,
1204
+ body: JSON.stringify({ transactionHash: `0x${'99'.repeat(32)}` }),
1205
+ },
1206
+ )
1207
+ expect(pendingResponse.status).toBe(409)
1208
+ expect(((await pendingResponse.json()) as { error: { code: string } }).error.code).toBe(
1209
+ 'source_transaction_pending',
1210
+ )
1211
+ })
1212
+ },
1213
+ )
1214
+
695
1215
  describe('POST /funding/transfers/{destination}', () => {
696
1216
  test.each([
697
1217
  { body: vaultTransferBody, destination: 'vault' },
@@ -756,24 +1276,360 @@ describe('POST /funding/transfers/{destination}', () => {
756
1276
  })
757
1277
 
758
1278
  describe('funding transfers OpenAPI', () => {
1279
+ test('publishes generator-ready transfer contracts', async () => {
1280
+ const spec = await (await create({ auth: false }).request('/openapi.json')).json()
1281
+ const detailPath = Object.keys(spec.paths).find(
1282
+ (path) => path.includes('/v1/funding/transfers/{') && !path.endsWith('/source-transactions'),
1283
+ )!
1284
+ const sourceTransactionPath = Object.keys(spec.paths).find((path) =>
1285
+ path.endsWith('/source-transactions'),
1286
+ )!
1287
+ const operations = {
1288
+ create: spec.paths['/v1/funding/transfers'].post,
1289
+ createVault: spec.paths['/v1/funding/transfers/vault'].post,
1290
+ createZone: spec.paths['/v1/funding/transfers/zone'].post,
1291
+ get: spec.paths[detailPath].get,
1292
+ list: spec.paths['/v1/funding/transfers'].get,
1293
+ registerSourceTransaction: spec.paths[sourceTransactionPath].post,
1294
+ }
1295
+ const summarize = (operation: (typeof operations)[keyof typeof operations]) => ({
1296
+ errors: Object.fromEntries(
1297
+ (Object.entries(operation.responses) as [string, ContractResponse][])
1298
+ .filter(([status]) => Number(status) >= 400)
1299
+ .map(([status, response]) => [
1300
+ status,
1301
+ response.$ref ?? response.content?.['application/json']?.schema,
1302
+ ]),
1303
+ ),
1304
+ operationId: operation.operationId,
1305
+ request: operation.requestBody?.content['application/json'].schema,
1306
+ response: operation.responses[200]?.content?.['application/json']?.schema,
1307
+ })
1308
+
1309
+ expect({
1310
+ components: [
1311
+ 'CreateFundingTransferRequest',
1312
+ 'CreateFundingTransferVaultRequest',
1313
+ 'CreateFundingTransferZoneRequest',
1314
+ 'CreatedFundingTransfer',
1315
+ 'FundingAction',
1316
+ 'FundingActionCall',
1317
+ 'FundingEvmCallsAction',
1318
+ 'FundingTransfer',
1319
+ 'FundingTransferFee',
1320
+ 'FundingTransferList',
1321
+ 'FundingTransferQuote',
1322
+ 'FundingTransferStatusReason',
1323
+ 'RegisterFundingTransferSourceTransactionRequest',
1324
+ ].filter((name) => spec.components.schemas[name]),
1325
+ formats: {
1326
+ createdAt: spec.components.schemas.FundingTransfer.properties.createdAt.format,
1327
+ quoteExpiresAt: spec.components.schemas.FundingTransferQuote.properties.expiresAt.format,
1328
+ quoteSampledAt: spec.components.schemas.FundingTransferQuote.properties.sampledAt.format,
1329
+ updatedAt: spec.components.schemas.FundingTransfer.properties.updatedAt.format,
1330
+ },
1331
+ models: {
1332
+ action: spec.components.schemas.FundingAction.oneOf,
1333
+ createdAction: spec.components.schemas.CreatedFundingTransfer.properties.action,
1334
+ evmCall: spec.components.schemas.FundingEvmCallsAction.properties.calls.items,
1335
+ feeToken: spec.components.schemas.FundingTransferFee.properties.token,
1336
+ list: spec.components.schemas.FundingTransferList.properties.data.items,
1337
+ quote: spec.components.schemas.FundingTransfer.properties.quote,
1338
+ statusReason: spec.components.schemas.FundingTransfer.properties.statusReason,
1339
+ },
1340
+ operations: {
1341
+ create: summarize(operations.create),
1342
+ createVault: summarize(operations.createVault),
1343
+ createZone: summarize(operations.createZone),
1344
+ get: summarize(operations.get),
1345
+ list: summarize(operations.list),
1346
+ registerSourceTransaction: summarize(operations.registerSourceTransaction),
1347
+ },
1348
+ operationErrorCodes: {
1349
+ createInvalid:
1350
+ spec.components.schemas.ApiKeyMalformedOrBodyInvalidOrIdempotencyKeyRequiredError
1351
+ .properties.error.properties.code.enum,
1352
+ idempotencyConflict:
1353
+ spec.components.schemas.IdempotencyConflictOrIdempotencyInProgressError.properties.error
1354
+ .properties.code.enum,
1355
+ notImplemented:
1356
+ spec.components.schemas.NotImplemented501Error.properties.error.properties.code.enum,
1357
+ parametersInvalid:
1358
+ spec.components.schemas.ApiKeyMalformedOrParamInvalidError.properties.error.properties
1359
+ .code.enum,
1360
+ queryInvalid:
1361
+ spec.components.schemas.ApiKeyMalformedOrQueryInvalidError.properties.error.properties
1362
+ .code.enum,
1363
+ quoteNotAvailable:
1364
+ spec.components.schemas.QuoteNotAvailableError.properties.error.properties.code.enum,
1365
+ transferNotFound:
1366
+ spec.components.schemas.FundingTransferNotFoundError.properties.error.properties.code
1367
+ .enum,
1368
+ },
1369
+ }).toMatchInlineSnapshot(`
1370
+ {
1371
+ "components": [
1372
+ "CreateFundingTransferRequest",
1373
+ "CreateFundingTransferVaultRequest",
1374
+ "CreateFundingTransferZoneRequest",
1375
+ "CreatedFundingTransfer",
1376
+ "FundingAction",
1377
+ "FundingActionCall",
1378
+ "FundingEvmCallsAction",
1379
+ "FundingTransfer",
1380
+ "FundingTransferFee",
1381
+ "FundingTransferList",
1382
+ "FundingTransferQuote",
1383
+ "FundingTransferStatusReason",
1384
+ "RegisterFundingTransferSourceTransactionRequest",
1385
+ ],
1386
+ "formats": {
1387
+ "createdAt": "date-time",
1388
+ "quoteExpiresAt": "date-time",
1389
+ "quoteSampledAt": "date-time",
1390
+ "updatedAt": "date-time",
1391
+ },
1392
+ "models": {
1393
+ "action": [
1394
+ {
1395
+ "$ref": "#/components/schemas/FundingEvmCallsAction",
1396
+ },
1397
+ ],
1398
+ "createdAction": {
1399
+ "$ref": "#/components/schemas/FundingAction",
1400
+ },
1401
+ "evmCall": {
1402
+ "$ref": "#/components/schemas/FundingActionCall",
1403
+ },
1404
+ "feeToken": {
1405
+ "$ref": "#/components/schemas/FundingToken",
1406
+ },
1407
+ "list": {
1408
+ "$ref": "#/components/schemas/FundingTransfer",
1409
+ },
1410
+ "quote": {
1411
+ "$ref": "#/components/schemas/FundingTransferQuote",
1412
+ },
1413
+ "statusReason": {
1414
+ "$ref": "#/components/schemas/FundingTransferStatusReason",
1415
+ },
1416
+ },
1417
+ "operationErrorCodes": {
1418
+ "createInvalid": [
1419
+ "api_key_malformed",
1420
+ "body_invalid",
1421
+ "idempotency_key_required",
1422
+ ],
1423
+ "idempotencyConflict": [
1424
+ "idempotency_conflict",
1425
+ "idempotency_in_progress",
1426
+ ],
1427
+ "notImplemented": [
1428
+ "not_implemented",
1429
+ ],
1430
+ "parametersInvalid": [
1431
+ "api_key_malformed",
1432
+ "param_invalid",
1433
+ ],
1434
+ "queryInvalid": [
1435
+ "api_key_malformed",
1436
+ "query_invalid",
1437
+ ],
1438
+ "quoteNotAvailable": [
1439
+ "quote_not_available",
1440
+ ],
1441
+ "transferNotFound": [
1442
+ "funding_transfer_not_found",
1443
+ ],
1444
+ },
1445
+ "operations": {
1446
+ "create": {
1447
+ "errors": {
1448
+ "400": {
1449
+ "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidOrIdempotencyKeyRequiredError",
1450
+ },
1451
+ "401": {
1452
+ "$ref": "#/components/schemas/AuthenticationError",
1453
+ },
1454
+ "403": {
1455
+ "$ref": "#/components/schemas/ForbiddenError",
1456
+ },
1457
+ "404": {
1458
+ "$ref": "#/components/schemas/QuoteNotAvailableError",
1459
+ },
1460
+ "409": {
1461
+ "$ref": "#/components/schemas/IdempotencyConflictOrIdempotencyInProgressError",
1462
+ },
1463
+ "429": "#/components/responses/RateLimited",
1464
+ "500": "#/components/responses/InternalError",
1465
+ "502": {
1466
+ "$ref": "#/components/schemas/UpstreamError",
1467
+ },
1468
+ "504": "#/components/responses/RequestTimeout",
1469
+ },
1470
+ "operationId": "createFundingTransfer",
1471
+ "request": {
1472
+ "$ref": "#/components/schemas/CreateFundingTransferRequest",
1473
+ },
1474
+ "response": {
1475
+ "$ref": "#/components/schemas/CreatedFundingTransfer",
1476
+ },
1477
+ },
1478
+ "createVault": {
1479
+ "errors": {
1480
+ "400": {
1481
+ "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidOrIdempotencyKeyRequiredError",
1482
+ },
1483
+ "401": {
1484
+ "$ref": "#/components/schemas/AuthenticationError",
1485
+ },
1486
+ "403": {
1487
+ "$ref": "#/components/schemas/ForbiddenError",
1488
+ },
1489
+ "429": "#/components/responses/RateLimited",
1490
+ "500": "#/components/responses/InternalError",
1491
+ "501": {
1492
+ "$ref": "#/components/schemas/NotImplemented501Error",
1493
+ },
1494
+ "504": "#/components/responses/RequestTimeout",
1495
+ },
1496
+ "operationId": "createFundingTransferVault",
1497
+ "request": {
1498
+ "$ref": "#/components/schemas/CreateFundingTransferVaultRequest",
1499
+ },
1500
+ "response": undefined,
1501
+ },
1502
+ "createZone": {
1503
+ "errors": {
1504
+ "400": {
1505
+ "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidOrIdempotencyKeyRequiredError",
1506
+ },
1507
+ "401": {
1508
+ "$ref": "#/components/schemas/AuthenticationError",
1509
+ },
1510
+ "403": {
1511
+ "$ref": "#/components/schemas/ForbiddenError",
1512
+ },
1513
+ "429": "#/components/responses/RateLimited",
1514
+ "500": "#/components/responses/InternalError",
1515
+ "501": {
1516
+ "$ref": "#/components/schemas/NotImplemented501Error",
1517
+ },
1518
+ "504": "#/components/responses/RequestTimeout",
1519
+ },
1520
+ "operationId": "createFundingTransferZone",
1521
+ "request": {
1522
+ "$ref": "#/components/schemas/CreateFundingTransferZoneRequest",
1523
+ },
1524
+ "response": undefined,
1525
+ },
1526
+ "get": {
1527
+ "errors": {
1528
+ "400": {
1529
+ "$ref": "#/components/schemas/ApiKeyMalformedOrParamInvalidError",
1530
+ },
1531
+ "401": {
1532
+ "$ref": "#/components/schemas/AuthenticationError",
1533
+ },
1534
+ "403": {
1535
+ "$ref": "#/components/schemas/ForbiddenError",
1536
+ },
1537
+ "404": {
1538
+ "$ref": "#/components/schemas/FundingTransferNotFoundError",
1539
+ },
1540
+ "429": "#/components/responses/RateLimited",
1541
+ "500": "#/components/responses/InternalError",
1542
+ "502": {
1543
+ "$ref": "#/components/schemas/UpstreamError",
1544
+ },
1545
+ "504": "#/components/responses/RequestTimeout",
1546
+ },
1547
+ "operationId": "getFundingTransfer",
1548
+ "request": undefined,
1549
+ "response": {
1550
+ "$ref": "#/components/schemas/FundingTransfer",
1551
+ },
1552
+ },
1553
+ "list": {
1554
+ "errors": {
1555
+ "400": {
1556
+ "$ref": "#/components/schemas/ApiKeyMalformedOrQueryInvalidError",
1557
+ },
1558
+ "401": {
1559
+ "$ref": "#/components/schemas/AuthenticationError",
1560
+ },
1561
+ "403": {
1562
+ "$ref": "#/components/schemas/ForbiddenError",
1563
+ },
1564
+ "429": "#/components/responses/RateLimited",
1565
+ "500": "#/components/responses/InternalError",
1566
+ "502": {
1567
+ "$ref": "#/components/schemas/UpstreamError",
1568
+ },
1569
+ "504": "#/components/responses/RequestTimeout",
1570
+ },
1571
+ "operationId": "listFundingTransfers",
1572
+ "request": undefined,
1573
+ "response": {
1574
+ "$ref": "#/components/schemas/FundingTransferList",
1575
+ },
1576
+ },
1577
+ "registerSourceTransaction": {
1578
+ "errors": {
1579
+ "400": {
1580
+ "$ref": "#/components/schemas/ApiKeyMalformedOrBodyInvalidOrParamInvalidOrSourceTransactionInvalidError",
1581
+ },
1582
+ "401": {
1583
+ "$ref": "#/components/schemas/AuthenticationError",
1584
+ },
1585
+ "403": {
1586
+ "$ref": "#/components/schemas/ForbiddenError",
1587
+ },
1588
+ "404": {
1589
+ "$ref": "#/components/schemas/FundingTransferNotFoundError",
1590
+ },
1591
+ "409": {
1592
+ "$ref": "#/components/schemas/FundingTransferNotAwaitingSourceOrSourceTransactionConflictOrSourceTransactionPendingError",
1593
+ },
1594
+ "429": "#/components/responses/RateLimited",
1595
+ "500": "#/components/responses/InternalError",
1596
+ "501": {
1597
+ "$ref": "#/components/schemas/NotImplemented501Error",
1598
+ },
1599
+ "502": {
1600
+ "$ref": "#/components/schemas/UpstreamError",
1601
+ },
1602
+ "504": "#/components/responses/RequestTimeout",
1603
+ },
1604
+ "operationId": "registerFundingTransferSourceTransaction",
1605
+ "request": {
1606
+ "$ref": "#/components/schemas/RegisterFundingTransferSourceTransactionRequest",
1607
+ },
1608
+ "response": {
1609
+ "$ref": "#/components/schemas/FundingTransfer",
1610
+ },
1611
+ },
1612
+ },
1613
+ }
1614
+ `)
1615
+ })
1616
+
759
1617
  test('publishes an executable account transfer example', async () => {
760
1618
  const app = create({ auth: false })
761
- type TokenSchema = { examples: string[] }
762
1619
  type TransferExample = { destinationToken: { address: string } }
763
1620
  type Operation = {
764
1621
  requestBody: {
765
- content: Record<string, { schema: { properties: { destinationToken: TokenSchema } } }>
1622
+ content: Record<string, { schema: JsonSchema }>
766
1623
  }
767
1624
  responses: Record<string, { content: Record<string, { example: TransferExample }> }>
768
1625
  }
769
- type Spec = { paths: Record<string, { post: Operation }> }
1626
+ type Spec = ComponentSpec & { paths: Record<string, { post: Operation }> }
770
1627
  const spec = (await (await app.request('/openapi.json')).json()) as Spec
771
1628
  const operation = spec.paths['/v1/funding/transfers']!.post
1629
+ const request = dereference(spec, operation.requestBody.content['application/json']!.schema)
772
1630
 
773
1631
  expect({
774
- request:
775
- operation.requestBody.content['application/json']!.schema.properties.destinationToken
776
- .examples,
1632
+ request: request.properties!['destinationToken']!.examples,
777
1633
  response:
778
1634
  operation.responses['200']!.content['application/json']!.example.destinationToken.address,
779
1635
  }).toEqual({
@@ -826,8 +1682,11 @@ describe('funding transfers OpenAPI', () => {
826
1682
  'x-tagGroups': { name: string; tags: string[] }[]
827
1683
  }
828
1684
  const spec = (await (await app.request('/openapi.json')).json()) as Spec
829
- const detailPath = Object.keys(spec.paths).find((path) =>
830
- path.includes('/v1/funding/transfers/{'),
1685
+ const detailPath = Object.keys(spec.paths).find(
1686
+ (path) => path.includes('/v1/funding/transfers/{') && !path.endsWith('/source-transactions'),
1687
+ )!
1688
+ const sourceTransactionPath = Object.keys(spec.paths).find((path) =>
1689
+ path.endsWith('/source-transactions'),
831
1690
  )!
832
1691
  const depositAddressPath = Object.keys(spec.paths).find(
833
1692
  (path) =>
@@ -856,6 +1715,7 @@ describe('funding transfers OpenAPI', () => {
856
1715
  list: summarize(spec.paths['/v1/funding/transfers']!.get!),
857
1716
  providers: summarize(spec.paths['/v1/funding/providers']!.get!),
858
1717
  quotes: summarize(spec.paths['/v1/funding/quotes']!.get!),
1718
+ sourceTransaction: summarize(spec.paths[sourceTransactionPath]!.post!),
859
1719
  vault: summarize(spec.paths['/v1/funding/transfers/vault']!.post!),
860
1720
  zone: summarize(spec.paths['/v1/funding/transfers/zone']!.post!),
861
1721
  }
@@ -926,6 +1786,10 @@ describe('funding transfers OpenAPI', () => {
926
1786
  "description": "Returns live quotes for transferring stablecoins to Tempo.",
927
1787
  "summary": "Get quotes",
928
1788
  },
1789
+ "sourceTransaction": {
1790
+ "description": "Registers and verifies a funding source transaction.",
1791
+ "summary": "Register source transaction",
1792
+ },
929
1793
  "vault": {
930
1794
  "description": "Creates a transfer into a Tempo Earn vault.",
931
1795
  "summary": "Create transfer into vault",
@@ -941,6 +1805,7 @@ describe('funding transfers OpenAPI', () => {
941
1805
  "Get providers",
942
1806
  "List deposit addresses",
943
1807
  "Create deposit address",
1808
+ "Register source transaction",
944
1809
  "List transfers",
945
1810
  "Create transfer",
946
1811
  "Create transfer into vault",
@@ -1002,6 +1867,7 @@ describe('funding transfers OpenAPI', () => {
1002
1867
  "amount",
1003
1868
  "destinationToken",
1004
1869
  "mode",
1870
+ "provider",
1005
1871
  "recipient",
1006
1872
  "sender",
1007
1873
  "slippageBps",
@@ -1031,6 +1897,7 @@ describe('funding transfers OpenAPI', () => {
1031
1897
  "429",
1032
1898
  "500",
1033
1899
  "502",
1900
+ "504",
1034
1901
  ],
1035
1902
  "vault": {
1036
1903
  "additionalProperties": false,
@@ -1052,6 +1919,7 @@ describe('funding transfers OpenAPI', () => {
1052
1919
  "429",
1053
1920
  "500",
1054
1921
  "501",
1922
+ "504",
1055
1923
  ],
1056
1924
  "zone": {
1057
1925
  "additionalProperties": false,
@@ -1075,6 +1943,7 @@ describe('funding transfers OpenAPI', () => {
1075
1943
  "429",
1076
1944
  "500",
1077
1945
  "501",
1946
+ "504",
1078
1947
  ],
1079
1948
  }
1080
1949
  `)
@@ -1120,8 +1989,11 @@ describe('funding transfers OpenAPI', () => {
1120
1989
  }
1121
1990
  >
1122
1991
  }
1123
- const detailPath = Object.keys(spec.paths).find((path) =>
1124
- path.includes('/v1/funding/transfers/{'),
1992
+ const detailPath = Object.keys(spec.paths).find(
1993
+ (path) => path.includes('/v1/funding/transfers/{') && !path.endsWith('/source-transactions'),
1994
+ )
1995
+ const sourceTransactionPath = Object.keys(spec.paths).find((path) =>
1996
+ path.endsWith('/source-transactions'),
1125
1997
  )
1126
1998
  const depositAddressPath = Object.keys(spec.paths).find(
1127
1999
  (path) =>
@@ -1157,6 +2029,10 @@ describe('funding transfers OpenAPI', () => {
1157
2029
  requiredCreateHeader: spec.paths['/v1/funding/transfers']?.post?.parameters.find(
1158
2030
  (parameter) => parameter.name === 'idempotency-key',
1159
2031
  ),
2032
+ sourceTransaction:
2033
+ sourceTransactionPath && spec.paths[sourceTransactionPath]?.post?.operationId,
2034
+ sourceTransactionScopes:
2035
+ sourceTransactionPath && spec.paths[sourceTransactionPath]?.post?.['x-required-scopes'],
1160
2036
  vault: spec.paths['/v1/funding/transfers/vault']?.post?.operationId,
1161
2037
  vaultScopes: spec.paths['/v1/funding/transfers/vault']?.post?.['x-required-scopes'],
1162
2038
  zone: spec.paths['/v1/funding/transfers/zone']?.post?.operationId,
@@ -1185,6 +2061,8 @@ describe('funding transfers OpenAPI', () => {
1185
2061
  required: true,
1186
2062
  schema: expect.any(Object),
1187
2063
  }),
2064
+ sourceTransaction: 'registerFundingTransferSourceTransaction',
2065
+ sourceTransactionScopes: ['funding:write'],
1188
2066
  vault: 'createFundingTransferVault',
1189
2067
  vaultScopes: ['funding:write'],
1190
2068
  zone: 'createFundingTransferZone',
@@ -1211,7 +2089,7 @@ const fundingProjectReader = {
1211
2089
  const fundingForeign = {
1212
2090
  id: 'key_funding_other',
1213
2091
  orgId: 'org_funding_other',
1214
- scopes: ['funding:read'],
2092
+ scopes: ['funding:read', 'funding:write'],
1215
2093
  token: 'secret_funding_other',
1216
2094
  } satisfies TestApp.kvStore.Key
1217
2095