tapimo 0.10.2 → 0.12.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 (386) hide show
  1. package/dist/App.d.ts +4896 -2166
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +2 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.d.ts +15 -0
  6. package/dist/Assets.d.ts.map +1 -1
  7. package/dist/Assets.js +7 -0
  8. package/dist/Assets.js.map +1 -1
  9. package/dist/Webhooks.d.ts +0 -75
  10. package/dist/Webhooks.d.ts.map +1 -1
  11. package/dist/Webhooks.js +1 -143
  12. package/dist/Webhooks.js.map +1 -1
  13. package/dist/admin/App.d.ts +127 -78
  14. package/dist/admin/App.d.ts.map +1 -1
  15. package/dist/admin/apps/earn-vaults.d.ts +115 -1
  16. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  17. package/dist/admin/apps/earn-vaults.js +59 -6
  18. package/dist/admin/apps/earn-vaults.js.map +1 -1
  19. package/dist/admin/apps/organizations.d.ts +3 -3
  20. package/dist/admin/apps/organizations.d.ts.map +1 -1
  21. package/dist/admin/apps/organizations.js +4 -2
  22. package/dist/admin/apps/organizations.js.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +83 -83
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.d.ts.map +1 -1
  26. package/dist/admin/ui.generated.js +151 -151
  27. package/dist/admin/ui.generated.js.map +1 -1
  28. package/dist/apps/data/App.d.ts +5924 -4310
  29. package/dist/apps/data/App.d.ts.map +1 -1
  30. package/dist/apps/data/App.js +61 -9
  31. package/dist/apps/data/App.js.map +1 -1
  32. package/dist/apps/data/ZoneSelection.d.ts +62 -0
  33. package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
  34. package/dist/apps/data/ZoneSelection.js +44 -0
  35. package/dist/apps/data/ZoneSelection.js.map +1 -0
  36. package/dist/apps/data/routes/activities.d.ts +2368 -5
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +423 -114
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +131 -0
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +25 -25
  43. package/dist/apps/data/routes/balances.js +1 -1
  44. package/dist/apps/data/routes/balances.js.map +1 -1
  45. package/dist/apps/data/routes/blocks.d.ts +1 -1
  46. package/dist/apps/data/routes/blocks.js +1 -1
  47. package/dist/apps/data/routes/blocks.js.map +1 -1
  48. package/dist/apps/data/routes/earn.d.ts +135 -10
  49. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  50. package/dist/apps/data/routes/earn.js +157 -25
  51. package/dist/apps/data/routes/earn.js.map +1 -1
  52. package/dist/apps/data/routes/exchanges.d.ts +829 -123
  53. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  54. package/dist/apps/data/routes/exchanges.js +16 -13
  55. package/dist/apps/data/routes/exchanges.js.map +1 -1
  56. package/dist/apps/data/routes/fee-amm.d.ts +26 -26
  57. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  58. package/dist/apps/data/routes/fee-amm.js +9 -11
  59. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  60. package/dist/apps/data/routes/receipts.d.ts +54 -54
  61. package/dist/apps/data/routes/receipts.js +1 -1
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/rpc.js +24 -10
  64. package/dist/apps/data/routes/rpc.js.map +1 -1
  65. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  66. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  67. package/dist/apps/data/routes/tokenlist.js +16 -15
  68. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  69. package/dist/apps/data/routes/tokens.d.ts +17 -3
  70. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  71. package/dist/apps/data/routes/tokens.js +34 -15
  72. package/dist/apps/data/routes/tokens.js.map +1 -1
  73. package/dist/apps/data/routes/transactions.d.ts +1 -1
  74. package/dist/apps/data/routes/transactions.js +1 -1
  75. package/dist/apps/data/routes/transactions.js.map +1 -1
  76. package/dist/apps/data/routes/transfers.d.ts +13 -13
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +7 -9
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/valuation.d.ts +21 -21
  81. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  82. package/dist/apps/data/routes/webhooks.d.ts +1011 -151
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +126 -739
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/data/routes/zones.d.ts +15 -15
  87. package/dist/apps/funding/App.d.ts +27 -27
  88. package/dist/apps/funding/routes/chains.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/chains.js +5 -4
  90. package/dist/apps/funding/routes/chains.js.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.d.ts +18 -18
  92. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  93. package/dist/apps/funding/routes/deposit-addresses.js +8 -8
  94. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  95. package/dist/apps/funding/routes/providers.d.ts.map +1 -1
  96. package/dist/apps/funding/routes/providers.js +3 -2
  97. package/dist/apps/funding/routes/providers.js.map +1 -1
  98. package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
  99. package/dist/apps/funding/routes/quotes.js +2 -2
  100. package/dist/apps/funding/routes/quotes.js.map +1 -1
  101. package/dist/apps/funding/routes/transfers.d.ts +33 -33
  102. package/dist/apps/management/App.d.ts +1433 -322
  103. package/dist/apps/management/App.d.ts.map +1 -1
  104. package/dist/apps/management/Billing.d.ts.map +1 -1
  105. package/dist/apps/management/Billing.js +19 -6
  106. package/dist/apps/management/Billing.js.map +1 -1
  107. package/dist/apps/management/routes/api-keys.d.ts +154 -94
  108. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  109. package/dist/apps/management/routes/api-keys.js +76 -30
  110. package/dist/apps/management/routes/api-keys.js.map +1 -1
  111. package/dist/apps/management/routes/billing.d.ts +103 -30
  112. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  113. package/dist/apps/management/routes/billing.js +91 -32
  114. package/dist/apps/management/routes/billing.js.map +1 -1
  115. package/dist/apps/management/routes/faucet.d.ts +12 -12
  116. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  117. package/dist/apps/management/routes/faucet.js +23 -12
  118. package/dist/apps/management/routes/faucet.js.map +1 -1
  119. package/dist/apps/management/routes/invitations.d.ts +46 -13
  120. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  121. package/dist/apps/management/routes/invitations.js +79 -29
  122. package/dist/apps/management/routes/invitations.js.map +1 -1
  123. package/dist/apps/management/routes/invite-links.d.ts +128 -83
  124. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  125. package/dist/apps/management/routes/invite-links.js +100 -41
  126. package/dist/apps/management/routes/invite-links.js.map +1 -1
  127. package/dist/apps/management/routes/me.d.ts +5 -5
  128. package/dist/apps/management/routes/me.d.ts.map +1 -1
  129. package/dist/apps/management/routes/me.js +14 -10
  130. package/dist/apps/management/routes/me.js.map +1 -1
  131. package/dist/apps/management/routes/members.d.ts +68 -47
  132. package/dist/apps/management/routes/members.d.ts.map +1 -1
  133. package/dist/apps/management/routes/members.js +37 -18
  134. package/dist/apps/management/routes/members.js.map +1 -1
  135. package/dist/apps/management/routes/orgs.d.ts +48 -8
  136. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  137. package/dist/apps/management/routes/orgs.js +48 -19
  138. package/dist/apps/management/routes/orgs.js.map +1 -1
  139. package/dist/apps/management/routes/projects.d.ts +130 -68
  140. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  141. package/dist/apps/management/routes/projects.js +72 -21
  142. package/dist/apps/management/routes/projects.js.map +1 -1
  143. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  144. package/dist/apps/management/routes/scopes.js +8 -5
  145. package/dist/apps/management/routes/scopes.js.map +1 -1
  146. package/dist/apps/management/routes/usage.d.ts +38 -38
  147. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  148. package/dist/apps/management/routes/usage.js +41 -31
  149. package/dist/apps/management/routes/usage.js.map +1 -1
  150. package/dist/apps/management/routes/verified-token-requests.d.ts +34 -6
  151. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
  152. package/dist/apps/management/routes/verified-token-requests.js +36 -17
  153. package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
  154. package/dist/apps/management/routes/webhooks.d.ts +1186 -197
  155. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  156. package/dist/apps/management/routes/webhooks.js +106 -38
  157. package/dist/apps/management/routes/webhooks.js.map +1 -1
  158. package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
  159. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  160. package/dist/apps/mpp/internal/Adapter.js +12 -0
  161. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  162. package/dist/db/Db.d.ts +2 -0
  163. package/dist/db/Db.d.ts.map +1 -1
  164. package/dist/db/Db.js +61 -0
  165. package/dist/db/Db.js.map +1 -1
  166. package/dist/db/Schema.d.ts +30 -2
  167. package/dist/db/Schema.d.ts.map +1 -1
  168. package/dist/db/Schema.js +28 -3
  169. package/dist/db/Schema.js.map +1 -1
  170. package/dist/db/tables/earnVaults.d.ts +5 -1
  171. package/dist/db/tables/earnVaults.d.ts.map +1 -1
  172. package/dist/db/tables/earnVaults.js +5 -0
  173. package/dist/db/tables/earnVaults.js.map +1 -1
  174. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  175. package/dist/db/tables/webhookDeliveries.js +10 -0
  176. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  177. package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
  178. package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
  179. package/dist/db/tables/webhookQueueCompletions.js +32 -0
  180. package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
  181. package/dist/db/tables/webhookQueueEvents.d.ts +16 -20
  182. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  183. package/dist/db/tables/webhookQueueEvents.js +68 -43
  184. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  185. package/dist/db/tables/webhookSubscriptions.d.ts +47 -72
  186. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  187. package/dist/db/tables/webhookSubscriptions.js +92 -105
  188. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  189. package/dist/internal/Auth.d.ts +8 -1
  190. package/dist/internal/Auth.d.ts.map +1 -1
  191. package/dist/internal/Auth.js +13 -3
  192. package/dist/internal/Auth.js.map +1 -1
  193. package/dist/internal/Cache.d.ts.map +1 -1
  194. package/dist/internal/Cache.js +11 -7
  195. package/dist/internal/Cache.js.map +1 -1
  196. package/dist/internal/EarnVaults.d.ts +41 -2
  197. package/dist/internal/EarnVaults.d.ts.map +1 -1
  198. package/dist/internal/EarnVaults.js +167 -0
  199. package/dist/internal/EarnVaults.js.map +1 -1
  200. package/dist/internal/MetricSink.d.ts +35 -125
  201. package/dist/internal/MetricSink.d.ts.map +1 -1
  202. package/dist/internal/MetricSink.js +50 -121
  203. package/dist/internal/MetricSink.js.map +1 -1
  204. package/dist/internal/OpenApi.d.ts +26 -22
  205. package/dist/internal/OpenApi.d.ts.map +1 -1
  206. package/dist/internal/OpenApi.js +128 -39
  207. package/dist/internal/OpenApi.js.map +1 -1
  208. package/dist/internal/Response.d.ts +13 -8
  209. package/dist/internal/Response.d.ts.map +1 -1
  210. package/dist/internal/Response.js +16 -5
  211. package/dist/internal/Response.js.map +1 -1
  212. package/dist/internal/Schema.d.ts +8 -8
  213. package/dist/internal/Schema.d.ts.map +1 -1
  214. package/dist/internal/Schema.js +10 -9
  215. package/dist/internal/Schema.js.map +1 -1
  216. package/dist/internal/Store.d.ts +15 -6
  217. package/dist/internal/Store.d.ts.map +1 -1
  218. package/dist/internal/Store.js +45 -15
  219. package/dist/internal/Store.js.map +1 -1
  220. package/dist/internal/Tidx.d.ts +2 -0
  221. package/dist/internal/Tidx.d.ts.map +1 -1
  222. package/dist/internal/Tidx.js +16 -4
  223. package/dist/internal/Tidx.js.map +1 -1
  224. package/dist/internal/WebhookTransfer.d.ts +1 -55
  225. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  226. package/dist/internal/WebhookTransfer.js +1 -239
  227. package/dist/internal/WebhookTransfer.js.map +1 -1
  228. package/dist/internal/Webhooks.d.ts +112 -56
  229. package/dist/internal/Webhooks.d.ts.map +1 -1
  230. package/dist/internal/Webhooks.js +124 -93
  231. package/dist/internal/Webhooks.js.map +1 -1
  232. package/dist/internal/funding/Deposit.d.ts +2 -2
  233. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  234. package/dist/internal/funding/Deposit.js +9 -8
  235. package/dist/internal/funding/Deposit.js.map +1 -1
  236. package/dist/internal/funding/DepositAddress.d.ts +4 -4
  237. package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
  238. package/dist/internal/funding/DepositAddress.js +9 -8
  239. package/dist/internal/funding/DepositAddress.js.map +1 -1
  240. package/dist/internal/funding/DepositWebhook.d.ts +25 -0
  241. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -0
  242. package/dist/internal/funding/DepositWebhook.js +74 -0
  243. package/dist/internal/funding/DepositWebhook.js.map +1 -0
  244. package/dist/internal/funding/Provider.d.ts.map +1 -1
  245. package/dist/internal/funding/Provider.js +13 -12
  246. package/dist/internal/funding/Provider.js.map +1 -1
  247. package/dist/internal/funding/Reconciliation.d.ts +6 -0
  248. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  249. package/dist/internal/funding/Reconciliation.js +42 -10
  250. package/dist/internal/funding/Reconciliation.js.map +1 -1
  251. package/dist/internal/funding/index.d.ts +1 -0
  252. package/dist/internal/funding/index.d.ts.map +1 -1
  253. package/dist/internal/funding/index.js +1 -0
  254. package/dist/internal/funding/index.js.map +1 -1
  255. package/dist/internal/funding/providers/stargate.d.ts +19 -0
  256. package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
  257. package/dist/internal/funding/providers/stargate.js +143 -0
  258. package/dist/internal/funding/providers/stargate.js.map +1 -0
  259. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  260. package/dist/internal/webhookDestinations/slack.js +16 -0
  261. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  262. package/dist/providers/index.d.ts +7 -0
  263. package/dist/providers/index.d.ts.map +1 -1
  264. package/dist/providers/index.js +3 -0
  265. package/dist/providers/index.js.map +1 -1
  266. package/package.json +2 -2
  267. package/src/App.test.ts +48 -0
  268. package/src/App.ts +7 -2
  269. package/src/Assets.test.ts +18 -0
  270. package/src/Assets.ts +22 -0
  271. package/src/Client.test-d.ts +287 -17
  272. package/src/Webhooks.ts +1 -220
  273. package/src/admin/apps/earn-vaults.test.ts +143 -0
  274. package/src/admin/apps/earn-vaults.ts +83 -6
  275. package/src/admin/apps/organizations.ts +6 -2
  276. package/src/admin/ui/src/routes/api-keys.tsx +36 -0
  277. package/src/admin/ui.generated.ts +151 -189
  278. package/src/apps/data/App.test.ts +31 -0
  279. package/src/apps/data/App.ts +64 -10
  280. package/src/apps/data/ZoneSelection.test.ts +196 -0
  281. package/src/apps/data/ZoneSelection.ts +108 -0
  282. package/src/apps/data/routes/activities.test.ts +435 -12
  283. package/src/apps/data/routes/activities.ts +562 -125
  284. package/src/apps/data/routes/balances.ts +1 -1
  285. package/src/apps/data/routes/blocks.ts +1 -1
  286. package/src/apps/data/routes/earn.test.ts +183 -23
  287. package/src/apps/data/routes/earn.ts +225 -29
  288. package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
  289. package/src/apps/data/routes/exchanges.test.ts +31 -1
  290. package/src/apps/data/routes/exchanges.ts +19 -15
  291. package/src/apps/data/routes/fee-amm.ts +18 -11
  292. package/src/apps/data/routes/receipts.ts +1 -1
  293. package/src/apps/data/routes/rpc.test.ts +75 -0
  294. package/src/apps/data/routes/rpc.ts +23 -9
  295. package/src/apps/data/routes/tokenlist.ts +21 -18
  296. package/src/apps/data/routes/tokens.test.ts +29 -0
  297. package/src/apps/data/routes/tokens.ts +38 -15
  298. package/src/apps/data/routes/transactions.ts +1 -1
  299. package/src/apps/data/routes/transfers.test.ts +3 -3
  300. package/src/apps/data/routes/transfers.ts +12 -9
  301. package/src/apps/data/routes/webhooks.test.ts +179 -1497
  302. package/src/apps/data/routes/webhooks.ts +156 -976
  303. package/src/apps/data/routes/zones.test.ts +19 -13
  304. package/src/apps/funding/routes/chains.test.ts +211 -7
  305. package/src/apps/funding/routes/chains.ts +19 -12
  306. package/src/apps/funding/routes/deposit-addresses.test.ts +394 -0
  307. package/src/apps/funding/routes/deposit-addresses.ts +84 -72
  308. package/src/apps/funding/routes/providers.ts +9 -5
  309. package/src/apps/funding/routes/quotes.test.ts +46 -0
  310. package/src/apps/funding/routes/quotes.ts +8 -5
  311. package/src/apps/funding/routes/transfers.test.ts +35 -32
  312. package/src/apps/management/Billing.test.ts +26 -0
  313. package/src/apps/management/Billing.ts +24 -7
  314. package/src/apps/management/routes/api-keys.test.ts +178 -0
  315. package/src/apps/management/routes/api-keys.ts +203 -144
  316. package/src/apps/management/routes/billing.test.ts +209 -0
  317. package/src/apps/management/routes/billing.ts +254 -177
  318. package/src/apps/management/routes/faucet.test.ts +39 -0
  319. package/src/apps/management/routes/faucet.ts +75 -41
  320. package/src/apps/management/routes/invitations.test.ts +216 -0
  321. package/src/apps/management/routes/invitations.ts +186 -109
  322. package/src/apps/management/routes/invite-links.test.ts +263 -1
  323. package/src/apps/management/routes/invite-links.ts +295 -214
  324. package/src/apps/management/routes/me.test.ts +49 -0
  325. package/src/apps/management/routes/me.ts +42 -32
  326. package/src/apps/management/routes/members.test.ts +124 -1
  327. package/src/apps/management/routes/members.ts +81 -51
  328. package/src/apps/management/routes/orgs.test.ts +176 -0
  329. package/src/apps/management/routes/orgs.ts +101 -59
  330. package/src/apps/management/routes/projects.test.ts +183 -0
  331. package/src/apps/management/routes/projects.ts +124 -60
  332. package/src/apps/management/routes/scopes.test.ts +44 -0
  333. package/src/apps/management/routes/scopes.ts +37 -28
  334. package/src/apps/management/routes/usage.test.ts +92 -0
  335. package/src/apps/management/routes/usage.ts +186 -151
  336. package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
  337. package/src/apps/management/routes/verified-token-requests.ts +166 -133
  338. package/src/apps/management/routes/webhooks.test.ts +313 -1
  339. package/src/apps/management/routes/webhooks.ts +220 -71
  340. package/src/apps/mpp/internal/Adapter.ts +13 -1
  341. package/src/db/Db.test.ts +42 -5
  342. package/src/db/Db.ts +85 -0
  343. package/src/db/Schema.ts +41 -3
  344. package/src/db/tables/earnVaults.test.ts +22 -0
  345. package/src/db/tables/earnVaults.ts +11 -1
  346. package/src/db/tables/organizations.test.ts +5 -2
  347. package/src/db/tables/webhookDeliveries.ts +13 -0
  348. package/src/db/tables/webhookQueueCompletions.ts +55 -0
  349. package/src/db/tables/webhookQueueEvents.ts +97 -57
  350. package/src/db/tables/webhookSubscriptions.ts +134 -144
  351. package/src/internal/Auth.ts +23 -5
  352. package/src/internal/Cache.ts +14 -7
  353. package/src/internal/EarnVaults.capabilities.test.ts +33 -0
  354. package/src/internal/EarnVaults.test.ts +79 -0
  355. package/src/internal/EarnVaults.ts +277 -0
  356. package/src/internal/MetricSink.test.ts +41 -373
  357. package/src/internal/MetricSink.ts +92 -273
  358. package/src/internal/OpenApi.ts +165 -49
  359. package/src/internal/Response.test.ts +95 -0
  360. package/src/internal/Response.ts +40 -2
  361. package/src/internal/Schema.test.ts +34 -0
  362. package/src/internal/Schema.ts +20 -9
  363. package/src/internal/Store.test.ts +186 -0
  364. package/src/internal/Store.ts +57 -17
  365. package/src/internal/Tidx.test.ts +19 -0
  366. package/src/internal/Tidx.ts +16 -4
  367. package/src/internal/WebhookTransfer.ts +1 -351
  368. package/src/internal/Webhooks.test.ts +222 -145
  369. package/src/internal/Webhooks.ts +234 -135
  370. package/src/internal/funding/Deposit.ts +71 -64
  371. package/src/internal/funding/DepositAddress.ts +50 -43
  372. package/src/internal/funding/DepositWebhook.test.ts +131 -0
  373. package/src/internal/funding/DepositWebhook.ts +101 -0
  374. package/src/internal/funding/Provider.ts +164 -142
  375. package/src/internal/funding/Reconciliation.test.ts +14 -0
  376. package/src/internal/funding/Reconciliation.ts +161 -86
  377. package/src/internal/funding/index.test.ts +1 -0
  378. package/src/internal/funding/index.ts +1 -0
  379. package/src/internal/funding/providers/index.test.ts +185 -0
  380. package/src/internal/funding/providers/stargate.ts +184 -0
  381. package/src/internal/webhookDestinations/slack.ts +15 -0
  382. package/src/providers/index.test.ts +1 -0
  383. package/src/providers/index.ts +9 -0
  384. package/src/Webhooks.bench.ts +0 -714
  385. package/src/Webhooks.memory.bench.ts +0 -250
  386. package/src/Webhooks.test.ts +0 -146
@@ -1,17 +1,11 @@
1
- import { type Db, Store, Webhooks as Core } from 'tapimo'
2
- import * as z from 'zod/mini'
1
+ import { type Db, Webhooks as Core } from 'tapimo'
3
2
 
4
3
  import * as Cursor from '../../../internal/Cursor.js'
5
- import * as Viem from '../../../internal/Viem.js'
6
4
  import * as TestApp from '../../../../test/App.js'
7
5
  import * as Mppx from '../../../../test/Mppx.js'
8
- import * as Runtime from '../../../../test/runtime.js'
9
6
  import * as Tempo from '../../../../test/Tempo.js'
10
7
  import * as Webhooks from './webhooks.js'
11
8
 
12
- /** Humanized transaction payload delivered by transaction webhooks. */
13
- type TransactionEvent = z.output<typeof Webhooks.schema.TransactionEvent>
14
-
15
9
  /** API key with both webhook scopes, owned by `org_test`. */
16
10
  const writer = {
17
11
  id: 'key_writer',
@@ -115,6 +109,29 @@ describe('feature gate', () => {
115
109
  const body = (await response.json()) as { error?: { code?: string } }
116
110
  expect(body.error?.code).toMatchInlineSnapshot(`"webhooks_not_enabled"`)
117
111
  })
112
+
113
+ test('does not register funding events without a host producer', async () => {
114
+ const app = TestApp.client({
115
+ auth: { keys: [writer] },
116
+ webhook: { supportedChainIds: [4217, 42431] },
117
+ })
118
+ const catalog = await app.v1.webhooks['event-types'].$get({})
119
+ const catalogBody = await TestApp.json(catalog, Webhooks.schema.getWebhookEventTypes.Response)
120
+ const created = await app.v1.webhooks.$post(
121
+ {
122
+ json: {
123
+ destination: { type: 'url', url: 'https://example.com/funding' },
124
+ eventType: 'funding:deposit.updated',
125
+ },
126
+ },
127
+ as(writer),
128
+ )
129
+
130
+ expect(catalogBody.data.map((eventType) => eventType.type)).not.toContain(
131
+ 'funding:deposit.updated',
132
+ )
133
+ expect(created.status).toBe(400)
134
+ })
118
135
  })
119
136
 
120
137
  describe('access matrix', () => {
@@ -191,6 +208,7 @@ describe('access matrix', () => {
191
208
  expect(body.data.map((entry) => entry.type)).toMatchInlineSnapshot(`
192
209
  [
193
210
  "block:created",
211
+ "funding:deposit.updated",
194
212
  "log:emitted",
195
213
  "token:transfer",
196
214
  "transaction:included",
@@ -220,6 +238,151 @@ describe('access matrix', () => {
220
238
  })
221
239
 
222
240
  describe('CRUD lifecycle', () => {
241
+ test('requires funding read access to create a funding deposit subscription', async () => {
242
+ const webhookWriter = {
243
+ id: 'key_funding_webhook_only',
244
+ orgId: 'org_funding_webhook_only',
245
+ scopes: ['webhooks:write'],
246
+ token: 'secret_funding_webhook_only',
247
+ } satisfies TestApp.kvStore.Key
248
+ const app = TestApp.client({ auth: { keys: [webhookWriter] } })
249
+ const response = await app.v1.webhooks.$post(
250
+ {
251
+ json: {
252
+ destination: { type: 'url', url: 'https://example.com/funding' },
253
+ eventType: 'funding:deposit.updated',
254
+ },
255
+ },
256
+ as(webhookWriter),
257
+ )
258
+ const body = (await response.json()) as { error?: { code?: string } }
259
+
260
+ expect(response.status).toBe(403)
261
+ expect(body.error?.code).toBe('api_key_forbidden')
262
+ })
263
+
264
+ test('captures funding deposit ownership scope without reading the RPC head', async () => {
265
+ const db = TestApp.database()
266
+ const projectWriter = {
267
+ id: 'key_funding_webhook_project',
268
+ orgId: 'org_funding_webhook',
269
+ projectId: 'prj_funding_webhook',
270
+ scopes: ['funding:read', 'webhooks:write'],
271
+ token: 'secret_funding_webhook_project',
272
+ } satisfies TestApp.kvStore.Key
273
+ const app = TestApp.client({ auth: { keys: [projectWriter] }, db })
274
+ const response = await app.v1.webhooks.$post(
275
+ {
276
+ json: {
277
+ destination: { type: 'url', url: 'https://example.com/funding' },
278
+ eventType: 'funding:deposit.updated',
279
+ filters: { status: 'completed' },
280
+ },
281
+ },
282
+ as(projectWriter),
283
+ )
284
+ const body = await TestApp.json(response, Webhooks.schema.createWebhook.Response)
285
+ const stored = await Core.getSubscription(
286
+ db,
287
+ { orgId: projectWriter.orgId, type: 'api_key' },
288
+ body.id,
289
+ )
290
+
291
+ expect(response.status).toBe(200)
292
+ expect(stored).toMatchObject({
293
+ environment: 'production',
294
+ projectId: projectWriter.projectId,
295
+ })
296
+ expect(await Core.getCursor(db, body.id)).toBeNull()
297
+ })
298
+
299
+ test('isolates funding subscriptions by API-key environment and project', async () => {
300
+ const db = TestApp.database()
301
+ const shared = {
302
+ orgId: 'org_funding_access',
303
+ scopes: ['funding:read', 'webhooks:read', 'webhooks:write'],
304
+ } as const
305
+ const projectA = {
306
+ ...shared,
307
+ id: 'key_funding_project_a',
308
+ projectId: 'prj_funding_a',
309
+ token: 'secret_funding_project_a',
310
+ } satisfies TestApp.kvStore.Key
311
+ const projectB = {
312
+ ...shared,
313
+ id: 'key_funding_project_b',
314
+ projectId: 'prj_funding_b',
315
+ token: 'secret_funding_project_b',
316
+ } satisfies TestApp.kvStore.Key
317
+ const sandboxProjectB = {
318
+ ...projectB,
319
+ environment: 'sandbox',
320
+ id: 'key_funding_project_b_sandbox',
321
+ token: 'secret_funding_project_b_sandbox',
322
+ } satisfies TestApp.kvStore.Key
323
+ const projectBWebhookReader = {
324
+ ...projectB,
325
+ id: 'key_funding_project_b_webhook_reader',
326
+ scopes: ['webhooks:read'],
327
+ token: 'secret_funding_project_b_webhook_reader',
328
+ } satisfies TestApp.kvStore.Key
329
+ const organization = {
330
+ ...shared,
331
+ id: 'key_funding_organization',
332
+ token: 'secret_funding_organization',
333
+ } satisfies TestApp.kvStore.Key
334
+ const app = TestApp.client({
335
+ auth: {
336
+ keys: [organization, projectA, projectB, projectBWebhookReader, sandboxProjectB],
337
+ },
338
+ db,
339
+ })
340
+ const subscription = await Core.createSubscription(db, {
341
+ chainId: 4217,
342
+ destination: { type: 'url', url: 'https://example.com/project-b' },
343
+ environment: 'production',
344
+ eventType: 'funding:deposit.updated',
345
+ owner: { orgId: shared.orgId, type: 'api_key' },
346
+ projectId: projectB.projectId,
347
+ })
348
+
349
+ const listed = await app.v1.webhooks.$get({ query: { include: 'totalCount' } }, as(projectA))
350
+ const listedBody = await TestApp.json(listed, Webhooks.schema.listWebhooks.Response)
351
+ const hidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
352
+ { param: { id: subscription.id } },
353
+ as(projectA),
354
+ )
355
+ const sandboxHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
356
+ { param: { id: subscription.id } },
357
+ as(sandboxProjectB),
358
+ )
359
+ const scopeHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
360
+ { param: { id: subscription.id } },
361
+ as(projectBWebhookReader),
362
+ )
363
+ const patchHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$patch(
364
+ { json: { status: 'paused' }, param: { id: subscription.id } },
365
+ as(projectA),
366
+ )
367
+ const deleteHidden = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$delete(
368
+ { param: { id: subscription.id } },
369
+ as(projectA),
370
+ )
371
+ const organizationRead = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].$get(
372
+ { param: { id: subscription.id } },
373
+ as(organization),
374
+ )
375
+
376
+ expect(listed.status).toBe(200)
377
+ expect(listedBody).toMatchObject({ data: [], meta: { totalCount: 0 } })
378
+ expect(hidden.status).toBe(404)
379
+ expect(sandboxHidden.status).toBe(404)
380
+ expect(scopeHidden.status).toBe(404)
381
+ expect(patchHidden.status).toBe(404)
382
+ expect(deleteHidden.status).toBe(404)
383
+ expect(organizationRead.status).toBe(200)
384
+ })
385
+
223
386
  test('creates, reads, lists, updates, and deletes a subscription', async () => {
224
387
  const db = TestApp.database()
225
388
  const app = TestApp.client({
@@ -1152,6 +1315,7 @@ describe('OpenAPI visibility', () => {
1152
1315
  "Included transaction event",
1153
1316
  "Log event",
1154
1317
  "Block event",
1318
+ "Funding deposit updated event",
1155
1319
  "Ping (test delivery)",
1156
1320
  ]
1157
1321
  `)
@@ -1250,6 +1414,14 @@ describe('OpenAPI visibility', () => {
1250
1414
  ],
1251
1415
  "title": "Block subscription",
1252
1416
  },
1417
+ {
1418
+ "filters": [
1419
+ "depositAddressId",
1420
+ "recipient",
1421
+ "status",
1422
+ ],
1423
+ "title": "Funding deposit subscription",
1424
+ },
1253
1425
  ]
1254
1426
  `)
1255
1427
  })
@@ -1351,1496 +1523,6 @@ describe('MPP access (disabled for now — API-key-only)', () => {
1351
1523
  })
1352
1524
  })
1353
1525
 
1354
- // The poll tests pin a fixed immutable historical window on testnet chain 42431.
1355
- // Under `localnet`, the test RPC client is redirected to the disposable local
1356
- // chain, which lacks that history, so the whole block is testnet-only.
1357
- describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1358
- /** Decodes an opaque creation checkpoint or `(block, index)` poller cursor. */
1359
- function decodeCursor(token: string | null): number[] | null {
1360
- if (!token) return null
1361
- const json = atob(token.replaceAll('-', '+').replaceAll('_', '/'))
1362
- return JSON.parse(json) as number[]
1363
- }
1364
-
1365
- /** Encodes a `(block, index)` keyset cursor in the poller's opaque format. */
1366
- function encodeCursor(values: [number, number]): string {
1367
- return btoa(JSON.stringify(values)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')
1368
- }
1369
-
1370
- /** End-of-block index sentinel (matches the poller's own cursor sentinel). */
1371
- const endOfBlockIndex = 2_147_483_647
1372
-
1373
- // Every scanner reads this immutable window over real RPC (chain 42431 to
1374
- // `rpc.testnet.tempo.xyz`), so the results remain snapshot-stable.
1375
- const client = Viem.getClient({ chainId: 42431, rpc: TestApp.rpc })
1376
- // Confirmed scan window `[windowFrom, windowTo]`: anchor the cursor just below
1377
- // `windowFrom`, and pass `headBlockNumber = windowTo + confirmations` so the
1378
- // poller's `safeBlock` lands exactly on `windowTo`.
1379
- const windowFrom = 1_000_000
1380
- const windowTo = 1_000_002
1381
- const windowCursor = encodeCursor([windowFrom - 1, endOfBlockIndex])
1382
- const windowHead = windowTo + 1
1383
- // A sender whose only transfers in `[windowFrom, windowTo]` are exactly two
1384
- // rows, both in block 1,000,002 (`log_idx` 0 then 1): a TIP-20 transfer
1385
- // followed by its fee payment. Filtering on it makes the scan deterministic.
1386
- const windowRecipient = '0x9e39034aae71fb89f66061a2602eb6efec271754'
1387
- const windowSender = '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38'
1388
- // This token has 226 transfers across blocks 1,000,000 through 1,000,003.
1389
- const volumeHead = 1_000_005
1390
- const volumeToken = '0x20c0000000000000000000000000000000000001'
1391
-
1392
- async function createTransferSubscription(db: Db.Db) {
1393
- return Core.createSubscription(db, {
1394
- chainId: 42431,
1395
- eventType: 'token:transfer',
1396
- filters: { sender: windowSender },
1397
- owner: { orgId: 'org_test', type: 'api_key' },
1398
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
1399
- })
1400
- }
1401
-
1402
- async function createVolumeSubscription(db: Db.Db, ownerId = 'org_test') {
1403
- return Core.createSubscription(db, {
1404
- chainId: 42431,
1405
- destination: { type: 'url', url: `https://hooks.example.com/${ownerId}` },
1406
- eventType: 'token:transfer',
1407
- filters: { token: volumeToken },
1408
- owner: { orgId: ownerId, type: 'api_key' },
1409
- })
1410
- }
1411
-
1412
- test('dispatches a signed envelope per row and advances the cursor', async () => {
1413
- const db = TestApp.database()
1414
- const subscription = await createTransferSubscription(db)
1415
- // Anchor the subscription just below the pinned window so the poller scans
1416
- // it this tick (a brand-new subscription anchors at head and skips its first
1417
- // tick).
1418
- await Core.setCursor(db, subscription.id, windowCursor)
1419
-
1420
- const dispatched: Webhooks.poll.Dispatchable[] = []
1421
- const scanCheckpoints: Webhooks.poll.ScanCheckpoint[] = []
1422
- const stages: Webhooks.poll.Stage[] = []
1423
- let cursorLagBlocks: number | undefined
1424
- const result = await Webhooks.poll({
1425
- chainId: 42431,
1426
- dispatch: async (input) => {
1427
- dispatched.push(input)
1428
- return { ok: true }
1429
- },
1430
- eventTypes: ['token:transfer', 'token:transfer'],
1431
- headBlockNumber: windowHead,
1432
- client,
1433
- db,
1434
- onCursorLag(blocks) {
1435
- cursorLagBlocks = blocks
1436
- },
1437
- onScanCheckpoint(checkpoint) {
1438
- scanCheckpoints.push(checkpoint)
1439
- },
1440
- onStage(stage) {
1441
- stages.push(stage)
1442
- },
1443
- })
1444
-
1445
- expect(result).toMatchInlineSnapshot(`
1446
- {
1447
- "chainId": 42431,
1448
- "dispatched": 2,
1449
- "failed": 0,
1450
- "safeBlock": 1000002,
1451
- "scanned": 2,
1452
- "subscriptions": 1,
1453
- }
1454
- `)
1455
- expect(cursorLagBlocks).toBe(3)
1456
- expect(scanCheckpoints.map(({ durationMs: _, ...checkpoint }) => checkpoint))
1457
- .toMatchInlineSnapshot(`
1458
- [
1459
- {
1460
- "concurrency": 8,
1461
- "eventType": "token:transfer",
1462
- "maxWindowBlocks": 3,
1463
- "phase": "start",
1464
- "rpcMethod": "eth_getLogs",
1465
- "subscriptionCount": 1,
1466
- "subscriptionLimit": 256,
1467
- "totalWindowBlocks": 3,
1468
- },
1469
- {
1470
- "concurrency": 8,
1471
- "eventType": "token:transfer",
1472
- "failedSubscriptions": 0,
1473
- "matchedItems": 2,
1474
- "maxWindowBlocks": 3,
1475
- "outcome": "ok",
1476
- "phase": "complete",
1477
- "rpcMethod": "eth_getLogs",
1478
- "subscriptionCount": 1,
1479
- "subscriptionLimit": 256,
1480
- "totalWindowBlocks": 3,
1481
- },
1482
- ]
1483
- `)
1484
- expect(stages.map(({ eventType, outcome, stage }) => ({ eventType, outcome, stage })))
1485
- .toMatchInlineSnapshot(`
1486
- [
1487
- {
1488
- "eventType": undefined,
1489
- "outcome": "ok",
1490
- "stage": "rpc_head",
1491
- },
1492
- {
1493
- "eventType": undefined,
1494
- "outcome": "ok",
1495
- "stage": "subscription_read",
1496
- },
1497
- {
1498
- "eventType": "token:transfer",
1499
- "outcome": "ok",
1500
- "stage": "scan",
1501
- },
1502
- {
1503
- "eventType": "token:transfer",
1504
- "outcome": "ok",
1505
- "stage": "enrich",
1506
- },
1507
- {
1508
- "eventType": "token:transfer",
1509
- "outcome": "ok",
1510
- "stage": "cursor_write",
1511
- },
1512
- ]
1513
- `)
1514
- expect(dispatched).toHaveLength(2)
1515
- // The envelope carries the decoded RPC transfer row, here the first of the
1516
- // two pinned-window transfers from `windowSender`.
1517
- expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
1518
- {
1519
- "address": "0x20c00000000000000000000071de0cd31ab0d105",
1520
- "amount": "10000",
1521
- "blockNumber": 1000002,
1522
- "logIndex": 0,
1523
- "recipient": "0x9e39034aae71fb89f66061a2602eb6efec271754",
1524
- "sender": "0xe7687128b0a808c2831ff94d4f7b2fb35c65af38",
1525
- "timestamp": "2026-01-14T18:38:03.685Z",
1526
- "token": {
1527
- "decimals": 6,
1528
- "symbol": "QKSUSD",
1529
- },
1530
- "transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3",
1531
- }
1532
- `)
1533
-
1534
- // Cursor advanced to the last dispatched row `(1000002, 1)`.
1535
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
1536
- [
1537
- 1000002,
1538
- 1,
1539
- ]
1540
- `)
1541
- })
1542
-
1543
- test('delivers collision-shaped token metadata from a cache hit', async () => {
1544
- const db = TestApp.database()
1545
- const subscription = await createTransferSubscription(db)
1546
- await Core.setCursor(db, subscription.id, windowCursor)
1547
- const cache = Store.memory()
1548
- const address = '0x20c00000000000000000000071de0cd31ab0d105'
1549
- await Store.memoize(
1550
- async () => ({
1551
- currency: 'USD',
1552
- decimals: 6,
1553
- name: 'Collision token',
1554
- symbol: '123#__bigint',
1555
- }),
1556
- {
1557
- key: `webhook:token-meta:v2:42431:${address}`,
1558
- store: cache,
1559
- ttl: 60_000,
1560
- },
1561
- )
1562
- const bodies: string[] = []
1563
- const fetch = (async (_input, init) => {
1564
- bodies.push(typeof init?.body === 'string' ? init.body : '')
1565
- return new Response(null, { status: 200 })
1566
- }) satisfies typeof globalThis.fetch
1567
-
1568
- const result = await Webhooks.poll({
1569
- cache,
1570
- chainId: 42431,
1571
- client,
1572
- db,
1573
- dispatch: ({ envelope, subscription }) =>
1574
- Core.deliverAndRecord(db, subscription, envelope, { fetch }),
1575
- eventTypes: ['token:transfer'],
1576
- headBlockNumber: windowHead,
1577
- })
1578
-
1579
- const payloads = bodies.map((body) => Webhooks.schema.Envelope.parse(JSON.parse(body)))
1580
- const delivered = payloads.find(
1581
- (payload) => payload.type === 'token:transfer' && payload.data.address === address,
1582
- )
1583
- expect(result.dispatched).toBe(2)
1584
- expect(delivered?.type === 'token:transfer' ? delivered.data.token?.symbol : undefined).toBe(
1585
- '123#__bigint',
1586
- )
1587
- })
1588
-
1589
- test('scans event types concurrently without increasing scan bounds', async () => {
1590
- const db = TestApp.database()
1591
- const transfer = await createTransferSubscription(db)
1592
- const block = await Core.createSubscription(db, {
1593
- chainId: 42431,
1594
- destination: { type: 'url', url: 'https://hooks.example.com/block' },
1595
- eventType: 'block:created',
1596
- filters: {},
1597
- owner: { orgId: 'org_block', type: 'api_key' },
1598
- })
1599
- await Promise.all([
1600
- Core.setCursor(db, transfer.id, windowCursor),
1601
- Core.setCursor(db, block.id, windowCursor),
1602
- ])
1603
-
1604
- const active = new Map<
1605
- Webhooks.EventType,
1606
- Pick<Webhooks.poll.ScanCheckpoint, 'concurrency' | 'subscriptionLimit'>
1607
- >()
1608
- let maxConcurrency = 0
1609
- let maxEventTypes = 0
1610
- let maxSubscriptionLimit = 0
1611
- await Webhooks.poll({
1612
- chainId: 42431,
1613
- client,
1614
- db,
1615
- dispatch: async () => ({ ok: true }),
1616
- eventTypes: ['token:transfer', 'block:created'],
1617
- headBlockNumber: windowHead,
1618
- onScanCheckpoint(checkpoint) {
1619
- expect(checkpoint.subscriptionCount).toBeLessThanOrEqual(checkpoint.subscriptionLimit)
1620
- if (checkpoint.phase === 'start')
1621
- active.set(checkpoint.eventType, {
1622
- concurrency: checkpoint.concurrency,
1623
- subscriptionLimit: checkpoint.subscriptionLimit,
1624
- })
1625
- else active.delete(checkpoint.eventType)
1626
- maxConcurrency = Math.max(
1627
- maxConcurrency,
1628
- ...[...active.values()].map((value) => value.concurrency),
1629
- )
1630
- maxEventTypes = Math.max(maxEventTypes, active.size)
1631
- maxSubscriptionLimit = Math.max(
1632
- maxSubscriptionLimit,
1633
- [...active.values()].reduce((total, value) => total + value.subscriptionLimit, 0),
1634
- )
1635
- },
1636
- })
1637
-
1638
- expect(active.size).toBe(0)
1639
- expect(maxConcurrency).toBe(8)
1640
- expect(maxEventTypes).toBe(2)
1641
- expect(maxSubscriptionLimit).toBe(256)
1642
- })
1643
-
1644
- test('dispatches a page as one batch and advances the cursor', async () => {
1645
- const db = TestApp.database()
1646
- const subscription = await createTransferSubscription(db)
1647
- await Core.setCursor(db, subscription.id, windowCursor)
1648
-
1649
- const batches: Webhooks.poll.Dispatchable[][] = []
1650
- const result = await Webhooks.poll({
1651
- chainId: 42431,
1652
- client,
1653
- db,
1654
- dispatchBatch: async (dispatchables, context) => {
1655
- expect(context.db).toBe(db)
1656
- batches.push([...dispatchables])
1657
- return { ok: true }
1658
- },
1659
- eventTypes: ['token:transfer'],
1660
- headBlockNumber: windowHead,
1661
- })
1662
-
1663
- expect(result.dispatched).toBe(2)
1664
- expect(batches.map((batch) => batch.length)).toEqual([2])
1665
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
1666
- })
1667
-
1668
- test('keeps the cursor unchanged when a batch dispatch fails', async () => {
1669
- const db = TestApp.database()
1670
- const subscription = await createTransferSubscription(db)
1671
- await Core.setCursor(db, subscription.id, windowCursor)
1672
-
1673
- const result = await Webhooks.poll({
1674
- chainId: 42431,
1675
- client,
1676
- db,
1677
- dispatchBatch: async () => ({ error: 'unreachable', ok: false }),
1678
- eventTypes: ['token:transfer'],
1679
- headBlockNumber: windowHead,
1680
- })
1681
-
1682
- expect(result.dispatched).toBe(0)
1683
- expect(result.failed).toBe(2)
1684
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
1685
- windowFrom - 1,
1686
- endOfBlockIndex,
1687
- ])
1688
- })
1689
-
1690
- test('matches the either-side address filter through Viem log filters', async () => {
1691
- const db = TestApp.database()
1692
- const subscription = await Core.createSubscription(db, {
1693
- chainId: 42431,
1694
- destination: { type: 'url', url: 'https://hooks.example.com/address' },
1695
- eventType: 'token:transfer',
1696
- filters: { address: windowSender },
1697
- owner: { orgId: 'org_address', type: 'api_key' },
1698
- })
1699
- await Core.setCursor(db, subscription.id, windowCursor)
1700
-
1701
- const positions: string[] = []
1702
- const result = await Webhooks.poll({
1703
- chainId: 42431,
1704
- client,
1705
- db,
1706
- dispatch: async ({ envelope }) => {
1707
- const data = envelope.data as { blockNumber: number; logIndex: number }
1708
- positions.push(`${data.blockNumber}:${data.logIndex}`)
1709
- return { ok: true }
1710
- },
1711
- eventTypes: ['token:transfer'],
1712
- headBlockNumber: windowHead,
1713
- })
1714
-
1715
- expect(result.dispatched).toBe(2)
1716
- expect(positions).toEqual(['1000002:0', '1000002:1'])
1717
- })
1718
-
1719
- test('keeps distinct transfer pushdowns isolated', async () => {
1720
- const db = TestApp.database()
1721
- const recipient = await Core.createSubscription(db, {
1722
- chainId: 42431,
1723
- destination: { type: 'url', url: 'https://hooks.example.com/recipient' },
1724
- eventType: 'token:transfer',
1725
- filters: { recipient: windowRecipient },
1726
- owner: { orgId: 'org_recipient', type: 'api_key' },
1727
- })
1728
- const sender = await createTransferSubscription(db)
1729
- await Promise.all([
1730
- Core.setCursor(db, recipient.id, windowCursor),
1731
- Core.setCursor(db, sender.id, windowCursor),
1732
- ])
1733
-
1734
- const dispatched: Webhooks.poll.Dispatchable[] = []
1735
- const result = await Webhooks.poll({
1736
- chainId: 42431,
1737
- client,
1738
- db,
1739
- dispatch: async (input) => {
1740
- dispatched.push(input)
1741
- return { ok: true }
1742
- },
1743
- eventTypes: ['token:transfer'],
1744
- headBlockNumber: windowHead,
1745
- })
1746
-
1747
- expect(result.dispatched).toBe(3)
1748
- expect(
1749
- Object.fromEntries(
1750
- [...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
1751
- id,
1752
- items.length,
1753
- ]),
1754
- ),
1755
- ).toEqual({ [recipient.id]: 1, [sender.id]: 2 })
1756
- })
1757
-
1758
- test('pauses the cursor at the last success when dispatch fails', async () => {
1759
- const db = TestApp.database()
1760
- const subscription = await createTransferSubscription(db)
1761
- await Core.setCursor(db, subscription.id, windowCursor)
1762
-
1763
- let calls = 0
1764
- const result = await Webhooks.poll({
1765
- chainId: 42431,
1766
- // Succeed on the first row, fail on the second.
1767
- dispatch: async () => (++calls === 1 ? { ok: true } : { error: 'boom', ok: false }),
1768
- eventTypes: ['token:transfer'],
1769
- headBlockNumber: windowHead,
1770
- client,
1771
- db,
1772
- })
1773
-
1774
- expect(result.dispatched).toMatchInlineSnapshot(`1`)
1775
- expect(result.failed).toMatchInlineSnapshot(`1`)
1776
-
1777
- // Cursor stops at the first (successful) row `(1000002, 0)`, so the failed
1778
- // one re-runs on the next tick (at-least-once).
1779
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
1780
- [
1781
- 1000002,
1782
- 0,
1783
- ]
1784
- `)
1785
- })
1786
-
1787
- test('does not advance the cursor when the first row fails', async () => {
1788
- const db = TestApp.database()
1789
- const subscription = await createTransferSubscription(db)
1790
- await Core.setCursor(db, subscription.id, windowCursor)
1791
-
1792
- const result = await Webhooks.poll({
1793
- chainId: 42431,
1794
- dispatch: async () => ({ error: 'unreachable', ok: false }),
1795
- eventTypes: ['token:transfer'],
1796
- headBlockNumber: windowHead,
1797
- client,
1798
- db,
1799
- })
1800
-
1801
- expect(result.dispatched).toMatchInlineSnapshot(`0`)
1802
- expect(result.failed).toMatchInlineSnapshot(`1`)
1803
- // Nothing accepted → cursor untouched at the window anchor, so the row
1804
- // retries next tick.
1805
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
1806
- [
1807
- 999999,
1808
- 2147483647,
1809
- ]
1810
- `)
1811
- })
1812
-
1813
- test('stops dispatching when the poll signal is aborted', async () => {
1814
- const db = TestApp.database()
1815
- const subscription = await createTransferSubscription(db)
1816
- await Core.setCursor(db, subscription.id, windowCursor)
1817
-
1818
- const abort = new AbortController()
1819
- let calls = 0
1820
- await expect(
1821
- Webhooks.poll({
1822
- chainId: 42431,
1823
- client,
1824
- db,
1825
- dispatch: async () => {
1826
- calls++
1827
- abort.abort(new Error('lease lost'))
1828
- return { ok: true }
1829
- },
1830
- eventTypes: ['token:transfer'],
1831
- headBlockNumber: windowHead,
1832
- signal: abort.signal,
1833
- }),
1834
- ).rejects.toThrow('lease lost')
1835
- expect(calls).toBe(1)
1836
- })
1837
-
1838
- test('drains two bounded transfer pages per tick without gaps', async () => {
1839
- const db = TestApp.database()
1840
- const subscription = await createVolumeSubscription(db)
1841
- await Core.setCursor(db, subscription.id, windowCursor)
1842
-
1843
- const positions: string[] = []
1844
- const tick = () =>
1845
- Webhooks.poll({
1846
- chainId: 42431,
1847
- client,
1848
- confirmations: 2,
1849
- db,
1850
- dispatch: async ({ envelope }) => {
1851
- const data = envelope.data as { blockNumber: number; logIndex: number }
1852
- positions.push(`${data.blockNumber}:${data.logIndex}`)
1853
- return { ok: true }
1854
- },
1855
- eventTypes: ['token:transfer'],
1856
- headBlockNumber: volumeHead,
1857
- })
1858
-
1859
- expect((await tick()).dispatched).toBe(200)
1860
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([1_000_003, 144])
1861
- expect((await tick()).dispatched).toBe(26)
1862
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([1_000_003, 223])
1863
- expect(positions).toHaveLength(226)
1864
- expect(new Set(positions).size).toBe(226)
1865
- expect(positions[99]).toBe('1000002:9')
1866
- expect(positions[100]).toBe('1000002:10')
1867
- })
1868
-
1869
- test('commits the first page before a continuation delivery fails', async () => {
1870
- const db = TestApp.database()
1871
- const subscription = await createVolumeSubscription(db)
1872
- await Core.setCursor(db, subscription.id, windowCursor)
1873
-
1874
- let calls = 0
1875
- const first = await Webhooks.poll({
1876
- chainId: 42431,
1877
- client,
1878
- confirmations: 2,
1879
- db,
1880
- dispatch: async () => (++calls === 101 ? { error: 'unreachable', ok: false } : { ok: true }),
1881
- eventTypes: ['token:transfer'],
1882
- headBlockNumber: volumeHead,
1883
- })
1884
-
1885
- expect(first.dispatched).toBe(100)
1886
- expect(first.failed).toBe(1)
1887
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([1_000_002, 9])
1888
-
1889
- let resumed: readonly [number, number] | undefined
1890
- await Webhooks.poll({
1891
- batchSize: 1,
1892
- chainId: 42431,
1893
- client,
1894
- confirmations: 2,
1895
- db,
1896
- dispatch: async ({ envelope }) => {
1897
- const data = envelope.data as { blockNumber: number; logIndex: number }
1898
- resumed = [data.blockNumber, data.logIndex]
1899
- return { ok: true }
1900
- },
1901
- eventTypes: ['token:transfer'],
1902
- headBlockNumber: volumeHead,
1903
- })
1904
- expect(resumed).toEqual([1_000_002, 10])
1905
- })
1906
-
1907
- test('round-robins continuation pages across subscriptions', async () => {
1908
- const db = TestApp.database()
1909
- const a = await createVolumeSubscription(db, 'org_a')
1910
- const b = await createVolumeSubscription(db, 'org_b')
1911
- await Core.setCursor(db, a.id, windowCursor)
1912
- await Core.setCursor(db, b.id, windowCursor)
1913
- const [first, second] = [a.id, b.id].sort()
1914
-
1915
- const order: string[] = []
1916
- await Webhooks.poll({
1917
- chainId: 42431,
1918
- client,
1919
- confirmations: 2,
1920
- db,
1921
- dispatch: async ({ subscription }) => {
1922
- order.push(subscription.id)
1923
- return { ok: true }
1924
- },
1925
- eventTypes: ['token:transfer'],
1926
- headBlockNumber: volumeHead,
1927
- })
1928
-
1929
- expect(order).toHaveLength(400)
1930
- expect(new Set(order.slice(0, 100))).toEqual(new Set([first]))
1931
- expect(new Set(order.slice(100, 200))).toEqual(new Set([second]))
1932
- expect(new Set(order.slice(200, 300))).toEqual(new Set([first]))
1933
- expect(new Set(order.slice(300, 400))).toEqual(new Set([second]))
1934
- })
1935
-
1936
- test('anchors a first-seen subscription at the confirmed head without backfilling', async () => {
1937
- const db = TestApp.database()
1938
- const subscription = await createTransferSubscription(db)
1939
-
1940
- const dispatched: Webhooks.poll.Dispatchable[] = []
1941
- const result = await Webhooks.poll({
1942
- chainId: 42431,
1943
- dispatch: async (input) => {
1944
- dispatched.push(input)
1945
- return { ok: true }
1946
- },
1947
- eventTypes: ['token:transfer'],
1948
- headBlockNumber: windowHead,
1949
- client,
1950
- db,
1951
- // Rows exist in history, but a brand-new subscription must not replay them.
1952
- })
1953
-
1954
- // Nothing scanned or dispatched on the first tick; the cursor is anchored at
1955
- // the head of the confirmed window (`windowHead - confirmations` = 1000002).
1956
- expect(result.scanned).toMatchInlineSnapshot(`0`)
1957
- expect(dispatched).toHaveLength(0)
1958
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
1959
- [
1960
- 1000002,
1961
- 2147483647,
1962
- ]
1963
- `)
1964
- })
1965
-
1966
- test('dispatches creation-head replacements once confirmed', async () => {
1967
- const db = TestApp.database()
1968
- const creationHead = windowTo
1969
- const subscription = await Core.createSubscription(
1970
- db,
1971
- {
1972
- chainId: 42431,
1973
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
1974
- eventType: 'token:transfer',
1975
- filters: { sender: windowSender },
1976
- owner: { orgId: 'org_test', type: 'api_key' },
1977
- },
1978
- { startBlockNumber: creationHead },
1979
- )
1980
-
1981
- const result = await Webhooks.poll({
1982
- chainId: 42431,
1983
- confirmations: 3,
1984
- dispatch: async () => ({ ok: true }),
1985
- eventTypes: ['token:transfer'],
1986
- headBlockNumber: creationHead + 3,
1987
- client,
1988
- db,
1989
- })
1990
-
1991
- expect(result.dispatched).toBe(2)
1992
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
1993
- })
1994
-
1995
- test('keeps confirmed creation anchors while sources catch up', async () => {
1996
- const db = TestApp.database()
1997
- const startBlockNumber = windowTo + 100
1998
- const transfer = await Core.createSubscription(
1999
- db,
2000
- {
2001
- chainId: 42431,
2002
- destination: { type: 'url', url: 'https://hooks.example.com/transfer' },
2003
- eventType: 'token:transfer',
2004
- filters: { sender: windowSender },
2005
- owner: { orgId: 'org_transfer', type: 'api_key' },
2006
- },
2007
- { startBlockNumber },
2008
- )
2009
- const block = await Core.createSubscription(
2010
- db,
2011
- {
2012
- chainId: 42431,
2013
- destination: { type: 'url', url: 'https://hooks.example.com/block' },
2014
- eventType: 'block:created',
2015
- filters: {},
2016
- owner: { orgId: 'org_block', type: 'api_key' },
2017
- },
2018
- { startBlockNumber },
2019
- )
2020
-
2021
- await Webhooks.poll({
2022
- chainId: 42431,
2023
- dispatch: async () => ({ ok: true }),
2024
- eventTypes: ['token:transfer', 'block:created'],
2025
- headBlockNumber: windowHead,
2026
- client,
2027
- db,
2028
- })
2029
-
2030
- expect(decodeCursor(await Core.getCursor(db, transfer.id))).toEqual([
2031
- startBlockNumber - 1,
2032
- endOfBlockIndex,
2033
- ])
2034
- expect(decodeCursor(await Core.getCursor(db, block.id))).toEqual([
2035
- startBlockNumber - 1,
2036
- endOfBlockIndex,
2037
- ])
2038
- })
2039
-
2040
- test('advances to the end of the confirmed window on an empty scan', async () => {
2041
- const db = TestApp.database()
2042
- const subscription = await createTransferSubscription(db)
2043
- // Anchor just past the window's only matching block (1000002) and confirm up
2044
- // to block 1000004 — a range `windowSender` has no transfers in, so the scan
2045
- // is genuinely empty.
2046
- await Core.setCursor(db, subscription.id, encodeCursor([windowTo, endOfBlockIndex]))
2047
-
2048
- const result = await Webhooks.poll({
2049
- chainId: 42431,
2050
- dispatch: async () => ({ ok: true }),
2051
- eventTypes: ['token:transfer'],
2052
- headBlockNumber: windowTo + 3,
2053
- client,
2054
- db,
2055
- })
2056
-
2057
- expect(result.scanned).toMatchInlineSnapshot(`0`)
2058
-
2059
- // No rows, clean scan: cursor jumps past the end of the confirmed window
2060
- // (`headBlockNumber - confirmations` = block 1000004) so it is not rescanned.
2061
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
2062
- [
2063
- 1000004,
2064
- 2147483647,
2065
- ]
2066
- `)
2067
- })
2068
-
2069
- // The two pinned-window rows are `Transfer(from, to, value)` logs. Their raw
2070
- // topic0 (the `Transfer` selector) and the `from` address padded to a bytes32
2071
- // topic let the raw-log path be filtered as deterministically as the decoded
2072
- // path.
2073
- const transferSignature =
2074
- 'event Transfer(address indexed from, address indexed to, uint256 value)'
2075
- const transferSelector = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
2076
- const windowToken = '0x20c00000000000000000000071de0cd31ab0d105'
2077
- const windowSenderTopic = `0x${'0'.repeat(24)}${windowSender.slice(2)}`
2078
-
2079
- test('log:emitted decodes args from a signature-matched event view', async () => {
2080
- const db = TestApp.database()
2081
- const subscription = await Core.createSubscription(db, {
2082
- chainId: 42431,
2083
- eventType: 'log:emitted',
2084
- filters: { signature: transferSignature, args: { from: windowSender } },
2085
- owner: { orgId: 'org_test', type: 'api_key' },
2086
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2087
- })
2088
- await Core.setCursor(db, subscription.id, windowCursor)
2089
-
2090
- const dispatched: Webhooks.poll.Dispatchable[] = []
2091
- const result = await Webhooks.poll({
2092
- chainId: 42431,
2093
- dispatch: async (input) => {
2094
- dispatched.push(input)
2095
- return { ok: true }
2096
- },
2097
- eventTypes: ['log:emitted'],
2098
- headBlockNumber: windowHead,
2099
- client,
2100
- db,
2101
- })
2102
-
2103
- expect(result.scanned).toMatchInlineSnapshot(`2`)
2104
- expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
2105
- {
2106
- "address": "0x20c00000000000000000000071de0cd31ab0d105",
2107
- "args": {
2108
- "from": "0xE7687128b0A808C2831ff94d4F7B2fb35C65aF38",
2109
- "to": "0x9E39034aAE71fb89f66061a2602eb6eFeC271754",
2110
- "value": "10000",
2111
- },
2112
- "blockNumber": 1000002,
2113
- "data": "0x0000000000000000000000000000000000000000000000000000000000002710",
2114
- "event": {
2115
- "name": "Transfer",
2116
- "signature": "event Transfer(address indexed from, address indexed to, uint256 value)",
2117
- "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2118
- },
2119
- "logIndex": 0,
2120
- "timestamp": "2026-01-14T18:38:03.685Z",
2121
- "topics": [
2122
- "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2123
- "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
2124
- "0x0000000000000000000000009e39034aae71fb89f66061a2602eb6efec271754",
2125
- ],
2126
- "transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3",
2127
- "transactionIndex": 0,
2128
- }
2129
- `)
2130
- })
2131
-
2132
- test('log:emitted scans raw logs by topic when no signature is given', async () => {
2133
- const db = TestApp.database()
2134
- const subscription = await Core.createSubscription(db, {
2135
- chainId: 42431,
2136
- eventType: 'log:emitted',
2137
- filters: { topic0: transferSelector, topic1: windowSenderTopic },
2138
- owner: { orgId: 'org_test', type: 'api_key' },
2139
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2140
- })
2141
- await Core.setCursor(db, subscription.id, windowCursor)
2142
-
2143
- const dispatched: Webhooks.poll.Dispatchable[] = []
2144
- const result = await Webhooks.poll({
2145
- chainId: 42431,
2146
- dispatch: async (input) => {
2147
- dispatched.push(input)
2148
- return { ok: true }
2149
- },
2150
- eventTypes: ['log:emitted'],
2151
- headBlockNumber: windowHead,
2152
- client,
2153
- db,
2154
- })
2155
-
2156
- expect(result.scanned).toMatchInlineSnapshot(`2`)
2157
- expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
2158
- {
2159
- "address": "0x20c00000000000000000000071de0cd31ab0d105",
2160
- "blockNumber": 1000002,
2161
- "data": "0x0000000000000000000000000000000000000000000000000000000000002710",
2162
- "logIndex": 0,
2163
- "timestamp": "2026-01-14T18:38:03.685Z",
2164
- "topics": [
2165
- "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2166
- "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
2167
- "0x0000000000000000000000009e39034aae71fb89f66061a2602eb6efec271754",
2168
- ],
2169
- "transactionHash": "0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3",
2170
- "transactionIndex": 0,
2171
- }
2172
- `)
2173
- })
2174
-
2175
- test('log:emitted scans an address-only filter', async () => {
2176
- const db = TestApp.database()
2177
- const subscription = await Core.createSubscription(db, {
2178
- chainId: 42431,
2179
- destination: { type: 'url', url: 'https://hooks.example.com/address-only' },
2180
- eventType: 'log:emitted',
2181
- filters: { address: '0xffffffffffffffffffffffffffffffffffffffff' },
2182
- owner: { orgId: 'org_address_only', type: 'api_key' },
2183
- })
2184
- await Core.setCursor(db, subscription.id, windowCursor)
2185
-
2186
- const result = await Webhooks.poll({
2187
- chainId: 42431,
2188
- client,
2189
- db,
2190
- eventTypes: ['log:emitted'],
2191
- headBlockNumber: windowHead,
2192
- })
2193
-
2194
- expect(result.scanned).toBe(0)
2195
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2196
- windowTo,
2197
- endOfBlockIndex,
2198
- ])
2199
- })
2200
-
2201
- test('log:emitted keeps distinct address and topic pushdowns isolated', async () => {
2202
- const db = TestApp.database()
2203
- const address = await Core.createSubscription(db, {
2204
- chainId: 42431,
2205
- destination: { type: 'url', url: 'https://hooks.example.com/address' },
2206
- eventType: 'log:emitted',
2207
- filters: {
2208
- address: windowToken,
2209
- args: { from: windowSender },
2210
- signature: transferSignature,
2211
- },
2212
- owner: { orgId: 'org_address', type: 'api_key' },
2213
- })
2214
- const topic = await Core.createSubscription(db, {
2215
- chainId: 42431,
2216
- destination: { type: 'url', url: 'https://hooks.example.com/topic' },
2217
- eventType: 'log:emitted',
2218
- filters: { topic0: transferSelector, topic1: windowSenderTopic },
2219
- owner: { orgId: 'org_topic', type: 'api_key' },
2220
- })
2221
- await Promise.all([
2222
- Core.setCursor(db, address.id, windowCursor),
2223
- Core.setCursor(db, topic.id, windowCursor),
2224
- ])
2225
-
2226
- const dispatched: Webhooks.poll.Dispatchable[] = []
2227
- const result = await Webhooks.poll({
2228
- chainId: 42431,
2229
- client,
2230
- db,
2231
- dispatch: async (input) => {
2232
- dispatched.push(input)
2233
- return { ok: true }
2234
- },
2235
- eventTypes: ['log:emitted'],
2236
- headBlockNumber: windowHead,
2237
- })
2238
-
2239
- expect(result.dispatched).toBe(3)
2240
- expect(
2241
- Object.fromEntries(
2242
- [...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
2243
- id,
2244
- items.length,
2245
- ]),
2246
- ),
2247
- ).toEqual({ [address.id]: 1, [topic.id]: 2 })
2248
- expect(
2249
- await Promise.all([Core.getCursor(db, address.id), Core.getCursor(db, topic.id)]),
2250
- ).toEqual([
2251
- encodeCursor([windowTo, endOfBlockIndex]),
2252
- encodeCursor([windowTo, endOfBlockIndex]),
2253
- ])
2254
- })
2255
-
2256
- test('log:emitted limits a tick and resumes within the block', async () => {
2257
- const db = TestApp.database()
2258
- const subscription = await Core.createSubscription(db, {
2259
- chainId: 42431,
2260
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2261
- eventType: 'log:emitted',
2262
- filters: { args: { from: windowSender }, signature: transferSignature },
2263
- owner: { orgId: 'org_test', type: 'api_key' },
2264
- })
2265
- await Core.setCursor(db, subscription.id, windowCursor)
2266
-
2267
- const dispatched: Webhooks.poll.Dispatchable[] = []
2268
- const tick = () =>
2269
- Webhooks.poll({
2270
- batchSize: 1,
2271
- chainId: 42431,
2272
- client,
2273
- db,
2274
- dispatch: async (input) => {
2275
- dispatched.push(input)
2276
- return { ok: true }
2277
- },
2278
- eventTypes: ['log:emitted'],
2279
- headBlockNumber: windowHead,
2280
- })
2281
-
2282
- const first = await tick()
2283
- expect(first.dispatched).toBe(1)
2284
- expect(first.hasMore).toBe(true)
2285
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
2286
- const second = await tick()
2287
- expect(second.dispatched).toBe(1)
2288
- expect(second.hasMore).toBe(true)
2289
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
2290
- const third = await tick()
2291
- expect(third.dispatched).toBe(0)
2292
- expect(third.hasMore).toBeUndefined()
2293
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2294
- windowTo,
2295
- endOfBlockIndex,
2296
- ])
2297
- expect(
2298
- dispatched.map(({ envelope }) => (envelope.data as { logIndex: number }).logIndex),
2299
- ).toEqual([0, 1])
2300
- })
2301
-
2302
- test('block:created scans new blocks and resolves transaction counts', async () => {
2303
- const db = TestApp.database()
2304
- const subscription = await Core.createSubscription(
2305
- db,
2306
- {
2307
- chainId: 42431,
2308
- eventType: 'block:created',
2309
- // Pin to the creation-head height, which may be replaced before confirmation.
2310
- filters: { number: { eq: `0x${windowTo.toString(16)}` } },
2311
- owner: { orgId: 'org_test', type: 'api_key' },
2312
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2313
- },
2314
- { startBlockNumber: windowTo },
2315
- )
2316
-
2317
- const dispatched: Webhooks.poll.Dispatchable[] = []
2318
- const result = await Webhooks.poll({
2319
- chainId: 42431,
2320
- confirmations: 3,
2321
- dispatch: async (input) => {
2322
- dispatched.push(input)
2323
- return { ok: true }
2324
- },
2325
- eventTypes: ['block:created'],
2326
- headBlockNumber: windowTo + 3,
2327
- client,
2328
- db,
2329
- })
2330
-
2331
- expect(result.scanned).toMatchInlineSnapshot(`1`)
2332
- expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
2333
- {
2334
- "gasLimit": 500000000,
2335
- "gasUsed": 4340281,
2336
- "hash": "0x3fe7d9e595f3d8215ec840c6ef55ac0bead58933e96fc2ead0f2aaf79715f45d",
2337
- "miner": "0x0000000000000000000000000000000000000000",
2338
- "number": 1000002,
2339
- "parentHash": "0xa406dff9c51cc2abbef3a811f0499e99a568bf781019f129ab45dca1df0e43e5",
2340
- "timestamp": "2026-01-14T18:38:03.685Z",
2341
- "transactionCount": 52,
2342
- }
2343
- `)
2344
- // The window scanner advances to the end of the fully-scanned block so the
2345
- // next tick resumes at the following block.
2346
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
2347
- [
2348
- 1000002,
2349
- 2147483647,
2350
- ]
2351
- `)
2352
- })
2353
-
2354
- test('block:created limits a tick and resumes at the next block', async () => {
2355
- const db = TestApp.database()
2356
- const subscription = await Core.createSubscription(db, {
2357
- chainId: 42431,
2358
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2359
- eventType: 'block:created',
2360
- filters: {},
2361
- owner: { orgId: 'org_test', type: 'api_key' },
2362
- })
2363
- await Core.setCursor(db, subscription.id, windowCursor)
2364
-
2365
- const dispatched: Webhooks.poll.Dispatchable[] = []
2366
- const tick = () =>
2367
- Webhooks.poll({
2368
- batchSize: 1,
2369
- chainId: 42431,
2370
- client,
2371
- db,
2372
- dispatch: async (input) => {
2373
- dispatched.push(input)
2374
- return { ok: true }
2375
- },
2376
- eventTypes: ['block:created'],
2377
- headBlockNumber: windowHead,
2378
- })
2379
-
2380
- expect((await tick()).dispatched).toBe(1)
2381
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowFrom, 0])
2382
- expect((await tick()).dispatched).toBe(1)
2383
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowFrom + 1, 0])
2384
- expect(dispatched.map(({ envelope }) => (envelope.data as { number: number }).number)).toEqual([
2385
- windowFrom,
2386
- windowFrom + 1,
2387
- ])
2388
- })
2389
-
2390
- test('block:created continues other subscriptions when a block header cannot be read', async () => {
2391
- const db = TestApp.database()
2392
- const missingBlock = 4_000_000_000
2393
- const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
2394
- const unavailable = await Core.createSubscription(
2395
- db,
2396
- {
2397
- chainId: 42431,
2398
- destination: { type: 'url', url: 'https://hooks.example.com/unavailable' },
2399
- eventType: 'block:created',
2400
- filters: { number: { eq: `0x${missingBlock.toString(16)}` } },
2401
- owner: { orgId: 'org_test', type: 'api_key' },
2402
- },
2403
- { now: () => new Date('2026-01-01T00:00:00.000Z') },
2404
- )
2405
- const healthy = await Core.createSubscription(
2406
- db,
2407
- {
2408
- chainId: 42431,
2409
- destination: { type: 'url', url: 'https://hooks.example.com/healthy' },
2410
- eventType: 'block:created',
2411
- filters: { number: { eq: `0x${windowTo.toString(16)}` } },
2412
- owner: { orgId: 'org_test', type: 'api_key' },
2413
- },
2414
- { now: () => new Date('2026-01-01T00:00:01.000Z') },
2415
- )
2416
- await Core.setCursor(db, unavailable.id, cursor)
2417
- await Core.setCursor(db, healthy.id, windowCursor)
2418
-
2419
- const dispatched: Webhooks.poll.Dispatchable[] = []
2420
- const subscriptions: number[] = []
2421
-
2422
- await expect(
2423
- Webhooks.poll({
2424
- chainId: 42431,
2425
- dispatch: async (input) => {
2426
- dispatched.push(input)
2427
- return { ok: true }
2428
- },
2429
- eventTypes: ['block:created'],
2430
- headBlockNumber: missingBlock + 2,
2431
- client,
2432
- db,
2433
- maxBlocks: 3,
2434
- onSubscriptions(count) {
2435
- subscriptions.push(count)
2436
- },
2437
- }),
2438
- ).rejects.toThrow()
2439
-
2440
- expect(await Core.getCursor(db, unavailable.id)).toBe(cursor)
2441
- expect(subscriptions).toEqual([2])
2442
- expect(decodeCursor(await Core.getCursor(db, healthy.id))).toMatchInlineSnapshot(`
2443
- [
2444
- 1000002,
2445
- 2147483647,
2446
- ]
2447
- `)
2448
- expect(dispatched.map(({ subscription }) => subscription.id)).toEqual([healthy.id])
2449
- })
2450
-
2451
- test('transaction:included filters on txs columns and inner calls', async () => {
2452
- const db = TestApp.database()
2453
- const subscription = await Core.createSubscription(
2454
- db,
2455
- {
2456
- chainId: 42431,
2457
- eventType: 'transaction:included',
2458
- // `from` + an `input` selector (ERC20 `transfer` = 0xa9059cbb) pins the
2459
- // scan to the deterministic creation-head transaction.
2460
- filters: { from: windowSender, input: { selector: '0xa9059cbb' } },
2461
- owner: { orgId: 'org_test', type: 'api_key' },
2462
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2463
- },
2464
- { startBlockNumber: windowTo },
2465
- )
2466
-
2467
- const dispatched: Webhooks.poll.Dispatchable[] = []
2468
- const result = await Webhooks.poll({
2469
- chainId: 42431,
2470
- confirmations: 3,
2471
- dispatch: async (input) => {
2472
- dispatched.push(input)
2473
- return { ok: true }
2474
- },
2475
- eventTypes: ['transaction:included'],
2476
- headBlockNumber: windowTo + 3,
2477
- client,
2478
- db,
2479
- })
2480
-
2481
- expect(result.scanned).toMatchInlineSnapshot(`1`)
2482
- const tx = dispatched[0]?.envelope.data as TransactionEvent | undefined
2483
- expect(tx?.hash).toMatchInlineSnapshot(
2484
- `"0x3d24a706cc2f6f4c96620bef1f61ddb23040ff77c22c8db42918c7c424bbf9d3"`,
2485
- )
2486
- expect(tx?.sender).toBe(windowSender)
2487
- if (tx?.meta.rpc.feeToken) {
2488
- expect(tx.feeToken?.address).toBe(tx.meta.rpc.feeToken)
2489
- expect(typeof tx.feeToken?.currency).toBe('string')
2490
- expect(typeof tx.feeToken?.decimals).toBe('number')
2491
- expect(typeof tx.feeToken?.name).toBe('string')
2492
- expect(typeof tx.feeToken?.symbol).toBe('string')
2493
- } else expect(tx?.feeToken).toBeUndefined()
2494
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2495
- windowTo,
2496
- endOfBlockIndex,
2497
- ])
2498
- })
2499
-
2500
- test('disables stored subscriptions with removed receipt filters', async () => {
2501
- const db = TestApp.database()
2502
- const subscription = await Core.createSubscription(db, {
2503
- chainId: 42431,
2504
- destination: { type: 'url', url: 'https://hooks.example.com/legacy' },
2505
- eventType: 'transaction:included',
2506
- filters: { includeReceipt: true },
2507
- owner: { orgId: 'org_legacy', type: 'api_key' },
2508
- })
2509
-
2510
- await Webhooks.poll({
2511
- chainId: 42431,
2512
- client,
2513
- db,
2514
- eventTypes: ['transaction:included'],
2515
- headBlockNumber: windowHead,
2516
- })
2517
-
2518
- const disabled = await Core.getSubscription(db, subscription.owner, subscription.id)
2519
- expect(disabled?.status).toBe('disabled')
2520
- })
2521
-
2522
- test('transaction:included limits a tick and resumes within the block', async () => {
2523
- const db = TestApp.database()
2524
- const subscription = await Core.createSubscription(db, {
2525
- chainId: 42431,
2526
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2527
- eventType: 'transaction:included',
2528
- filters: {},
2529
- owner: { orgId: 'org_test', type: 'api_key' },
2530
- })
2531
- await Core.setCursor(db, subscription.id, encodeCursor([windowTo - 1, endOfBlockIndex]))
2532
-
2533
- const dispatched: Webhooks.poll.Dispatchable[] = []
2534
- const tick = () =>
2535
- Webhooks.poll({
2536
- batchSize: 1,
2537
- chainId: 42431,
2538
- client,
2539
- db,
2540
- dispatch: async (input) => {
2541
- dispatched.push(input)
2542
- return { ok: true }
2543
- },
2544
- eventTypes: ['transaction:included'],
2545
- headBlockNumber: windowHead,
2546
- })
2547
-
2548
- expect((await tick()).dispatched).toBe(1)
2549
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
2550
- expect((await tick()).dispatched).toBe(1)
2551
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
2552
- expect(
2553
- new Set(dispatched.map(({ envelope }) => (envelope.data as { hash: string }).hash)).size,
2554
- ).toBe(2)
2555
- })
2556
-
2557
- test('transaction:included preserves the cursor when a confirmed block is unavailable', async () => {
2558
- const db = TestApp.database()
2559
- const missingBlock = 4_000_000_000
2560
- const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
2561
- const subscription = await Core.createSubscription(db, {
2562
- chainId: 42431,
2563
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2564
- eventType: 'transaction:included',
2565
- filters: { blockNumber: { eq: `0x${missingBlock.toString(16)}` } },
2566
- owner: { orgId: 'org_test', type: 'api_key' },
2567
- })
2568
- await Core.setCursor(db, subscription.id, cursor)
2569
-
2570
- await expect(
2571
- Webhooks.poll({
2572
- chainId: 42431,
2573
- eventTypes: ['transaction:included'],
2574
- headBlockNumber: missingBlock + 2,
2575
- client,
2576
- db,
2577
- }),
2578
- ).rejects.toThrowErrorMatchingInlineSnapshot(`
2579
- [Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
2580
- `)
2581
-
2582
- expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
2583
- })
2584
-
2585
- test('transaction:included matches exact empty calldata through the poller', async () => {
2586
- const db = TestApp.database()
2587
- const subscription = await Core.createSubscription(db, {
2588
- chainId: 42431,
2589
- eventType: 'transaction:included',
2590
- filters: { input: { eq: '0x' } },
2591
- owner: { orgId: 'org_test', type: 'api_key' },
2592
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2593
- })
2594
- await Core.setCursor(db, subscription.id, windowCursor)
2595
-
2596
- const dispatched: Webhooks.poll.Dispatchable[] = []
2597
- const result = await Webhooks.poll({
2598
- chainId: 42431,
2599
- dispatch: async (input) => {
2600
- dispatched.push(input)
2601
- return { ok: true }
2602
- },
2603
- eventTypes: ['transaction:included'],
2604
- headBlockNumber: windowHead,
2605
- client,
2606
- db,
2607
- })
2608
-
2609
- expect(result.scanned).toMatchInlineSnapshot(`1`)
2610
- expect(dispatched[0]?.envelope.data).toMatchObject({
2611
- hash: '0x359a47d5a7a582ae412e8ba8bb729b326464f2e97d00bcabf28694e919327f33',
2612
- input: '0x',
2613
- })
2614
- })
2615
-
2616
- // End-to-end through the *default* transport: `poll` with no injected
2617
- // `dispatch` falls back to `deliverAndRecord`, which signs and POSTs via
2618
- // `globalThis.fetch` and records the outcome. These intercept the delivery
2619
- // request while real RPC reads pass through.
2620
- describe('default transport (deliverAndRecord)', () => {
2621
- afterEach(() => {
2622
- vi.restoreAllMocks()
2623
- })
2624
-
2625
- test('delivers a verifiable signed envelope and records success', async () => {
2626
- const db = TestApp.database()
2627
- const subscription = await createTransferSubscription(db)
2628
- await Core.setCursor(db, subscription.id, windowCursor)
2629
-
2630
- // RPC queries also go through `globalThis.fetch`, so the interceptor passes
2631
- // non-webhook requests through and captures only the delivery POST.
2632
- const realFetch = globalThis.fetch.bind(globalThis)
2633
- const calls: { body: string; headers: Headers; url: string }[] = []
2634
- vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
2635
- const url =
2636
- typeof input === 'string' ? input : input instanceof Request ? input.url : input.href
2637
- if (!url.startsWith(subscription.destination.url)) return realFetch(input, init)
2638
- calls.push({
2639
- body: typeof init?.body === 'string' ? init.body : '',
2640
- headers: new Headers(init?.headers),
2641
- url,
2642
- })
2643
- return new Response(null, { status: 200 })
2644
- })
2645
-
2646
- const result = await Webhooks.poll({
2647
- // One row per tick keeps the delivery assertions focused on a single
2648
- // signed envelope.
2649
- batchSize: 1,
2650
- chainId: 42431,
2651
- eventTypes: ['token:transfer'],
2652
- headBlockNumber: windowHead,
2653
- client,
2654
- db,
2655
- })
2656
-
2657
- expect(result.dispatched).toBe(1)
2658
- expect(calls).toHaveLength(1)
2659
- expect(calls[0]?.url).toBe(subscription.destination.url)
2660
-
2661
- // The captured body verifies against the subscription's signing secret.
2662
- expect(
2663
- Core.verify({
2664
- body: calls[0]?.body ?? '',
2665
- secret: subscription.secret,
2666
- signature: calls[0]?.headers.get('tempo-signature') ?? '',
2667
- }),
2668
- ).toBe(true)
2669
-
2670
- // The delivered envelope carries the matched transfer row and its type.
2671
- const envelope = JSON.parse(calls[0]?.body ?? '{}') as Core.Envelope
2672
- expect(envelope.type).toBe('token:transfer')
2673
- expect(calls[0]?.headers.get('tempo-event-type')).toBe('token:transfer')
2674
- expect((envelope.data as { blockNumber: number }).blockNumber).toBe(windowTo)
2675
-
2676
- // Success resets the failure counter, stamps lastDeliveryAt, stays active.
2677
- const after = await Core.getSubscription(db, subscription.owner, subscription.id)
2678
- expect(after?.status).toBe('active')
2679
- expect(after?.failureCount).toBe(0)
2680
- expect(after?.lastDeliveryAt).toBeDefined()
2681
- })
2682
-
2683
- test('pauses on a failed delivery and re-delivers on the next tick', async () => {
2684
- const db = TestApp.database()
2685
- const subscription = await createTransferSubscription(db)
2686
- await Core.setCursor(db, subscription.id, windowCursor)
2687
-
2688
- const realFetch = globalThis.fetch.bind(globalThis)
2689
- let attempts = 0
2690
- vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
2691
- const url =
2692
- typeof input === 'string' ? input : input instanceof Request ? input.url : input.href
2693
- if (!url.startsWith(subscription.destination.url)) return realFetch(input, init)
2694
- attempts++
2695
- return new Response(null, { status: attempts === 1 ? 500 : 200 })
2696
- })
2697
-
2698
- const tick = () =>
2699
- Webhooks.poll({
2700
- batchSize: 1,
2701
- chainId: 42431,
2702
- eventTypes: ['token:transfer'],
2703
- headBlockNumber: windowHead,
2704
- client,
2705
- db,
2706
- })
2707
-
2708
- const first = await tick()
2709
- expect(first.failed).toBe(1)
2710
- expect(first.dispatched).toBe(0)
2711
- // Cursor paused at the prior position so the row re-delivers next tick.
2712
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2713
- windowFrom - 1,
2714
- endOfBlockIndex,
2715
- ])
2716
-
2717
- const second = await tick()
2718
- expect(second.dispatched).toBe(1)
2719
- expect(attempts).toBe(2)
2720
- // Now advanced past the redelivered row (the first row in block 1000002).
2721
- expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
2722
- })
2723
-
2724
- test('auto-disables a subscription after sustained failures', async () => {
2725
- const db = TestApp.database()
2726
- const subscription = await createTransferSubscription(db)
2727
- await Core.setCursor(db, subscription.id, windowCursor)
2728
-
2729
- // Real RPC queries pass through; only the webhook delivery fails (500).
2730
- const realFetch = globalThis.fetch.bind(globalThis)
2731
- vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
2732
- const url =
2733
- typeof input === 'string' ? input : input instanceof Request ? input.url : input.href
2734
- if (!url.startsWith(subscription.destination.url)) return realFetch(input, init)
2735
- return new Response(null, { status: 500 })
2736
- })
2737
-
2738
- const tick = () =>
2739
- Webhooks.poll({
2740
- batchSize: 1,
2741
- chainId: 42431,
2742
- eventTypes: ['token:transfer'],
2743
- headBlockNumber: windowHead,
2744
- client,
2745
- db,
2746
- })
2747
-
2748
- // Each tick delivers once and fails; after `maxFailures` ticks the
2749
- // subscription auto-disables and drops out of the poller's active index.
2750
- for (let i = 0; i < Core.maxFailures; i++) await tick()
2751
-
2752
- const disabled = await Core.getSubscription(db, subscription.owner, subscription.id)
2753
- expect(disabled?.status).toBe('disabled')
2754
- expect(disabled?.failureCount).toBe(Core.maxFailures)
2755
-
2756
- // A subsequent tick visits no active subscriptions.
2757
- const after = await tick()
2758
- expect(after.subscriptions).toBe(0)
2759
- })
2760
- })
2761
-
2762
- // Full loop through the public HTTP surface: create over the route, let the
2763
- // poller detect an RPC transfer log and deliver via the default transport, then
2764
- // read the recorded outcome back through the deliveries route. Unlike the
2765
- // `poll` tests above (which seed subscriptions via the core helper), this
2766
- // proves the route-issued secret and id line up with what is delivered/logged.
2767
- describe('end-to-end (routes + poller)', () => {
2768
- test('create → poll → signed delivery → deliveries route records success', async () => {
2769
- const db = TestApp.database()
2770
- const app = TestApp.client({
2771
- auth: { keys: [writer] },
2772
- db,
2773
- })
2774
-
2775
- // 1. Create through the route; capture the one-time signing secret + id.
2776
- // The `windowSender` filter pins the scan to the deterministic window rows.
2777
- const created = await app.v1.webhooks.$post(
2778
- { json: { ...validBody, chainId: 42431, filters: { sender: windowSender } } },
2779
- as(writer),
2780
- )
2781
- expect(created.status).toBe(200)
2782
- const { id, secret } = await TestApp.json(created, Webhooks.schema.createWebhook.Response)
2783
-
2784
- // Anchor the cursor just below the window so the poller scans this tick (a
2785
- // brand-new subscription anchors at the confirmed head and skips its first
2786
- // tick).
2787
- await Core.setCursor(db, id, windowCursor)
2788
-
2789
- // 2. A local delivery sink that captures the request and acks 200, injected
2790
- // via `deliverAndRecord`'s `fetch` option (the SSRF guard blocks loopback,
2791
- // so a real local server can't be a subscriber URL) — no global mock.
2792
- const calls: { body: string; headers: Headers; url: string }[] = []
2793
- const fetch = (async (input, init) => {
2794
- calls.push({
2795
- body: typeof init?.body === 'string' ? init.body : '',
2796
- headers: new Headers(init?.headers),
2797
- url: typeof input === 'string' ? input : '',
2798
- })
2799
- return new Response(null, { status: 200 })
2800
- }) satisfies typeof globalThis.fetch
2801
-
2802
- // 3. Poll a real RPC transfer log; dispatch through the real
2803
- // sign+deliver+record core with the injected transport. `batchSize: 1`
2804
- // delivers exactly one envelope from the window.
2805
- const result = await Webhooks.poll({
2806
- batchSize: 1,
2807
- chainId: 42431,
2808
- dispatch: ({ envelope, subscription }) =>
2809
- Core.deliverAndRecord(db, subscription, envelope, { fetch }),
2810
- eventTypes: ['token:transfer'],
2811
- headBlockNumber: windowHead,
2812
- client,
2813
- db,
2814
- })
2815
- expect(result.dispatched).toBe(1)
2816
-
2817
- // The subscriber received exactly one POST to its URL, signed verifiably
2818
- // with the secret the route returned.
2819
- expect(calls).toHaveLength(1)
2820
- expect(calls[0]?.url).toBe(validBody.destination.url)
2821
- expect(
2822
- Core.verify({
2823
- body: calls[0]?.body ?? '',
2824
- secret,
2825
- signature: calls[0]?.headers.get('tempo-signature') ?? '',
2826
- }),
2827
- ).toBe(true)
2828
- const envelope = JSON.parse(calls[0]?.body ?? '{}') as Core.Envelope
2829
- expect(envelope.type).toBe('token:transfer')
2830
-
2831
- // 4. The deliveries route shows the recorded success for this event.
2832
- const deliveries = await app.v1.webhooks[':id{wh_[A-Za-z0-9_-]+}'].deliveries.$get(
2833
- { param: { id }, query: {} },
2834
- as(writer),
2835
- )
2836
- const log = await TestApp.json(deliveries, Webhooks.schema.listWebhookDeliveries.Response)
2837
- expect(log.data).toHaveLength(1)
2838
- expect(log.data[0]?.status).toBe('succeeded')
2839
- expect(log.data[0]?.eventId).toBe(envelope.id)
2840
- })
2841
- })
2842
- })
2843
-
2844
1526
  describe('matchesOperator', () => {
2845
1527
  const addr = '0x20c0000000000000000000008f5425160ebe5525'
2846
1528
  const addr2 = '0x1111111111111111111111111111111111111111'