tapimo 0.7.2 → 0.9.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 (300) hide show
  1. package/README.md +10 -7
  2. package/dist/ApiKey.d.ts +3 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -0
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +7 -3
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +9 -2
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +20117 -19183
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +61 -4
  13. package/dist/App.js.map +1 -1
  14. package/dist/admin/App.d.ts +84 -74
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +14 -1
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -2
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +2 -0
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/apps/verified-tokens.d.ts +11 -11
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +67 -67
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts +2 -0
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +3 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/data/App.d.ts +11696 -13937
  31. package/dist/apps/data/App.d.ts.map +1 -1
  32. package/dist/apps/data/App.js +4 -8
  33. package/dist/apps/data/App.js.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +271 -271
  35. package/dist/apps/data/routes/addresses.d.ts +398 -167
  36. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  37. package/dist/apps/data/routes/balances.d.ts +278 -8
  38. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  39. package/dist/apps/data/routes/balances.js +114 -2
  40. package/dist/apps/data/routes/balances.js.map +1 -1
  41. package/dist/apps/data/routes/blocks.d.ts +123 -123
  42. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  43. package/dist/apps/data/routes/earn.d.ts +498 -6
  44. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  45. package/dist/apps/data/routes/earn.js +876 -4
  46. package/dist/apps/data/routes/earn.js.map +1 -1
  47. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  48. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  49. package/dist/apps/data/routes/indexer.d.ts +6 -6
  50. package/dist/apps/data/routes/indexer.js +4 -1
  51. package/dist/apps/data/routes/indexer.js.map +1 -1
  52. package/dist/apps/data/routes/receipts.d.ts +235 -235
  53. package/dist/apps/data/routes/rpc.d.ts +31 -31
  54. package/dist/apps/data/routes/rpc.js +4 -1
  55. package/dist/apps/data/routes/rpc.js.map +1 -1
  56. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  57. package/dist/apps/data/routes/tokens.d.ts +449 -449
  58. package/dist/apps/data/routes/transactions.d.ts +2 -2
  59. package/dist/apps/data/routes/transfers.d.ts +81 -81
  60. package/dist/apps/data/routes/valuation.d.ts +1 -1
  61. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  62. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  63. package/dist/apps/data/routes/zones.d.ts +14 -14
  64. package/dist/apps/funding/Access.d.ts +16 -0
  65. package/dist/apps/funding/Access.d.ts.map +1 -0
  66. package/dist/apps/funding/Access.js +22 -0
  67. package/dist/apps/funding/Access.js.map +1 -0
  68. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
  69. package/dist/apps/funding/App.d.ts.map +1 -0
  70. package/dist/apps/funding/App.js +61 -0
  71. package/dist/apps/funding/App.js.map +1 -0
  72. package/dist/apps/funding/Inventory.d.ts +5 -0
  73. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  74. package/dist/apps/funding/Inventory.js +25 -0
  75. package/dist/apps/funding/Inventory.js.map +1 -0
  76. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  77. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  78. package/dist/apps/funding/ProviderAttempts.js +24 -0
  79. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  80. package/dist/apps/funding/index.d.ts +2 -0
  81. package/dist/apps/funding/index.d.ts.map +1 -0
  82. package/dist/apps/funding/index.js +2 -0
  83. package/dist/apps/funding/index.js.map +1 -0
  84. package/dist/apps/funding/routes/chains.d.ts +169 -0
  85. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/chains.js +80 -0
  87. package/dist/apps/funding/routes/chains.js.map +1 -0
  88. package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/deposit-addresses.js +778 -0
  91. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  92. package/dist/apps/funding/routes/providers.d.ts +91 -0
  93. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/providers.js +56 -0
  95. package/dist/apps/funding/routes/providers.js.map +1 -0
  96. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  97. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/quotes.js +188 -0
  99. package/dist/apps/funding/routes/quotes.js.map +1 -0
  100. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  101. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  102. package/dist/apps/funding/routes/transfers.js +614 -0
  103. package/dist/apps/funding/routes/transfers.js.map +1 -0
  104. package/dist/apps/index.d.ts +1 -0
  105. package/dist/apps/index.d.ts.map +1 -1
  106. package/dist/apps/index.js +1 -0
  107. package/dist/apps/index.js.map +1 -1
  108. package/dist/apps/management/App.d.ts +430 -165
  109. package/dist/apps/management/App.d.ts.map +1 -1
  110. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  111. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  112. package/dist/apps/management/routes/api-keys.js +75 -1
  113. package/dist/apps/management/routes/api-keys.js.map +1 -1
  114. package/dist/apps/management/routes/billing.d.ts +6 -6
  115. package/dist/apps/management/routes/faucet.d.ts +35 -35
  116. package/dist/apps/management/routes/invitations.d.ts +122 -122
  117. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  118. package/dist/apps/management/routes/members.d.ts +69 -69
  119. package/dist/apps/management/routes/orgs.d.ts +99 -99
  120. package/dist/apps/management/routes/projects.d.ts +25 -25
  121. package/dist/apps/management/routes/scopes.d.ts +1 -1
  122. package/dist/apps/management/routes/usage.d.ts +72 -72
  123. package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
  124. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  125. package/dist/apps/mcp.d.ts +2 -3
  126. package/dist/apps/mcp.d.ts.map +1 -1
  127. package/dist/apps/mcp.js +16 -14
  128. package/dist/apps/mcp.js.map +1 -1
  129. package/dist/apps/metadata.d.ts +4 -0
  130. package/dist/apps/metadata.d.ts.map +1 -1
  131. package/dist/apps/metadata.js +10 -0
  132. package/dist/apps/metadata.js.map +1 -1
  133. package/dist/apps/mpp/App.d.ts +44 -44
  134. package/dist/cloudflare.d.ts +50 -0
  135. package/dist/cloudflare.d.ts.map +1 -1
  136. package/dist/cloudflare.js +178 -0
  137. package/dist/cloudflare.js.map +1 -1
  138. package/dist/db/Db.d.ts +2 -0
  139. package/dist/db/Db.d.ts.map +1 -1
  140. package/dist/db/Db.js +45 -0
  141. package/dist/db/Db.js.map +1 -1
  142. package/dist/db/Schema.d.ts +12 -0
  143. package/dist/db/Schema.d.ts.map +1 -1
  144. package/dist/db/Schema.js +20 -0
  145. package/dist/db/Schema.js.map +1 -1
  146. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  147. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  148. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  149. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  150. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  151. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  152. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  153. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  154. package/dist/db/tables/fundingDeposits.d.ts +74 -1
  155. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  156. package/dist/db/tables/fundingDeposits.js +126 -0
  157. package/dist/db/tables/fundingDeposits.js.map +1 -1
  158. package/dist/internal/Auth.d.ts +15 -11
  159. package/dist/internal/Auth.d.ts.map +1 -1
  160. package/dist/internal/Auth.js +52 -17
  161. package/dist/internal/Auth.js.map +1 -1
  162. package/dist/internal/Cache.js +1 -1
  163. package/dist/internal/Cache.js.map +1 -1
  164. package/dist/internal/EdgeCache.d.ts +2 -2
  165. package/dist/internal/EdgeCache.d.ts.map +1 -1
  166. package/dist/internal/EdgeCache.js +10 -4
  167. package/dist/internal/EdgeCache.js.map +1 -1
  168. package/dist/internal/IpAllowlist.d.ts +15 -0
  169. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  170. package/dist/internal/IpAllowlist.js +27 -0
  171. package/dist/internal/IpAllowlist.js.map +1 -0
  172. package/dist/internal/OpenApi.d.ts +1 -1
  173. package/dist/internal/OpenApi.js +1 -1
  174. package/dist/internal/OpenApi.js.map +1 -1
  175. package/dist/internal/Tidx.d.ts +6 -0
  176. package/dist/internal/Tidx.d.ts.map +1 -1
  177. package/dist/internal/Tidx.js +13 -3
  178. package/dist/internal/Tidx.js.map +1 -1
  179. package/dist/internal/Viem.d.ts +8 -0
  180. package/dist/internal/Viem.d.ts.map +1 -1
  181. package/dist/internal/Viem.js +12 -5
  182. package/dist/internal/Viem.js.map +1 -1
  183. package/dist/internal/funding/Deposit.d.ts +42 -0
  184. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  185. package/dist/internal/funding/Deposit.js +37 -5
  186. package/dist/internal/funding/Deposit.js.map +1 -1
  187. package/dist/internal/funding/Evidence.d.ts +1 -5
  188. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  189. package/dist/internal/funding/Evidence.js +4 -3
  190. package/dist/internal/funding/Evidence.js.map +1 -1
  191. package/dist/internal/funding/Provider.d.ts +102 -0
  192. package/dist/internal/funding/Provider.d.ts.map +1 -1
  193. package/dist/internal/funding/Provider.js +5 -0
  194. package/dist/internal/funding/Provider.js.map +1 -1
  195. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  196. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  197. package/dist/internal/funding/Reconciliation.js +348 -54
  198. package/dist/internal/funding/Reconciliation.js.map +1 -1
  199. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  200. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  201. package/dist/internal/funding/SourceObservation.js +143 -0
  202. package/dist/internal/funding/SourceObservation.js.map +1 -0
  203. package/dist/internal/funding/Subsidy.d.ts +2 -2
  204. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  205. package/dist/internal/funding/Subsidy.js +1 -1
  206. package/dist/internal/funding/Subsidy.js.map +1 -1
  207. package/dist/internal/funding/index.d.ts +1 -0
  208. package/dist/internal/funding/index.d.ts.map +1 -1
  209. package/dist/internal/funding/index.js +1 -0
  210. package/dist/internal/funding/index.js.map +1 -1
  211. package/dist/internal/funding/providers/relay.d.ts +43 -1
  212. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  213. package/dist/internal/funding/providers/relay.js +438 -27
  214. package/dist/internal/funding/providers/relay.js.map +1 -1
  215. package/package.json +3 -3
  216. package/src/ApiKey.test.ts +35 -0
  217. package/src/ApiKey.ts +27 -0
  218. package/src/ApiKeys.test.ts +29 -0
  219. package/src/ApiKeys.ts +11 -3
  220. package/src/App.test.ts +55 -5
  221. package/src/App.ts +79 -5
  222. package/src/Client.test-d.ts +203 -28
  223. package/src/admin/apps/api-keys.test.ts +25 -1
  224. package/src/admin/apps/api-keys.ts +6 -2
  225. package/src/admin/ui.generated.ts +67 -67
  226. package/src/analytics/Analytics.ts +4 -0
  227. package/src/apps/composition.test.ts +130 -3
  228. package/src/apps/data/App.test.ts +0 -12
  229. package/src/apps/data/App.ts +4 -8
  230. package/src/apps/data/routes/balances.test.ts +42 -0
  231. package/src/apps/data/routes/balances.ts +218 -85
  232. package/src/apps/data/routes/earn.test.ts +363 -0
  233. package/src/apps/data/routes/earn.ts +1141 -3
  234. package/src/apps/data/routes/indexer.ts +2 -1
  235. package/src/apps/data/routes/rpc.ts +2 -0
  236. package/src/apps/data/routes/tokens.test.ts +6 -8
  237. package/src/apps/funding/Access.ts +26 -0
  238. package/src/apps/funding/App.test.ts +78 -0
  239. package/src/apps/funding/App.ts +78 -0
  240. package/src/apps/funding/Inventory.ts +30 -0
  241. package/src/apps/funding/ProviderAttempts.ts +26 -0
  242. package/src/apps/funding/index.ts +1 -0
  243. package/src/apps/funding/routes/chains.test.ts +78 -0
  244. package/src/apps/funding/routes/chains.ts +101 -0
  245. package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
  246. package/src/apps/funding/routes/deposit-addresses.ts +932 -0
  247. package/src/apps/funding/routes/providers.test.ts +165 -0
  248. package/src/apps/funding/routes/providers.ts +73 -0
  249. package/src/apps/funding/routes/quotes.test.ts +511 -0
  250. package/src/apps/funding/routes/quotes.ts +205 -0
  251. package/src/apps/funding/routes/transfers.test.ts +1236 -0
  252. package/src/apps/funding/routes/transfers.ts +704 -0
  253. package/src/apps/index.ts +1 -0
  254. package/src/apps/management/routes/api-keys.test.ts +98 -0
  255. package/src/apps/management/routes/api-keys.ts +96 -1
  256. package/src/apps/mcp.test.ts +17 -2
  257. package/src/apps/mcp.ts +18 -13
  258. package/src/apps/metadata.ts +12 -0
  259. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  260. package/src/cloudflare.ts +279 -0
  261. package/src/db/Db.ts +71 -0
  262. package/src/db/Schema.ts +24 -0
  263. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  264. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  265. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  266. package/src/db/tables/fundingDeposits.test.ts +239 -0
  267. package/src/db/tables/fundingDeposits.ts +214 -1
  268. package/src/internal/Auth.test.ts +205 -5
  269. package/src/internal/Auth.ts +63 -19
  270. package/src/internal/Cache.test.ts +37 -3
  271. package/src/internal/Cache.ts +1 -1
  272. package/src/internal/EdgeCache.test.ts +7 -7
  273. package/src/internal/EdgeCache.ts +13 -4
  274. package/src/internal/IpAllowlist.test.ts +52 -0
  275. package/src/internal/IpAllowlist.ts +39 -0
  276. package/src/internal/Log.test.ts +7 -1
  277. package/src/internal/OpenApi.ts +1 -1
  278. package/src/internal/Tidx.test.ts +28 -0
  279. package/src/internal/Tidx.ts +19 -3
  280. package/src/internal/Viem.test.ts +28 -0
  281. package/src/internal/Viem.ts +21 -5
  282. package/src/internal/funding/Deposit.ts +74 -5
  283. package/src/internal/funding/Evidence.test.ts +2 -2
  284. package/src/internal/funding/Evidence.ts +4 -9
  285. package/src/internal/funding/Provider.ts +115 -0
  286. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  287. package/src/internal/funding/Reconciliation.test.ts +1364 -109
  288. package/src/internal/funding/Reconciliation.ts +467 -88
  289. package/src/internal/funding/SourceObservation.test.ts +301 -0
  290. package/src/internal/funding/SourceObservation.ts +267 -0
  291. package/src/internal/funding/Subsidy.ts +2 -2
  292. package/src/internal/funding/index.test.ts +1 -0
  293. package/src/internal/funding/index.ts +1 -0
  294. package/src/internal/funding/providers/relay.test.ts +94 -4
  295. package/src/internal/funding/providers/relay.ts +533 -27
  296. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  297. package/dist/apps/data/routes/funding.js +0 -1557
  298. package/dist/apps/data/routes/funding.js.map +0 -1
  299. package/src/apps/data/routes/funding.test.ts +0 -2994
  300. package/src/apps/data/routes/funding.ts +0 -1821
@@ -1,9 +1,11 @@
1
1
  import * as NodeBuffer from 'node:buffer'
2
2
 
3
- import { AbiEvent } from 'ox'
3
+ import { AbiEvent, AbiFunction, AbiParameters, Address, Hex } from 'ox'
4
4
 
5
+ import * as Metrics from '../../Metrics.js'
5
6
  import * as FundingCatalog from '../../db/tables/fundingCatalog.js'
6
7
  import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js'
8
+ import * as FundingDepositRequestObservations from '../../db/tables/fundingDepositRequestObservations.js'
7
9
  import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
8
10
  import * as TestApp from '../../../test/App.js'
9
11
  import * as TestFunding from '../../../test/Funding.js'
@@ -15,12 +17,13 @@ import * as Evidence from './Evidence.js'
15
17
  import * as Provider from './Provider.js'
16
18
  import * as Reconciliation from './Reconciliation.js'
17
19
  import * as Relay from './providers/relay.js'
20
+ import * as SourceObservation from './SourceObservation.js'
18
21
 
19
22
  const db = TestApp.database()
20
23
  const transferTopic = AbiEvent.getSelector('event Transfer(address,address,uint256)')
21
24
 
22
25
  describe('createTracker', () => {
23
- test('requires action when Relay completes without destination funds', async () => {
26
+ test('keeps Relay success pending without destination evidence', async () => {
24
27
  const route = TestFunding.transferSnapshot()
25
28
  const depositAddress = `0x${'44'.repeat(20)}`
26
29
  const sourceTransactionHash = `0x${'aa'.repeat(32)}`
@@ -44,6 +47,15 @@ describe('createTracker', () => {
44
47
  subsidize: false,
45
48
  },
46
49
  })
50
+ const counts: Parameters<Metrics.Metrics['count']>[] = []
51
+ const metrics = Metrics.from({
52
+ count(...parameters) {
53
+ counts.push(parameters)
54
+ },
55
+ flush() {},
56
+ gauge() {},
57
+ histogram() {},
58
+ })
47
59
  const server = await TestRelay.createServer(async (request, response) => {
48
60
  if (request.method === 'GET') {
49
61
  response.writeHead(200, { 'content-type': 'application/json' })
@@ -53,7 +65,7 @@ describe('createTracker', () => {
53
65
  {
54
66
  createdAt: '2026-01-01T00:01:00.000Z',
55
67
  data: {
56
- inTxs: [{ hash: providerTransactionHash }],
68
+ inTxs: [{ txHash: providerTransactionHash }],
57
69
  outTxs: [],
58
70
  },
59
71
  depositAddress: { depositTxHash: sourceTransactionHash },
@@ -132,6 +144,7 @@ describe('createTracker', () => {
132
144
  dispatch: async (message) => {
133
145
  messages.push(message)
134
146
  },
147
+ metrics,
135
148
  now: () => new Date('2027-01-01T00:00:00.000Z'),
136
149
  providers: [provider],
137
150
  verifyTransfers: Evidence.createVerifier({
@@ -142,15 +155,32 @@ describe('createTracker', () => {
142
155
  const tick = await tracker.tick()
143
156
  const message = messages[0]
144
157
  if (!message) throw new Error('Expected a reconciliation message.')
145
- const reconciliation = await tracker.reconcile(message)
146
- const duplicate = await tracker.reconcile(message)
158
+ const queuedBeforeAttribution: Provider.Webhook.receive.Queued = {
159
+ addressId: message.addressId,
160
+ pollLeaseVersion: message.pollLeaseVersion,
161
+ type: message.type,
162
+ }
163
+ const reconciliation = await tracker.reconcile(queuedBeforeAttribution)
164
+ const duplicate = await tracker.reconcile(queuedBeforeAttribution)
147
165
  const [record] = await FundingDeposits.listBySourceTransaction(db, {
148
166
  depositAddressId: address.id,
149
167
  sourceTransactionHash,
150
168
  })
169
+ const observation = await FundingDepositRequestObservations.get(db, {
170
+ depositAddressId: address.id,
171
+ providerRequestId: 'relay-child-request',
172
+ })
151
173
  const storedAddress = await FundingDepositAddresses.get(db, address.id)
152
174
 
153
- expect({ duplicate, reconciliation, record, storedAddress, tick }).toMatchInlineSnapshot(
175
+ expect({
176
+ counts,
177
+ duplicate,
178
+ observation,
179
+ reconciliation,
180
+ record,
181
+ storedAddress,
182
+ tick,
183
+ }).toMatchInlineSnapshot(
154
184
  {
155
185
  record: {
156
186
  createdAt: expect.any(String),
@@ -159,6 +189,7 @@ describe('createTracker', () => {
159
189
  snapshot: { depositAddressId: expect.any(String) },
160
190
  updatedAt: expect.any(String),
161
191
  },
192
+ observation: { depositAddressId: expect.any(String) },
162
193
  storedAddress: {
163
194
  id: expect.any(String),
164
195
  providerOutputToken: expect.any(Object),
@@ -167,9 +198,52 @@ describe('createTracker', () => {
167
198
  },
168
199
  `
169
200
  {
201
+ "counts": [
202
+ [
203
+ "funding_deposit_reconciliation_count",
204
+ 1,
205
+ {
206
+ "outcome": "enqueued",
207
+ "provider": "relay",
208
+ "trigger": "poll",
209
+ },
210
+ ],
211
+ [
212
+ "funding_deposit_detection_count",
213
+ 1,
214
+ {
215
+ "provider": "relay",
216
+ "trigger": "poll",
217
+ },
218
+ ],
219
+ [
220
+ "funding_deposit_reconciliation_count",
221
+ 1,
222
+ {
223
+ "outcome": "follow_up_enqueued",
224
+ "provider": "relay",
225
+ "trigger": "poll",
226
+ },
227
+ ],
228
+ [
229
+ "funding_deposit_reconciliation_count",
230
+ 1,
231
+ {
232
+ "outcome": "completed",
233
+ "provider": "relay",
234
+ "trigger": "poll",
235
+ },
236
+ ],
237
+ ],
170
238
  "duplicate": {
171
239
  "type": "busy",
172
240
  },
241
+ "observation": {
242
+ "depositAddressId": Any<String>,
243
+ "pollObservedAt": "2027-01-01T00:00:00.000Z",
244
+ "providerRequestId": "relay-child-request",
245
+ "webhookReceivedAt": null,
246
+ },
173
247
  "reconciliation": {
174
248
  "created": 1,
175
249
  "type": "completed",
@@ -178,9 +252,11 @@ describe('createTracker', () => {
178
252
  "record": {
179
253
  "createdAt": Any<String>,
180
254
  "depositAddressId": Any<String>,
255
+ "detectionTrigger": "poll",
181
256
  "environment": "production",
182
257
  "id": Any<String>,
183
258
  "orgId": "org_reconciliation",
259
+ "pollObservedAt": "2027-01-01T00:00:00.000Z",
184
260
  "projectId": null,
185
261
  "providerOutputAmount": null,
186
262
  "providerOutputToken": {
@@ -258,15 +334,13 @@ describe('createTracker', () => {
258
334
  "sourceChainId": "eip155:8453",
259
335
  "sourceTransactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
260
336
  "sourceTransferIndex": 0,
261
- "status": "action-required",
262
- "statusReason": {
263
- "code": "delivery_failed",
264
- "message": "The provider completed without delivering destination funds.",
265
- },
337
+ "status": "bridging",
338
+ "statusReason": null,
266
339
  "subsidyAmount": null,
267
340
  "tempoGasPaid": null,
268
341
  "updatedAt": Any<String>,
269
342
  "version": 3,
343
+ "webhookReceivedAt": null,
270
344
  },
271
345
  "storedAddress": {
272
346
  "address": "0x4444444444444444444444444444444444444444",
@@ -288,7 +362,9 @@ describe('createTracker', () => {
288
362
  "providerRequestIds": [],
289
363
  "providerState": {
290
364
  "reconciliation": {
291
- "pendingRequestIds": [],
365
+ "pendingRequestIds": [
366
+ "relay-child-request",
367
+ ],
292
368
  "requestIds": [
293
369
  "relay-child-request",
294
370
  ],
@@ -403,6 +479,7 @@ describe('createTracker', () => {
403
479
  status: 'success',
404
480
  updatedAt: '2026-01-06T00:02:00.000Z',
405
481
  },
482
+ trigger: 'manual',
406
483
  verifyTransfers: Evidence.createVerifier({ db }),
407
484
  })
408
485
  const [record] = await FundingDeposits.listBySourceTransaction(db, {
@@ -425,9 +502,11 @@ describe('createTracker', () => {
425
502
  "record": {
426
503
  "createdAt": Any<String>,
427
504
  "depositAddressId": Any<String>,
505
+ "detectionTrigger": "manual",
428
506
  "environment": "production",
429
507
  "id": Any<String>,
430
508
  "orgId": "org_unsubsidized_reconciliation",
509
+ "pollObservedAt": null,
431
510
  "projectId": null,
432
511
  "providerOutputAmount": {
433
512
  "baseUnits": "950000",
@@ -532,6 +611,7 @@ describe('createTracker', () => {
532
611
  "tempoGasPaid": null,
533
612
  "updatedAt": Any<String>,
534
613
  "version": 3,
614
+ "webhookReceivedAt": null,
535
615
  },
536
616
  "result": {
537
617
  "created": 1,
@@ -577,7 +657,7 @@ describe('createTracker', () => {
577
657
  requests: [
578
658
  {
579
659
  createdAt: '2026-01-04T00:01:00.000Z',
580
- data: { inTxs: [{ hash: sourceTransactionHash }], outTxs: [] },
660
+ data: { inTxs: [{ txHash: sourceTransactionHash }], outTxs: [] },
581
661
  depositAddress: { depositTxHash: sourceTransactionHash },
582
662
  id: 'relay-observed-request',
583
663
  status: 'pending',
@@ -688,6 +768,7 @@ describe('createTracker', () => {
688
768
  "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
689
769
  "verified": true,
690
770
  },
771
+ "detectionTrigger": "webhook",
691
772
  "id": Any<String>,
692
773
  "provider": {
693
774
  "id": "relay",
@@ -750,9 +831,25 @@ describe('createTracker', () => {
750
831
  subsidize: false,
751
832
  },
752
833
  })
834
+ const counts: Parameters<Metrics.Metrics['count']>[] = []
835
+ const dispatches: Parameters<Reconciliation.createTracker.Options['dispatch']>[] = []
836
+ const histograms: Parameters<Metrics.Metrics['histogram']>[] = []
837
+ const metrics = Metrics.from({
838
+ count(...parameters) {
839
+ counts.push(parameters)
840
+ },
841
+ flush() {},
842
+ gauge() {},
843
+ histogram(...parameters) {
844
+ histograms.push(parameters)
845
+ },
846
+ })
753
847
  const requestTargets: string[] = []
754
848
  let delivered = false
849
+ let dispatchFailure = true
755
850
  let includeAddressRequest = true
851
+ let indexed = false
852
+ let providerUpdateVisible = false
756
853
  let returnOlderDirect = false
757
854
  const server = await TestRelay.createServer(async (request, response) => {
758
855
  if (request.method === 'GET') {
@@ -763,19 +860,20 @@ describe('createTracker', () => {
763
860
  response.end(
764
861
  JSON.stringify({
765
862
  requests:
766
- includeAddressRequest || direct
863
+ indexed && (includeAddressRequest || direct)
767
864
  ? [
768
865
  {
769
866
  createdAt: '2026-01-02T00:01:00.000Z',
770
867
  data: {
771
- inTxs: [{ hash: sourceTransactionHash }],
772
- outTxs: delivered ? [{ hash: destinationTransactionHash }] : [],
868
+ inTxs: [{ txHash: sourceTransactionHash }],
869
+ outTxs: delivered ? [{ txHash: destinationTransactionHash }] : [],
773
870
  },
774
871
  depositAddress: { depositTxHash: sourceTransactionHash },
775
872
  id: 'relay-pending-request',
776
873
  status: delivered ? 'success' : 'pending',
777
874
  updatedAt: (() => {
778
875
  if (direct && returnOlderDirect) return '2026-01-01T00:00:00.000Z'
876
+ if (!providerUpdateVisible) return '2026-01-02T00:02:00.000Z'
779
877
  if (delivered) return '2026-01-02T00:03:00.500Z'
780
878
  return '2026-01-02T00:03:00Z'
781
879
  })(),
@@ -845,51 +943,906 @@ describe('createTracker', () => {
845
943
  routes: [],
846
944
  tokens: [],
847
945
  })
848
- const provider = Relay.relay({ apiKey: 'relay-api-key', baseUrl: server.url })
946
+ const provider = Relay.relay({ apiKey: 'relay-api-key', baseUrl: server.url })
947
+ let trackerNow = new Date('2026-01-02T00:03:10.000Z')
948
+ const tracker = Reconciliation.createTracker({
949
+ db,
950
+ dispatch: async (...parameters) => {
951
+ if (dispatchFailure) {
952
+ dispatchFailure = false
953
+ throw new Error('Reconciliation queue unavailable.')
954
+ }
955
+ dispatches.push(parameters)
956
+ },
957
+ metrics,
958
+ now: () => trackerNow,
959
+ providers: [provider],
960
+ verifyTransfers: Evidence.createVerifier({ db }),
961
+ })
962
+
963
+ const message = {
964
+ addressId: address.id,
965
+ requestId: 'relay-pending-request',
966
+ trigger: 'webhook',
967
+ type: 'funding:deposit-address:reconcile',
968
+ webhookTiming: {
969
+ enqueuedAt: '2026-01-02T00:03:07.000Z',
970
+ providerStatus: 'pending',
971
+ providerUpdatedAt: '2026-01-02T00:03:00.000Z',
972
+ receivedAt: '2026-01-02T00:03:06.000Z',
973
+ sentAt: '2026-01-02T00:03:05.000Z',
974
+ },
975
+ } satisfies Provider.Webhook.receive.Dispatchable
976
+ await expect(tracker.reconcile(message)).rejects.toThrowErrorMatchingInlineSnapshot(
977
+ `[Error: Reconciliation queue unavailable.]`,
978
+ )
979
+ const pending = await tracker.reconcile(message)
980
+ const [firstFollowUp] = dispatches
981
+ if (!firstFollowUp) throw new Error('Expected a provider follow-up message.')
982
+ indexed = true
983
+ const pendingFollowUp = await tracker.reconcile(firstFollowUp[0])
984
+ const exhausted = await tracker.reconcile({
985
+ ...firstFollowUp[0],
986
+ followUp: { attempt: 5, kind: 'provider' },
987
+ })
988
+ const [, secondFollowUp] = dispatches
989
+ if (!secondFollowUp) throw new Error('Expected a second provider follow-up message.')
990
+ delivered = true
991
+ includeAddressRequest = false
992
+ providerUpdateVisible = true
993
+ trackerNow = new Date('2026-01-02T00:03:15.000Z')
994
+ const completed = await tracker.reconcile(secondFollowUp[0])
995
+ includeAddressRequest = true
996
+ const replay = await tracker.reconcile({
997
+ addressId: address.id,
998
+ trigger: 'webhook',
999
+ type: 'funding:deposit-address:reconcile',
1000
+ })
1001
+ includeAddressRequest = false
1002
+ returnOlderDirect = true
1003
+ const webhook = await tracker.reconcile({
1004
+ addressId: address.id,
1005
+ requestId: 'relay-pending-request',
1006
+ trigger: 'webhook',
1007
+ type: 'funding:deposit-address:reconcile',
1008
+ })
1009
+ const [record] = await FundingDeposits.listBySourceTransaction(db, {
1010
+ depositAddressId: address.id,
1011
+ sourceTransactionHash,
1012
+ })
1013
+ const storedAddress = await FundingDepositAddresses.get(db, address.id)
1014
+
1015
+ expect({
1016
+ completed,
1017
+ counts,
1018
+ dispatches,
1019
+ exhausted,
1020
+ histograms,
1021
+ pending,
1022
+ pendingFollowUp,
1023
+ record,
1024
+ replay,
1025
+ requestTargets,
1026
+ storedAddress,
1027
+ webhook,
1028
+ }).toMatchInlineSnapshot(
1029
+ {
1030
+ dispatches: [
1031
+ [{ addressId: expect.any(String) }, { delaySeconds: 5 }],
1032
+ [{ addressId: expect.any(String) }, { delaySeconds: 5 }],
1033
+ ],
1034
+ record: {
1035
+ createdAt: expect.any(String),
1036
+ depositAddressId: expect.any(String),
1037
+ id: expect.any(String),
1038
+ snapshot: { depositAddressId: expect.any(String) },
1039
+ updatedAt: expect.any(String),
1040
+ },
1041
+ storedAddress: {
1042
+ id: expect.any(String),
1043
+ providerOutputToken: expect.any(Object),
1044
+ snapshot: expect.any(Object),
1045
+ },
1046
+ },
1047
+ `
1048
+ {
1049
+ "completed": {
1050
+ "created": 0,
1051
+ "type": "completed",
1052
+ "updated": 1,
1053
+ },
1054
+ "counts": [
1055
+ [
1056
+ "funding_deposit_reconciliation_count",
1057
+ 1,
1058
+ {
1059
+ "outcome": "follow_up_enqueue_failed",
1060
+ "provider": "relay",
1061
+ "trigger": "webhook",
1062
+ },
1063
+ ],
1064
+ [
1065
+ "funding_deposit_reconciliation_count",
1066
+ 1,
1067
+ {
1068
+ "outcome": "failed",
1069
+ "provider": "relay",
1070
+ "trigger": "webhook",
1071
+ },
1072
+ ],
1073
+ [
1074
+ "funding_deposit_reconciliation_count",
1075
+ 1,
1076
+ {
1077
+ "outcome": "follow_up_enqueued",
1078
+ "provider": "relay",
1079
+ "trigger": "webhook",
1080
+ },
1081
+ ],
1082
+ [
1083
+ "funding_deposit_reconciliation_count",
1084
+ 1,
1085
+ {
1086
+ "outcome": "completed",
1087
+ "provider": "relay",
1088
+ "trigger": "webhook",
1089
+ },
1090
+ ],
1091
+ [
1092
+ "funding_deposit_detection_count",
1093
+ 1,
1094
+ {
1095
+ "provider": "relay",
1096
+ "trigger": "webhook",
1097
+ },
1098
+ ],
1099
+ [
1100
+ "funding_deposit_reconciliation_count",
1101
+ 1,
1102
+ {
1103
+ "outcome": "follow_up_enqueued",
1104
+ "provider": "relay",
1105
+ "trigger": "webhook",
1106
+ },
1107
+ ],
1108
+ [
1109
+ "funding_deposit_reconciliation_count",
1110
+ 1,
1111
+ {
1112
+ "outcome": "completed",
1113
+ "provider": "relay",
1114
+ "trigger": "webhook",
1115
+ },
1116
+ ],
1117
+ [
1118
+ "funding_deposit_reconciliation_count",
1119
+ 1,
1120
+ {
1121
+ "outcome": "follow_up_exhausted",
1122
+ "provider": "relay",
1123
+ "trigger": "webhook",
1124
+ },
1125
+ ],
1126
+ [
1127
+ "funding_deposit_reconciliation_count",
1128
+ 1,
1129
+ {
1130
+ "outcome": "completed",
1131
+ "provider": "relay",
1132
+ "trigger": "webhook",
1133
+ },
1134
+ ],
1135
+ [
1136
+ "funding_deposit_reconciliation_count",
1137
+ 1,
1138
+ {
1139
+ "outcome": "completed",
1140
+ "provider": "relay",
1141
+ "trigger": "webhook",
1142
+ },
1143
+ ],
1144
+ [
1145
+ "funding_deposit_reconciliation_count",
1146
+ 1,
1147
+ {
1148
+ "outcome": "completed",
1149
+ "provider": "relay",
1150
+ "trigger": "webhook",
1151
+ },
1152
+ ],
1153
+ [
1154
+ "funding_deposit_reconciliation_count",
1155
+ 1,
1156
+ {
1157
+ "outcome": "completed",
1158
+ "provider": "relay",
1159
+ "trigger": "webhook",
1160
+ },
1161
+ ],
1162
+ ],
1163
+ "dispatches": [
1164
+ [
1165
+ {
1166
+ "addressId": Any<String>,
1167
+ "followUp": {
1168
+ "attempt": 0,
1169
+ "kind": "provider",
1170
+ },
1171
+ "requestId": "relay-pending-request",
1172
+ "trigger": "webhook",
1173
+ "type": "funding:deposit-address:reconcile",
1174
+ "webhookTiming": {
1175
+ "enqueuedAt": "2026-01-02T00:03:07.000Z",
1176
+ "providerStatus": "pending",
1177
+ "providerUpdatedAt": "2026-01-02T00:03:00.000Z",
1178
+ "receivedAt": "2026-01-02T00:03:06.000Z",
1179
+ "sentAt": "2026-01-02T00:03:05.000Z",
1180
+ },
1181
+ },
1182
+ {
1183
+ "delaySeconds": 5,
1184
+ },
1185
+ ],
1186
+ [
1187
+ {
1188
+ "addressId": Any<String>,
1189
+ "followUp": {
1190
+ "attempt": 1,
1191
+ "kind": "provider",
1192
+ },
1193
+ "requestId": "relay-pending-request",
1194
+ "trigger": "webhook",
1195
+ "type": "funding:deposit-address:reconcile",
1196
+ "webhookTiming": {
1197
+ "enqueuedAt": "2026-01-02T00:03:07.000Z",
1198
+ "providerStatus": "pending",
1199
+ "providerUpdatedAt": "2026-01-02T00:03:00.000Z",
1200
+ "receivedAt": "2026-01-02T00:03:06.000Z",
1201
+ "sentAt": "2026-01-02T00:03:05.000Z",
1202
+ },
1203
+ },
1204
+ {
1205
+ "delaySeconds": 5,
1206
+ },
1207
+ ],
1208
+ ],
1209
+ "exhausted": {
1210
+ "created": 0,
1211
+ "type": "completed",
1212
+ "updated": 1,
1213
+ },
1214
+ "histograms": [
1215
+ [
1216
+ "funding_deposit_reconciliation_duration_ms",
1217
+ 0,
1218
+ {
1219
+ "provider": "relay",
1220
+ "trigger": "webhook",
1221
+ },
1222
+ ],
1223
+ [
1224
+ "funding_deposit_reconciliation_duration_ms",
1225
+ 0,
1226
+ {
1227
+ "provider": "relay",
1228
+ "trigger": "webhook",
1229
+ },
1230
+ ],
1231
+ [
1232
+ "funding_deposit_reconciliation_duration_ms",
1233
+ 0,
1234
+ {
1235
+ "provider": "relay",
1236
+ "trigger": "webhook",
1237
+ },
1238
+ ],
1239
+ [
1240
+ "funding_deposit_webhook_latency_ms",
1241
+ 5000,
1242
+ {
1243
+ "provider": "relay",
1244
+ "segment": "provider_to_sent",
1245
+ "status": "pending",
1246
+ },
1247
+ ],
1248
+ [
1249
+ "funding_deposit_webhook_latency_ms",
1250
+ 1000,
1251
+ {
1252
+ "provider": "relay",
1253
+ "segment": "sent_to_received",
1254
+ "status": "pending",
1255
+ },
1256
+ ],
1257
+ [
1258
+ "funding_deposit_webhook_latency_ms",
1259
+ 1000,
1260
+ {
1261
+ "provider": "relay",
1262
+ "segment": "received_to_enqueued",
1263
+ "status": "pending",
1264
+ },
1265
+ ],
1266
+ [
1267
+ "funding_deposit_webhook_latency_ms",
1268
+ 8000,
1269
+ {
1270
+ "provider": "relay",
1271
+ "segment": "enqueued_to_reconciled",
1272
+ "status": "pending",
1273
+ },
1274
+ ],
1275
+ [
1276
+ "funding_deposit_webhook_latency_ms",
1277
+ 15000,
1278
+ {
1279
+ "provider": "relay",
1280
+ "segment": "provider_to_reconciled",
1281
+ "status": "pending",
1282
+ },
1283
+ ],
1284
+ [
1285
+ "funding_deposit_reconciliation_duration_ms",
1286
+ 0,
1287
+ {
1288
+ "provider": "relay",
1289
+ "trigger": "webhook",
1290
+ },
1291
+ ],
1292
+ [
1293
+ "funding_deposit_reconciliation_duration_ms",
1294
+ 0,
1295
+ {
1296
+ "provider": "relay",
1297
+ "trigger": "webhook",
1298
+ },
1299
+ ],
1300
+ [
1301
+ "funding_deposit_reconciliation_duration_ms",
1302
+ 0,
1303
+ {
1304
+ "provider": "relay",
1305
+ "trigger": "webhook",
1306
+ },
1307
+ ],
1308
+ ],
1309
+ "pending": {
1310
+ "created": 0,
1311
+ "type": "completed",
1312
+ "updated": 0,
1313
+ },
1314
+ "pendingFollowUp": {
1315
+ "created": 1,
1316
+ "type": "completed",
1317
+ "updated": 1,
1318
+ },
1319
+ "record": {
1320
+ "createdAt": Any<String>,
1321
+ "depositAddressId": Any<String>,
1322
+ "detectionTrigger": "webhook",
1323
+ "environment": "production",
1324
+ "id": Any<String>,
1325
+ "orgId": "org_pending_reconciliation",
1326
+ "pollObservedAt": null,
1327
+ "projectId": null,
1328
+ "providerOutputAmount": {
1329
+ "baseUnits": "1000000",
1330
+ "currency": "USD",
1331
+ "decimals": 6,
1332
+ "formatted": "1",
1333
+ },
1334
+ "providerOutputToken": {
1335
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1336
+ "currency": "USD",
1337
+ "decimals": 6,
1338
+ "name": "Bridged USDC (Stargate)",
1339
+ "standard": "TIP-20",
1340
+ "symbol": "USDC.e",
1341
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1342
+ "verified": true,
1343
+ },
1344
+ "providerRequestId": "relay-pending-request",
1345
+ "providerRequestIds": [
1346
+ "relay-pending-request",
1347
+ ],
1348
+ "providerState": null,
1349
+ "providerTransactionHashes": [
1350
+ "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
1351
+ ],
1352
+ "providerTransferIndex": 0,
1353
+ "retryState": null,
1354
+ "settlementTransaction": null,
1355
+ "settlementTransactionHash": null,
1356
+ "snapshot": {
1357
+ "depositAddressId": Any<String>,
1358
+ "destinationAmount": {
1359
+ "baseUnits": "1000000",
1360
+ "currency": "USD",
1361
+ "decimals": 6,
1362
+ "formatted": "1",
1363
+ },
1364
+ "destinationAmountRequired": {
1365
+ "baseUnits": "1000000",
1366
+ "currency": "USD",
1367
+ "decimals": 6,
1368
+ "formatted": "1",
1369
+ },
1370
+ "destinationChain": {
1371
+ "addressFormat": "hex",
1372
+ "id": "eip155:4217",
1373
+ "kind": "evm",
1374
+ "name": "Tempo",
1375
+ },
1376
+ "destinationToken": {
1377
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1378
+ "currency": "USD",
1379
+ "decimals": 6,
1380
+ "name": "Bridged USDC (Stargate)",
1381
+ "standard": "TIP-20",
1382
+ "symbol": "USDC.e",
1383
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1384
+ "verified": true,
1385
+ },
1386
+ "destinationTransactionHashes": [
1387
+ "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
1388
+ ],
1389
+ "provider": {
1390
+ "id": "relay",
1391
+ "name": "Relay",
1392
+ },
1393
+ "recipient": "0x1111111111111111111111111111111111111111",
1394
+ "refundAddress": "0x9999999999999999999999999999999999999999",
1395
+ "sender": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1396
+ "sourceAmount": {
1397
+ "baseUnits": "1000000",
1398
+ "currency": "USD",
1399
+ "decimals": 6,
1400
+ "formatted": "1",
1401
+ },
1402
+ "sourceChain": {
1403
+ "addressFormat": "hex",
1404
+ "id": "eip155:8453",
1405
+ "kind": "evm",
1406
+ "name": "Base",
1407
+ },
1408
+ "sourceToken": {
1409
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1410
+ "currency": "USD",
1411
+ "decimals": 6,
1412
+ "name": "USD Coin",
1413
+ "standard": "ERC-20",
1414
+ "symbol": "USDC",
1415
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1416
+ "verified": true,
1417
+ },
1418
+ "sourceTransactionHashes": [
1419
+ "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
1420
+ ],
1421
+ },
1422
+ "sourceChainId": "eip155:8453",
1423
+ "sourceTransactionHash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
1424
+ "sourceTransferIndex": 0,
1425
+ "status": "completed",
1426
+ "statusReason": null,
1427
+ "subsidyAmount": null,
1428
+ "tempoGasPaid": null,
1429
+ "updatedAt": Any<String>,
1430
+ "version": 7,
1431
+ "webhookReceivedAt": null,
1432
+ },
1433
+ "replay": {
1434
+ "created": 0,
1435
+ "type": "completed",
1436
+ "updated": 0,
1437
+ },
1438
+ "requestTargets": [
1439
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1440
+ "/requests/v3?id=relay-pending-request&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1441
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1442
+ "/requests/v3?id=relay-pending-request&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1443
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1444
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1445
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1446
+ "/requests/v3?id=relay-pending-request&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1447
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1448
+ "/requests/v3?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1449
+ "/requests/v3?id=relay-pending-request&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1450
+ ],
1451
+ "storedAddress": {
1452
+ "address": "0x8888888888888888888888888888888888888888",
1453
+ "apiKeyId": "key_pending_reconciliation",
1454
+ "createdAt": "2026-01-02T00:00:00.000Z",
1455
+ "deliveryStrategy": "provider",
1456
+ "destinationTokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1457
+ "environment": "production",
1458
+ "id": Any<String>,
1459
+ "lastPolledAt": "2026-01-02T00:03:15.000Z",
1460
+ "nextPollAt": "2026-01-02T00:04:15.000Z",
1461
+ "orgId": "org_pending_reconciliation",
1462
+ "pollFailureCount": 0,
1463
+ "pollLeaseUntil": null,
1464
+ "pollLeaseVersion": 7,
1465
+ "projectId": null,
1466
+ "providerId": "relay",
1467
+ "providerOutputToken": Any<Object>,
1468
+ "providerRequestIds": [],
1469
+ "providerState": {
1470
+ "reconciliation": {
1471
+ "pendingRequestIds": [],
1472
+ "requestIds": [
1473
+ "relay-pending-request",
1474
+ ],
1475
+ "updatedAt": "2026-01-02T00:03:00.500Z",
1476
+ "webhookTiming": {
1477
+ "enqueuedAt": "2026-01-02T00:03:07.000Z",
1478
+ "providerStatus": "pending",
1479
+ "providerUpdatedAt": "2026-01-02T00:03:00.000Z",
1480
+ "receivedAt": "2026-01-02T00:03:06.000Z",
1481
+ "reconciliationCompletedAt": "2026-01-02T00:03:15.000Z",
1482
+ "requestId": "relay-pending-request",
1483
+ "sentAt": "2026-01-02T00:03:05.000Z",
1484
+ },
1485
+ },
1486
+ },
1487
+ "recipient": "0x1111111111111111111111111111111111111111",
1488
+ "refundAddress": "0x9999999999999999999999999999999999999999",
1489
+ "snapshot": Any<Object>,
1490
+ "sourceChainId": "eip155:8453",
1491
+ "sourceTokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1492
+ "status": "active",
1493
+ "subsidize": false,
1494
+ "updatedAt": "2026-01-02T00:00:00.000Z",
1495
+ "version": 1,
1496
+ },
1497
+ "webhook": {
1498
+ "created": 0,
1499
+ "type": "completed",
1500
+ "updated": 0,
1501
+ },
1502
+ }
1503
+ `,
1504
+ )
1505
+ } finally {
1506
+ await server.closeAsync()
1507
+ }
1508
+ })
1509
+
1510
+ test('completes chain-detected Relay delivery before provider attribution', async () => {
1511
+ const route = TestFunding.transferSnapshot()
1512
+ const ambiguousOrderId = `0x${'3d'.repeat(32)}`
1513
+ const ambiguousSweepTransactionHash = `0x${'3e'.repeat(32)}`
1514
+ const depositAddress = `0x${'31'.repeat(20)}`
1515
+ const depository = `0x${'32'.repeat(20)}`
1516
+ const destinationTransactionHash = `0x${'33'.repeat(32)}`
1517
+ const orderId = `0x${'34'.repeat(32)}`
1518
+ const refundAddress = `0x${'38'.repeat(20)}`
1519
+ const solver = `0x${'35'.repeat(20)}`
1520
+ const sourceSweepTransactionHash = `0x${'36'.repeat(32)}`
1521
+ const sourceTransactionHash = `0x${'37'.repeat(32)}`
1522
+ const address = await DepositAddress.create(db, {
1523
+ apiKeyId: 'key_delivery_observation',
1524
+ deliveryStrategy: 'provider',
1525
+ environment: 'production',
1526
+ now: new Date('2026-01-04T00:00:00.000Z'),
1527
+ orgId: 'org_delivery_observation',
1528
+ providerOutputToken: route.destinationToken,
1529
+ snapshot: {
1530
+ address: depositAddress,
1531
+ destinationChain: route.destinationChain,
1532
+ destinationToken: route.destinationToken,
1533
+ provider: { id: 'relay', name: 'Relay' },
1534
+ recipient: route.recipient,
1535
+ refundAddress,
1536
+ sourceChain: route.sourceChain,
1537
+ sourceToken: route.sourceToken,
1538
+ subsidize: true,
1539
+ },
1540
+ })
1541
+ const counts: Parameters<Metrics.Metrics['count']>[] = []
1542
+ const dispatches: Parameters<Reconciliation.createTracker.Options['dispatch']>[] = []
1543
+ const metrics = Metrics.from({
1544
+ count(...parameters) {
1545
+ counts.push(parameters)
1546
+ },
1547
+ flush() {},
1548
+ gauge() {},
1549
+ histogram() {},
1550
+ })
1551
+ const rpcMethods: string[] = []
1552
+ let ambiguous = true
1553
+ let correlateDestination = false
1554
+ let delivered = false
1555
+ let indexed = false
1556
+ const server = await TestRelay.createServer(async (request, response) => {
1557
+ if (request.method === 'GET') {
1558
+ const url = new URL(request.url ?? '/', server.url)
1559
+ response.writeHead(200, { 'content-type': 'application/json' })
1560
+ response.end(
1561
+ JSON.stringify(
1562
+ url.pathname === '/chains'
1563
+ ? {
1564
+ chains: [
1565
+ {
1566
+ id: Number(route.sourceChain.id.slice('eip155:'.length)),
1567
+ protocol: { v2: { depository } },
1568
+ solverAddresses: [],
1569
+ },
1570
+ {
1571
+ id: Number(route.destinationChain.id.slice('eip155:'.length)),
1572
+ solverAddresses: [solver],
1573
+ },
1574
+ ],
1575
+ }
1576
+ : {
1577
+ requests: indexed
1578
+ ? [
1579
+ {
1580
+ createdAt: '2026-01-04T00:01:00.000Z',
1581
+ data: { outTxs: [] },
1582
+ depositAddress: { depositTxHash: sourceTransactionHash },
1583
+ id: 'relay-observed-request',
1584
+ protocol: {
1585
+ deposit: { origin: { depositor: refundAddress, depository } },
1586
+ orderId,
1587
+ solver: { address: solver },
1588
+ },
1589
+ status: 'pending',
1590
+ updatedAt: '2026-01-04T00:02:00.000Z',
1591
+ },
1592
+ ]
1593
+ : [],
1594
+ },
1595
+ ),
1596
+ )
1597
+ return
1598
+ }
1599
+
1600
+ const chunks: Uint8Array[] = []
1601
+ for await (const chunk of request) chunks.push(chunk)
1602
+ const body = JSON.parse(NodeBuffer.Buffer.concat(chunks).toString()) as {
1603
+ id: number
1604
+ method: string
1605
+ params?: readonly unknown[] | undefined
1606
+ }
1607
+ rpcMethods.push(body.method)
1608
+ const parameter = body.params?.[0]
1609
+ const result = (() => {
1610
+ if (body.method === 'eth_blockNumber') return '0x100'
1611
+ if (body.method === 'eth_getTransactionByHash')
1612
+ return parameter === destinationTransactionHash
1613
+ ? rpcTransaction({
1614
+ blockNumber: 0xf0,
1615
+ from: solver,
1616
+ hash: destinationTransactionHash,
1617
+ input: (() => {
1618
+ const input = AbiFunction.encodeData(
1619
+ AbiFunction.from('function transfer(address recipient,uint256 amount)'),
1620
+ [route.recipient as Address.Address, 1_000_000n],
1621
+ )
1622
+ if (!correlateDestination) return input
1623
+ return Hex.concat(input, orderId as Hex.Hex)
1624
+ })(),
1625
+ to: route.destinationToken.address,
1626
+ })
1627
+ : rpcTransaction({
1628
+ blockNumber: 0x82,
1629
+ from: depositAddress,
1630
+ hash:
1631
+ parameter === sourceSweepTransactionHash
1632
+ ? sourceSweepTransactionHash
1633
+ : ambiguousSweepTransactionHash,
1634
+ input: (() => {
1635
+ if (parameter === sourceSweepTransactionHash)
1636
+ return AbiFunction.encodeData(
1637
+ AbiFunction.from(
1638
+ 'function depositErc20(address depositor,address token,bytes32 id)',
1639
+ ),
1640
+ [
1641
+ refundAddress as Address.Address,
1642
+ route.sourceToken.address as Address.Address,
1643
+ orderId as Hex.Hex,
1644
+ ],
1645
+ )
1646
+ return AbiFunction.encodeData(
1647
+ AbiFunction.from(
1648
+ 'function depositErc20(address depositor,address token,uint256 amount,bytes32 id)',
1649
+ ),
1650
+ [
1651
+ refundAddress as Address.Address,
1652
+ route.sourceToken.address as Address.Address,
1653
+ 1_000_000n,
1654
+ ambiguousOrderId as Hex.Hex,
1655
+ ],
1656
+ )
1657
+ })(),
1658
+ to: depository,
1659
+ })
1660
+ if (body.method === 'eth_getLogs') {
1661
+ const filter = parameter as { address?: string | undefined }
1662
+ if (filter.address?.toLowerCase() === depository.toLowerCase())
1663
+ return [
1664
+ ...(ambiguous
1665
+ ? [
1666
+ relayDepositLog({
1667
+ amount: 1_000_000n,
1668
+ blockNumber: 0x81,
1669
+ depository,
1670
+ from: refundAddress,
1671
+ orderId: ambiguousOrderId,
1672
+ token: route.sourceToken.address,
1673
+ transactionHash: ambiguousSweepTransactionHash,
1674
+ }),
1675
+ ]
1676
+ : []),
1677
+ relayDepositLog({
1678
+ amount: 1_000_000n,
1679
+ blockNumber: 0x82,
1680
+ depository,
1681
+ from: refundAddress,
1682
+ orderId,
1683
+ token: route.sourceToken.address,
1684
+ transactionHash: sourceSweepTransactionHash,
1685
+ }),
1686
+ ]
1687
+ if (delivered)
1688
+ return [
1689
+ transferLog({
1690
+ amount: 1_000_000n,
1691
+ blockNumber: 0xf0,
1692
+ recipient: route.recipient,
1693
+ sender: solver,
1694
+ token: route.destinationToken.address,
1695
+ transactionHash: destinationTransactionHash,
1696
+ }),
1697
+ ]
1698
+ return []
1699
+ }
1700
+ if (body.method === 'eth_getTransactionReceipt') {
1701
+ const hash = parameter
1702
+ return {
1703
+ blockNumber: hash === sourceTransactionHash ? '0x80' : '0xf0',
1704
+ logs: [
1705
+ hash === sourceTransactionHash
1706
+ ? transferLog({
1707
+ amount: 1_000_000n,
1708
+ recipient: depositAddress,
1709
+ sender: `0x${'3c'.repeat(20)}`,
1710
+ token: route.sourceToken.address,
1711
+ })
1712
+ : transferLog({
1713
+ amount: 1_000_000n,
1714
+ recipient: route.recipient,
1715
+ sender: solver,
1716
+ token: route.destinationToken.address,
1717
+ }),
1718
+ ],
1719
+ status: '0x1',
1720
+ }
1721
+ }
1722
+ throw new Error(`Unexpected RPC method: ${body.method}`)
1723
+ })()
1724
+ response.writeHead(200, { 'content-type': 'application/json' })
1725
+ response.end(JSON.stringify({ id: body.id, jsonrpc: '2.0', result }))
1726
+ })
1727
+
1728
+ try {
1729
+ await FundingCatalog.publish(db, {
1730
+ chainTokens: [],
1731
+ chains: [
1732
+ {
1733
+ aliases: [],
1734
+ id: route.destinationChain.id as Chain.Id,
1735
+ name: route.destinationChain.name,
1736
+ parentChainId: null,
1737
+ rpcUrls: [server.url],
1738
+ slug: 'destination',
1739
+ },
1740
+ {
1741
+ aliases: [],
1742
+ id: route.sourceChain.id as Chain.Id,
1743
+ name: route.sourceChain.name,
1744
+ parentChainId: null,
1745
+ rpcUrls: [server.url],
1746
+ slug: 'source',
1747
+ },
1748
+ ],
1749
+ routes: [],
1750
+ tokens: [],
1751
+ })
1752
+ const provider = Relay.relay({
1753
+ apiKey: 'relay-api-key',
1754
+ baseUrl: server.url,
1755
+ now: () => new Date('2026-01-04T00:03:00.000Z'),
1756
+ observation: { db },
1757
+ })
1758
+ if (!Provider.canObserveDepositAddressRequest(provider))
1759
+ throw new Error('Expected Relay delivery observation.')
1760
+ const ambiguousObservation = await provider.observeDepositAddressRequest(
1761
+ {
1762
+ address: depositAddress,
1763
+ destinationChain: route.destinationChain,
1764
+ destinationToken: route.destinationToken,
1765
+ recipient: route.recipient,
1766
+ refundAddress,
1767
+ source: { amount: '1000000', transactionHash: sourceTransactionHash },
1768
+ sourceChain: route.sourceChain,
1769
+ sourceToken: route.sourceToken,
1770
+ },
1771
+ new AbortController().signal,
1772
+ )
1773
+ ambiguous = false
1774
+ delivered = true
1775
+ const uncorrelatedObservation = await provider.observeDepositAddressRequest(
1776
+ {
1777
+ address: depositAddress,
1778
+ destinationChain: route.destinationChain,
1779
+ destinationToken: route.destinationToken,
1780
+ recipient: route.recipient,
1781
+ refundAddress,
1782
+ source: { amount: '1000000', transactionHash: sourceTransactionHash },
1783
+ sourceChain: route.sourceChain,
1784
+ sourceToken: route.sourceToken,
1785
+ },
1786
+ new AbortController().signal,
1787
+ )
1788
+ correlateDestination = true
1789
+ delivered = false
1790
+ const observed = await SourceObservation.observe(db, {
1791
+ addressId: address.id,
1792
+ amount: '1000000',
1793
+ chainId: route.sourceChain.id,
1794
+ now: new Date('2026-01-04T00:00:30.000Z'),
1795
+ recipient: depositAddress,
1796
+ sender: refundAddress,
1797
+ tokenAddress: route.sourceToken.address,
1798
+ transactionHash: sourceTransactionHash,
1799
+ transferIndex: 0,
1800
+ })
1801
+ if (observed.type === 'ignored') throw new Error('Expected source observation.')
849
1802
  const tracker = Reconciliation.createTracker({
850
1803
  db,
851
- dispatch: async () => undefined,
852
- now: () => new Date('2027-01-02T00:00:00.000Z'),
1804
+ dispatch: async (...parameters) => {
1805
+ dispatches.push(parameters)
1806
+ },
1807
+ metrics,
1808
+ now: () => new Date('2026-01-04T00:03:00.000Z'),
853
1809
  providers: [provider],
854
1810
  verifyTransfers: Evidence.createVerifier({ db }),
855
1811
  })
856
-
857
1812
  const pending = await tracker.reconcile({
858
1813
  addressId: address.id,
1814
+ trigger: 'chain',
859
1815
  type: 'funding:deposit-address:reconcile',
860
1816
  })
1817
+ const [deliveryFollowUp] = dispatches
1818
+ if (!deliveryFollowUp) throw new Error('Expected a delivery follow-up message.')
861
1819
  delivered = true
862
- includeAddressRequest = false
863
- const completed = await tracker.reconcile({
864
- addressId: address.id,
865
- type: 'funding:deposit-address:reconcile',
866
- })
867
- includeAddressRequest = true
868
- const replay = await tracker.reconcile({
869
- addressId: address.id,
870
- type: 'funding:deposit-address:reconcile',
871
- })
872
- includeAddressRequest = false
873
- returnOlderDirect = true
874
- const webhook = await tracker.reconcile({
875
- addressId: address.id,
876
- requestId: 'relay-pending-request',
877
- type: 'funding:deposit-address:reconcile',
878
- })
1820
+ const completed = await tracker.reconcile(deliveryFollowUp[0])
1821
+ const completedRecord = await FundingDeposits.get(db, observed.record.id)
1822
+ const [, providerFollowUp] = dispatches
1823
+ if (!providerFollowUp) throw new Error('Expected a provider follow-up message.')
1824
+ indexed = true
1825
+ const attributed = await tracker.reconcile(providerFollowUp[0])
879
1826
  const [record] = await FundingDeposits.listBySourceTransaction(db, {
880
1827
  depositAddressId: address.id,
881
1828
  sourceTransactionHash,
882
1829
  })
883
- const storedAddress = await FundingDepositAddresses.get(db, address.id)
884
1830
 
885
1831
  expect({
1832
+ ambiguousObservation,
1833
+ attributed,
886
1834
  completed,
1835
+ completedRecord,
1836
+ counts,
1837
+ dispatches: dispatches.map(([message, dispatchOptions]) => [
1838
+ { ...message, addressId: '<address-id>' },
1839
+ dispatchOptions,
1840
+ ]),
887
1841
  pending,
888
1842
  record,
889
- replay,
890
- requestTargets,
891
- storedAddress,
892
- webhook,
1843
+ rpcMethods,
1844
+ sourceObservation: observed.type,
1845
+ uncorrelatedObservation,
893
1846
  }).toMatchInlineSnapshot(
894
1847
  {
895
1848
  record: {
@@ -899,30 +1852,250 @@ describe('createTracker', () => {
899
1852
  snapshot: { depositAddressId: expect.any(String) },
900
1853
  updatedAt: expect.any(String),
901
1854
  },
902
- storedAddress: {
1855
+ completedRecord: {
1856
+ createdAt: expect.any(String),
1857
+ depositAddressId: expect.any(String),
903
1858
  id: expect.any(String),
904
- providerOutputToken: expect.any(Object),
905
- snapshot: expect.any(Object),
1859
+ snapshot: { depositAddressId: expect.any(String) },
1860
+ updatedAt: expect.any(String),
906
1861
  },
907
1862
  },
908
1863
  `
909
1864
  {
1865
+ "ambiguousObservation": {
1866
+ "type": "pending",
1867
+ },
1868
+ "attributed": {
1869
+ "created": 0,
1870
+ "type": "completed",
1871
+ "updated": 1,
1872
+ },
910
1873
  "completed": {
911
1874
  "created": 0,
912
1875
  "type": "completed",
913
1876
  "updated": 1,
914
1877
  },
1878
+ "completedRecord": {
1879
+ "createdAt": Any<String>,
1880
+ "depositAddressId": Any<String>,
1881
+ "detectionTrigger": "chain",
1882
+ "environment": "production",
1883
+ "id": Any<String>,
1884
+ "orgId": "org_delivery_observation",
1885
+ "pollObservedAt": null,
1886
+ "projectId": null,
1887
+ "providerOutputAmount": {
1888
+ "baseUnits": "1000000",
1889
+ "currency": "USD",
1890
+ "decimals": 6,
1891
+ "formatted": "1",
1892
+ },
1893
+ "providerOutputToken": {
1894
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1895
+ "currency": "USD",
1896
+ "decimals": 6,
1897
+ "name": "Bridged USDC (Stargate)",
1898
+ "standard": "TIP-20",
1899
+ "symbol": "USDC.e",
1900
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1901
+ "verified": true,
1902
+ },
1903
+ "providerRequestId": null,
1904
+ "providerRequestIds": [],
1905
+ "providerState": {
1906
+ "relay": {
1907
+ "destinationObservedAt": "2026-01-04T00:03:00.000Z",
1908
+ "orderId": "0x3434343434343434343434343434343434343434343434343434343434343434",
1909
+ "sourceDepository": "0x3232323232323232323232323232323232323232",
1910
+ "sourceObservedAt": "2026-01-04T00:03:00.000Z",
1911
+ "sourceSweepTransactionHash": "0x3636363636363636363636363636363636363636363636363636363636363636",
1912
+ },
1913
+ },
1914
+ "providerTransactionHashes": [
1915
+ "0x3636363636363636363636363636363636363636363636363636363636363636",
1916
+ "0x3333333333333333333333333333333333333333333333333333333333333333",
1917
+ ],
1918
+ "providerTransferIndex": 0,
1919
+ "retryState": null,
1920
+ "settlementTransaction": null,
1921
+ "settlementTransactionHash": null,
1922
+ "snapshot": {
1923
+ "depositAddressId": Any<String>,
1924
+ "destinationAmount": {
1925
+ "baseUnits": "1000000",
1926
+ "currency": "USD",
1927
+ "decimals": 6,
1928
+ "formatted": "1",
1929
+ },
1930
+ "destinationChain": {
1931
+ "addressFormat": "hex",
1932
+ "id": "eip155:4217",
1933
+ "kind": "evm",
1934
+ "name": "Tempo",
1935
+ },
1936
+ "destinationToken": {
1937
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1938
+ "currency": "USD",
1939
+ "decimals": 6,
1940
+ "name": "Bridged USDC (Stargate)",
1941
+ "standard": "TIP-20",
1942
+ "symbol": "USDC.e",
1943
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1944
+ "verified": true,
1945
+ },
1946
+ "destinationTransactionHashes": [
1947
+ "0x3333333333333333333333333333333333333333333333333333333333333333",
1948
+ ],
1949
+ "provider": {
1950
+ "id": "relay",
1951
+ "name": "Relay",
1952
+ },
1953
+ "recipient": "0x1111111111111111111111111111111111111111",
1954
+ "refundAddress": "0x3838383838383838383838383838383838383838",
1955
+ "sender": "0x3838383838383838383838383838383838383838",
1956
+ "sourceAmount": {
1957
+ "baseUnits": "1000000",
1958
+ "currency": "USD",
1959
+ "decimals": 6,
1960
+ "formatted": "1",
1961
+ },
1962
+ "sourceChain": {
1963
+ "addressFormat": "hex",
1964
+ "id": "eip155:8453",
1965
+ "kind": "evm",
1966
+ "name": "Base",
1967
+ },
1968
+ "sourceToken": {
1969
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1970
+ "currency": "USD",
1971
+ "decimals": 6,
1972
+ "name": "USD Coin",
1973
+ "standard": "ERC-20",
1974
+ "symbol": "USDC",
1975
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1976
+ "verified": true,
1977
+ },
1978
+ "sourceTransactionHashes": [
1979
+ "0x3737373737373737373737373737373737373737373737373737373737373737",
1980
+ ],
1981
+ },
1982
+ "sourceChainId": "eip155:8453",
1983
+ "sourceTransactionHash": "0x3737373737373737373737373737373737373737373737373737373737373737",
1984
+ "sourceTransferIndex": 0,
1985
+ "status": "completed",
1986
+ "statusReason": null,
1987
+ "subsidyAmount": null,
1988
+ "tempoGasPaid": null,
1989
+ "updatedAt": Any<String>,
1990
+ "version": 3,
1991
+ "webhookReceivedAt": null,
1992
+ },
1993
+ "counts": [
1994
+ [
1995
+ "funding_deposit_delivery_observation_count",
1996
+ 1,
1997
+ {
1998
+ "outcome": "pending",
1999
+ "provider": "relay",
2000
+ },
2001
+ ],
2002
+ [
2003
+ "funding_deposit_reconciliation_count",
2004
+ 1,
2005
+ {
2006
+ "outcome": "follow_up_enqueued",
2007
+ "provider": "relay",
2008
+ "trigger": "chain",
2009
+ },
2010
+ ],
2011
+ [
2012
+ "funding_deposit_reconciliation_count",
2013
+ 1,
2014
+ {
2015
+ "outcome": "completed",
2016
+ "provider": "relay",
2017
+ "trigger": "chain",
2018
+ },
2019
+ ],
2020
+ [
2021
+ "funding_deposit_delivery_observation_count",
2022
+ 1,
2023
+ {
2024
+ "outcome": "verified",
2025
+ "provider": "relay",
2026
+ },
2027
+ ],
2028
+ [
2029
+ "funding_deposit_reconciliation_count",
2030
+ 1,
2031
+ {
2032
+ "outcome": "follow_up_enqueued",
2033
+ "provider": "relay",
2034
+ "trigger": "chain",
2035
+ },
2036
+ ],
2037
+ [
2038
+ "funding_deposit_reconciliation_count",
2039
+ 1,
2040
+ {
2041
+ "outcome": "completed",
2042
+ "provider": "relay",
2043
+ "trigger": "chain",
2044
+ },
2045
+ ],
2046
+ [
2047
+ "funding_deposit_reconciliation_count",
2048
+ 1,
2049
+ {
2050
+ "outcome": "completed",
2051
+ "provider": "relay",
2052
+ "trigger": "chain",
2053
+ },
2054
+ ],
2055
+ ],
2056
+ "dispatches": [
2057
+ [
2058
+ {
2059
+ "addressId": "<address-id>",
2060
+ "followUp": {
2061
+ "attempt": 0,
2062
+ "kind": "delivery",
2063
+ },
2064
+ "trigger": "chain",
2065
+ "type": "funding:deposit-address:reconcile",
2066
+ },
2067
+ {
2068
+ "delaySeconds": 1,
2069
+ },
2070
+ ],
2071
+ [
2072
+ {
2073
+ "addressId": "<address-id>",
2074
+ "followUp": {
2075
+ "attempt": 0,
2076
+ "kind": "provider",
2077
+ },
2078
+ "trigger": "chain",
2079
+ "type": "funding:deposit-address:reconcile",
2080
+ },
2081
+ {
2082
+ "delaySeconds": 1,
2083
+ },
2084
+ ],
2085
+ ],
915
2086
  "pending": {
916
- "created": 1,
2087
+ "created": 0,
917
2088
  "type": "completed",
918
2089
  "updated": 1,
919
2090
  },
920
2091
  "record": {
921
2092
  "createdAt": Any<String>,
922
2093
  "depositAddressId": Any<String>,
2094
+ "detectionTrigger": "chain",
923
2095
  "environment": "production",
924
2096
  "id": Any<String>,
925
- "orgId": "org_pending_reconciliation",
2097
+ "orgId": "org_delivery_observation",
2098
+ "pollObservedAt": null,
926
2099
  "projectId": null,
927
2100
  "providerOutputAmount": {
928
2101
  "baseUnits": "1000000",
@@ -940,13 +2113,22 @@ describe('createTracker', () => {
940
2113
  "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
941
2114
  "verified": true,
942
2115
  },
943
- "providerRequestId": "relay-pending-request",
2116
+ "providerRequestId": "relay-observed-request",
944
2117
  "providerRequestIds": [
945
- "relay-pending-request",
2118
+ "relay-observed-request",
946
2119
  ],
947
- "providerState": null,
2120
+ "providerState": {
2121
+ "relay": {
2122
+ "destinationObservedAt": "2026-01-04T00:03:00.000Z",
2123
+ "orderId": "0x3434343434343434343434343434343434343434343434343434343434343434",
2124
+ "sourceDepository": "0x3232323232323232323232323232323232323232",
2125
+ "sourceObservedAt": "2026-01-04T00:03:00.000Z",
2126
+ "sourceSweepTransactionHash": "0x3636363636363636363636363636363636363636363636363636363636363636",
2127
+ },
2128
+ },
948
2129
  "providerTransactionHashes": [
949
- "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
2130
+ "0x3636363636363636363636363636363636363636363636363636363636363636",
2131
+ "0x3333333333333333333333333333333333333333333333333333333333333333",
950
2132
  ],
951
2133
  "providerTransferIndex": 0,
952
2134
  "retryState": null,
@@ -983,15 +2165,15 @@ describe('createTracker', () => {
983
2165
  "verified": true,
984
2166
  },
985
2167
  "destinationTransactionHashes": [
986
- "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
2168
+ "0x3333333333333333333333333333333333333333333333333333333333333333",
987
2169
  ],
988
2170
  "provider": {
989
2171
  "id": "relay",
990
2172
  "name": "Relay",
991
2173
  },
992
2174
  "recipient": "0x1111111111111111111111111111111111111111",
993
- "refundAddress": "0x9999999999999999999999999999999999999999",
994
- "sender": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2175
+ "refundAddress": "0x3838383838383838383838383838383838383838",
2176
+ "sender": "0x3838383838383838383838383838383838383838",
995
2177
  "sourceAmount": {
996
2178
  "baseUnits": "1000000",
997
2179
  "currency": "USD",
@@ -1015,73 +2197,65 @@ describe('createTracker', () => {
1015
2197
  "verified": true,
1016
2198
  },
1017
2199
  "sourceTransactionHashes": [
1018
- "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
2200
+ "0x3737373737373737373737373737373737373737373737373737373737373737",
1019
2201
  ],
1020
2202
  },
1021
2203
  "sourceChainId": "eip155:8453",
1022
- "sourceTransactionHash": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
2204
+ "sourceTransactionHash": "0x3737373737373737373737373737373737373737373737373737373737373737",
1023
2205
  "sourceTransferIndex": 0,
1024
2206
  "status": "completed",
1025
2207
  "statusReason": null,
1026
2208
  "subsidyAmount": null,
1027
2209
  "tempoGasPaid": null,
1028
2210
  "updatedAt": Any<String>,
1029
- "version": 5,
1030
- },
1031
- "replay": {
1032
- "created": 0,
1033
- "type": "completed",
1034
- "updated": 0,
2211
+ "version": 4,
2212
+ "webhookReceivedAt": null,
1035
2213
  },
1036
- "requestTargets": [
1037
- "/requests/v2?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1038
- "/requests/v2?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1039
- "/requests/v2?id=relay-pending-request&limit=50&sortBy=updatedAt&sortDirection=desc",
1040
- "/requests/v2?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1041
- "/requests/v2?depositAddress=0x8888888888888888888888888888888888888888&includeChildRequests=true&limit=50&sortBy=updatedAt&sortDirection=desc",
1042
- "/requests/v2?id=relay-pending-request&limit=50&sortBy=updatedAt&sortDirection=desc",
2214
+ "rpcMethods": [
2215
+ "eth_getTransactionReceipt",
2216
+ "eth_blockNumber",
2217
+ "eth_getLogs",
2218
+ "eth_getTransactionByHash",
2219
+ "eth_getTransactionByHash",
2220
+ "eth_getTransactionReceipt",
2221
+ "eth_blockNumber",
2222
+ "eth_getLogs",
2223
+ "eth_getTransactionByHash",
2224
+ "eth_blockNumber",
2225
+ "eth_getLogs",
2226
+ "eth_getTransactionByHash",
2227
+ "eth_getTransactionReceipt",
2228
+ "eth_blockNumber",
2229
+ "eth_getTransactionReceipt",
2230
+ "eth_blockNumber",
2231
+ "eth_getLogs",
2232
+ "eth_getTransactionByHash",
2233
+ "eth_blockNumber",
2234
+ "eth_getLogs",
2235
+ "eth_getTransactionReceipt",
2236
+ "eth_blockNumber",
2237
+ "eth_blockNumber",
2238
+ "eth_getLogs",
2239
+ "eth_getTransactionByHash",
2240
+ "eth_getTransactionReceipt",
2241
+ "eth_blockNumber",
2242
+ "eth_getTransactionReceipt",
2243
+ "eth_blockNumber",
1043
2244
  ],
1044
- "storedAddress": {
1045
- "address": "0x8888888888888888888888888888888888888888",
1046
- "apiKeyId": "key_pending_reconciliation",
1047
- "createdAt": "2026-01-02T00:00:00.000Z",
1048
- "deliveryStrategy": "provider",
1049
- "destinationTokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1050
- "environment": "production",
1051
- "id": Any<String>,
1052
- "lastPolledAt": "2027-01-02T00:00:00.000Z",
1053
- "nextPollAt": "2027-01-02T00:01:00.000Z",
1054
- "orgId": "org_pending_reconciliation",
1055
- "pollFailureCount": 0,
1056
- "pollLeaseUntil": null,
1057
- "pollLeaseVersion": 4,
1058
- "projectId": null,
1059
- "providerId": "relay",
1060
- "providerOutputToken": Any<Object>,
1061
- "providerRequestIds": [],
2245
+ "sourceObservation": "created",
2246
+ "uncorrelatedObservation": {
1062
2247
  "providerState": {
1063
- "reconciliation": {
1064
- "pendingRequestIds": [],
1065
- "requestIds": [
1066
- "relay-pending-request",
1067
- ],
1068
- "updatedAt": "2026-01-02T00:03:00.500Z",
2248
+ "relay": {
2249
+ "orderId": "0x3434343434343434343434343434343434343434343434343434343434343434",
2250
+ "sourceDepository": "0x3232323232323232323232323232323232323232",
2251
+ "sourceObservedAt": "2026-01-04T00:03:00.000Z",
2252
+ "sourceSweepTransactionHash": "0x3636363636363636363636363636363636363636363636363636363636363636",
1069
2253
  },
1070
2254
  },
1071
- "recipient": "0x1111111111111111111111111111111111111111",
1072
- "refundAddress": "0x9999999999999999999999999999999999999999",
1073
- "snapshot": Any<Object>,
1074
- "sourceChainId": "eip155:8453",
1075
- "sourceTokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1076
- "status": "active",
1077
- "subsidize": false,
1078
- "updatedAt": "2026-01-02T00:00:00.000Z",
1079
- "version": 1,
1080
- },
1081
- "webhook": {
1082
- "created": 0,
1083
- "type": "completed",
1084
- "updated": 0,
2255
+ "providerTransactionHashes": [
2256
+ "0x3636363636363636363636363636363636363636363636363636363636363636",
2257
+ ],
2258
+ "type": "pending",
1085
2259
  },
1086
2260
  }
1087
2261
  `,
@@ -1136,6 +2310,7 @@ describe('createTracker', () => {
1136
2310
 
1137
2311
  const reconciliation = tracker.reconcile({
1138
2312
  addressId: address.id,
2313
+ trigger: 'webhook',
1139
2314
  type: 'funding:deposit-address:reconcile',
1140
2315
  })
1141
2316
  await providerStarted.promise
@@ -1368,9 +2543,11 @@ describe('createTracker', () => {
1368
2543
  {
1369
2544
  "createdAt": Any<String>,
1370
2545
  "depositAddressId": Any<String>,
2546
+ "detectionTrigger": "poll",
1371
2547
  "environment": "production",
1372
2548
  "id": Any<String>,
1373
2549
  "orgId": "org_source_evidence_reconciliation",
2550
+ "pollObservedAt": null,
1374
2551
  "projectId": null,
1375
2552
  "providerOutputAmount": null,
1376
2553
  "providerOutputToken": {
@@ -1453,13 +2630,16 @@ describe('createTracker', () => {
1453
2630
  "tempoGasPaid": null,
1454
2631
  "updatedAt": Any<String>,
1455
2632
  "version": 2,
2633
+ "webhookReceivedAt": null,
1456
2634
  },
1457
2635
  {
1458
2636
  "createdAt": Any<String>,
1459
2637
  "depositAddressId": Any<String>,
2638
+ "detectionTrigger": "poll",
1460
2639
  "environment": "production",
1461
2640
  "id": Any<String>,
1462
2641
  "orgId": "org_source_evidence_reconciliation",
2642
+ "pollObservedAt": null,
1463
2643
  "projectId": null,
1464
2644
  "providerOutputAmount": null,
1465
2645
  "providerOutputToken": {
@@ -1535,6 +2715,7 @@ describe('createTracker', () => {
1535
2715
  "tempoGasPaid": null,
1536
2716
  "updatedAt": Any<String>,
1537
2717
  "version": 1,
2718
+ "webhookReceivedAt": null,
1538
2719
  },
1539
2720
  ],
1540
2721
  "result": {
@@ -1585,8 +2766,8 @@ describe('createTracker', () => {
1585
2766
  {
1586
2767
  createdAt: '2026-01-03T00:01:00.000Z',
1587
2768
  data: {
1588
- inTxs: [{ hash: sourceTransactionHash }],
1589
- outTxs: [{ hash: destinationTransactionHash }],
2769
+ inTxs: [{ txHash: sourceTransactionHash }],
2770
+ outTxs: [{ txHash: destinationTransactionHash }],
1590
2771
  },
1591
2772
  depositAddress: { depositTxHash: sourceTransactionHash },
1592
2773
  id: 'relay-multiple-transfer-request',
@@ -1679,6 +2860,7 @@ describe('createTracker', () => {
1679
2860
 
1680
2861
  const result = await tracker.reconcile({
1681
2862
  addressId: address.id,
2863
+ trigger: 'webhook',
1682
2864
  type: 'funding:deposit-address:reconcile',
1683
2865
  })
1684
2866
  const records = (
@@ -1723,9 +2905,11 @@ describe('createTracker', () => {
1723
2905
  {
1724
2906
  "createdAt": Any<String>,
1725
2907
  "depositAddressId": Any<String>,
2908
+ "detectionTrigger": "webhook",
1726
2909
  "environment": "production",
1727
2910
  "id": Any<String>,
1728
2911
  "orgId": "org_precision_reconciliation",
2912
+ "pollObservedAt": null,
1729
2913
  "projectId": null,
1730
2914
  "providerOutputAmount": {
1731
2915
  "baseUnits": "2000000",
@@ -1830,13 +3014,16 @@ describe('createTracker', () => {
1830
3014
  "tempoGasPaid": null,
1831
3015
  "updatedAt": Any<String>,
1832
3016
  "version": 3,
3017
+ "webhookReceivedAt": null,
1833
3018
  },
1834
3019
  {
1835
3020
  "createdAt": Any<String>,
1836
3021
  "depositAddressId": Any<String>,
3022
+ "detectionTrigger": "webhook",
1837
3023
  "environment": "production",
1838
3024
  "id": Any<String>,
1839
3025
  "orgId": "org_precision_reconciliation",
3026
+ "pollObservedAt": null,
1840
3027
  "projectId": null,
1841
3028
  "providerOutputAmount": {
1842
3029
  "baseUnits": "1000000",
@@ -1941,6 +3128,7 @@ describe('createTracker', () => {
1941
3128
  "tempoGasPaid": null,
1942
3129
  "updatedAt": Any<String>,
1943
3130
  "version": 3,
3131
+ "webhookReceivedAt": null,
1944
3132
  },
1945
3133
  ],
1946
3134
  "result": {
@@ -1999,19 +3187,86 @@ describe('createTracker', () => {
1999
3187
  function transferLog(options: transferLog.Options) {
2000
3188
  return {
2001
3189
  address: options.token,
3190
+ ...(options.blockNumber !== undefined
3191
+ ? { blockNumber: `0x${options.blockNumber.toString(16)}` }
3192
+ : {}),
2002
3193
  data: `0x${options.amount.toString(16).padStart(64, '0')}`,
2003
3194
  logIndex: `0x${(options.logIndex ?? 0).toString(16)}`,
2004
3195
  topics: [transferTopic, topicAddress(options.sender), topicAddress(options.recipient)],
3196
+ ...(options.transactionHash ? { transactionHash: options.transactionHash } : {}),
2005
3197
  }
2006
3198
  }
2007
3199
 
2008
3200
  declare namespace transferLog {
2009
3201
  type Options = {
2010
3202
  amount: bigint
3203
+ blockNumber?: number | undefined
2011
3204
  logIndex?: number | undefined
2012
3205
  recipient: string
2013
3206
  sender: string
2014
3207
  token: string
3208
+ transactionHash?: string | undefined
3209
+ }
3210
+ }
3211
+
3212
+ function relayDepositLog(options: relayDepositLog.Options) {
3213
+ const event = AbiEvent.from(
3214
+ 'event RelayErc20Deposit(address from, address token, uint256 amount, bytes32 id)',
3215
+ )
3216
+ return {
3217
+ address: options.depository,
3218
+ blockNumber: `0x${options.blockNumber.toString(16)}`,
3219
+ data: AbiParameters.encode(event.inputs, [
3220
+ options.from as Address.Address,
3221
+ options.token as Address.Address,
3222
+ options.amount,
3223
+ options.orderId as Hex.Hex,
3224
+ ]),
3225
+ logIndex: '0x0',
3226
+ topics: [AbiEvent.getSelector(event)],
3227
+ transactionHash: options.transactionHash,
3228
+ }
3229
+ }
3230
+
3231
+ declare namespace relayDepositLog {
3232
+ type Options = {
3233
+ amount: bigint
3234
+ blockNumber: number
3235
+ depository: string
3236
+ from: string
3237
+ orderId: string
3238
+ token: string
3239
+ transactionHash: string
3240
+ }
3241
+ }
3242
+
3243
+ function rpcTransaction(options: rpcTransaction.Options) {
3244
+ return {
3245
+ blockHash: `0x${'39'.repeat(32)}`,
3246
+ blockNumber: `0x${options.blockNumber.toString(16)}`,
3247
+ from: options.from,
3248
+ gas: '0x5208',
3249
+ gasPrice: '0x1',
3250
+ hash: options.hash,
3251
+ input: options.input,
3252
+ nonce: '0x1',
3253
+ r: `0x${'3a'.repeat(32)}`,
3254
+ s: `0x${'3b'.repeat(32)}`,
3255
+ to: options.to,
3256
+ transactionIndex: '0x0',
3257
+ type: '0x0',
3258
+ v: '0x1b',
3259
+ value: '0x0',
3260
+ }
3261
+ }
3262
+
3263
+ declare namespace rpcTransaction {
3264
+ type Options = {
3265
+ blockNumber: number
3266
+ from: string
3267
+ hash: string
3268
+ input: string
3269
+ to: string
2015
3270
  }
2016
3271
  }
2017
3272