tapimo 0.8.0 → 0.10.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 (223) hide show
  1. package/dist/App.d.ts +7410 -7613
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +0 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Webhooks.d.ts +21 -20
  6. package/dist/Webhooks.d.ts.map +1 -1
  7. package/dist/Webhooks.js +66 -23
  8. package/dist/Webhooks.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  10. package/dist/apps/Zones.d.ts +34 -22
  11. package/dist/apps/Zones.d.ts.map +1 -1
  12. package/dist/apps/Zones.js +9 -8
  13. package/dist/apps/Zones.js.map +1 -1
  14. package/dist/apps/data/App.d.ts +3120 -2782
  15. package/dist/apps/data/App.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.d.ts +4 -5
  17. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  18. package/dist/apps/data/FxOracle.js +19 -4
  19. package/dist/apps/data/FxOracle.js.map +1 -1
  20. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  21. package/dist/apps/data/routes/activities.js +32 -27
  22. package/dist/apps/data/routes/activities.js.map +1 -1
  23. package/dist/apps/data/routes/addresses.d.ts +283 -52
  24. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.d.ts +291 -21
  26. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  27. package/dist/apps/data/routes/balances.js +232 -66
  28. package/dist/apps/data/routes/balances.js.map +1 -1
  29. package/dist/apps/data/routes/blocks.d.ts +4 -4
  30. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  31. package/dist/apps/data/routes/earn.d.ts +642 -126
  32. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  33. package/dist/apps/data/routes/earn.js +627 -58
  34. package/dist/apps/data/routes/earn.js.map +1 -1
  35. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  36. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  37. package/dist/apps/data/routes/exchanges.js +114 -118
  38. package/dist/apps/data/routes/exchanges.js.map +1 -1
  39. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  40. package/dist/apps/data/routes/receipts.js +68 -47
  41. package/dist/apps/data/routes/receipts.js.map +1 -1
  42. package/dist/apps/data/routes/tokens.d.ts +2 -0
  43. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  44. package/dist/apps/data/routes/transactions.d.ts +59 -51
  45. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  46. package/dist/apps/data/routes/transactions.js +25 -23
  47. package/dist/apps/data/routes/transactions.js.map +1 -1
  48. package/dist/apps/data/routes/transfers.d.ts +40 -18
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +618 -105
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/valuation.d.ts +21 -21
  53. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  54. package/dist/apps/data/routes/valuation.js +2 -2
  55. package/dist/apps/data/routes/valuation.js.map +1 -1
  56. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  57. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  58. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  59. package/dist/apps/data/routes/webhooks.js +484 -342
  60. package/dist/apps/data/routes/webhooks.js.map +1 -1
  61. package/dist/apps/data/routes/zones.d.ts +12 -12
  62. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  63. package/dist/apps/data/routes/zones.js +3 -1
  64. package/dist/apps/data/routes/zones.js.map +1 -1
  65. package/dist/apps/funding/App.d.ts +127 -386
  66. package/dist/apps/funding/App.d.ts.map +1 -1
  67. package/dist/apps/funding/App.js +20 -5
  68. package/dist/apps/funding/App.js.map +1 -1
  69. package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
  70. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  71. package/dist/apps/funding/routes/deposit-addresses.js +21 -103
  72. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  73. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  74. package/dist/apps/funding/routes/transfers.js +5 -5
  75. package/dist/apps/funding/routes/transfers.js.map +1 -1
  76. package/dist/apps/management/App.d.ts +429 -727
  77. package/dist/apps/management/App.d.ts.map +1 -1
  78. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  79. package/dist/apps/management/routes/billing.d.ts +72 -72
  80. package/dist/apps/management/routes/faucet.d.ts +6 -6
  81. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  82. package/dist/apps/management/routes/me.d.ts +6 -6
  83. package/dist/apps/management/routes/members.d.ts +30 -30
  84. package/dist/apps/management/routes/projects.d.ts +20 -20
  85. package/dist/apps/management/routes/usage.d.ts +24 -24
  86. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  87. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/mpp/App.d.ts +12 -0
  89. package/dist/apps/mpp/App.d.ts.map +1 -1
  90. package/dist/apps/mpp/Mpp.d.ts +3 -0
  91. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  92. package/dist/apps/mpp/Mpp.js +3 -0
  93. package/dist/apps/mpp/Mpp.js.map +1 -1
  94. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  95. package/dist/apps/mpp/internal/Adapter.js +9 -2
  96. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  97. package/dist/db/Db.d.ts +2 -2
  98. package/dist/db/Db.d.ts.map +1 -1
  99. package/dist/db/Db.js +25 -15
  100. package/dist/db/Db.js.map +1 -1
  101. package/dist/db/tables/fundingDeposits.d.ts +18 -1
  102. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  103. package/dist/db/tables/fundingDeposits.js +46 -2
  104. package/dist/db/tables/fundingDeposits.js.map +1 -1
  105. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  106. package/dist/db/tables/sponsoredTransactions.js +4 -1
  107. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  108. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  109. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  110. package/dist/db/tables/webhookSubscriptions.js +76 -3
  111. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  112. package/dist/internal/Auth.d.ts +4 -0
  113. package/dist/internal/Auth.d.ts.map +1 -1
  114. package/dist/internal/Auth.js +11 -2
  115. package/dist/internal/Auth.js.map +1 -1
  116. package/dist/internal/MetricSink.d.ts +112 -3
  117. package/dist/internal/MetricSink.d.ts.map +1 -1
  118. package/dist/internal/MetricSink.js +116 -3
  119. package/dist/internal/MetricSink.js.map +1 -1
  120. package/dist/internal/Schema.js +1 -1
  121. package/dist/internal/Schema.js.map +1 -1
  122. package/dist/internal/Tidx.d.ts +1 -3
  123. package/dist/internal/Tidx.d.ts.map +1 -1
  124. package/dist/internal/Tidx.js +1 -3
  125. package/dist/internal/Tidx.js.map +1 -1
  126. package/dist/internal/Viem.d.ts.map +1 -1
  127. package/dist/internal/Viem.js +2 -1
  128. package/dist/internal/Viem.js.map +1 -1
  129. package/dist/internal/WebhookDestination.d.ts +2 -0
  130. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  131. package/dist/internal/WebhookDestination.js +11 -0
  132. package/dist/internal/WebhookDestination.js.map +1 -1
  133. package/dist/internal/WebhookTransfer.d.ts +55 -0
  134. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  135. package/dist/internal/WebhookTransfer.js +296 -0
  136. package/dist/internal/WebhookTransfer.js.map +1 -0
  137. package/dist/internal/Webhooks.d.ts +37 -0
  138. package/dist/internal/Webhooks.d.ts.map +1 -1
  139. package/dist/internal/Webhooks.js +87 -13
  140. package/dist/internal/Webhooks.js.map +1 -1
  141. package/dist/internal/funding/Provider.d.ts +10 -6
  142. package/dist/internal/funding/Provider.d.ts.map +1 -1
  143. package/dist/internal/funding/Provider.js +11 -7
  144. package/dist/internal/funding/Provider.js.map +1 -1
  145. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  146. package/dist/internal/funding/Reconciliation.js +229 -16
  147. package/dist/internal/funding/Reconciliation.js.map +1 -1
  148. package/dist/internal/funding/providers/relay.d.ts +3 -0
  149. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  150. package/dist/internal/funding/providers/relay.js +18 -3
  151. package/dist/internal/funding/providers/relay.js.map +1 -1
  152. package/package.json +3 -3
  153. package/src/App.test.ts +32 -0
  154. package/src/App.ts +4 -1
  155. package/src/Client.test-d.ts +116 -38
  156. package/src/Webhooks.bench.ts +714 -0
  157. package/src/Webhooks.memory.bench.ts +250 -0
  158. package/src/Webhooks.test.ts +77 -6
  159. package/src/Webhooks.ts +83 -29
  160. package/src/apps/Zones.test.ts +5 -4
  161. package/src/apps/Zones.ts +9 -8
  162. package/src/apps/composition.test.ts +39 -10
  163. package/src/apps/data/FxOracle.test.ts +18 -0
  164. package/src/apps/data/FxOracle.ts +28 -5
  165. package/src/apps/data/routes/activities.test.ts +19 -0
  166. package/src/apps/data/routes/activities.ts +34 -30
  167. package/src/apps/data/routes/balances.test.ts +161 -10
  168. package/src/apps/data/routes/balances.ts +339 -147
  169. package/src/apps/data/routes/blocks.test.ts +12 -6
  170. package/src/apps/data/routes/earn.test.ts +286 -9
  171. package/src/apps/data/routes/earn.ts +841 -58
  172. package/src/apps/data/routes/exchanges.test.ts +7 -6
  173. package/src/apps/data/routes/exchanges.ts +124 -117
  174. package/src/apps/data/routes/receipts.test.ts +24 -7
  175. package/src/apps/data/routes/receipts.ts +67 -50
  176. package/src/apps/data/routes/tokens.test.ts +51 -0
  177. package/src/apps/data/routes/transactions.test.ts +42 -0
  178. package/src/apps/data/routes/transactions.ts +44 -39
  179. package/src/apps/data/routes/transfers.test.ts +402 -24
  180. package/src/apps/data/routes/transfers.ts +708 -101
  181. package/src/apps/data/routes/valuation.test.ts +26 -0
  182. package/src/apps/data/routes/valuation.ts +6 -2
  183. package/src/apps/data/routes/webhooks.test.ts +414 -130
  184. package/src/apps/data/routes/webhooks.ts +603 -419
  185. package/src/apps/data/routes/zones.ts +6 -1
  186. package/src/apps/funding/App.ts +20 -5
  187. package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
  188. package/src/apps/funding/routes/deposit-addresses.ts +34 -117
  189. package/src/apps/funding/routes/quotes.test.ts +1 -1
  190. package/src/apps/funding/routes/transfers.test.ts +6 -25
  191. package/src/apps/funding/routes/transfers.ts +5 -5
  192. package/src/apps/mpp/Mpp.test.ts +13 -1
  193. package/src/apps/mpp/Mpp.ts +8 -0
  194. package/src/apps/mpp/internal/Adapter.ts +10 -2
  195. package/src/db/Db.test.ts +40 -6
  196. package/src/db/Db.ts +42 -19
  197. package/src/db/tables/fundingDeposits.test.ts +101 -0
  198. package/src/db/tables/fundingDeposits.ts +71 -2
  199. package/src/db/tables/sponsoredTransactions.ts +9 -2
  200. package/src/db/tables/webhookSubscriptions.ts +116 -4
  201. package/src/internal/Auth.test.ts +28 -0
  202. package/src/internal/Auth.ts +15 -1
  203. package/src/internal/EarnVaults.test.ts +2 -2
  204. package/src/internal/MetricSink.test.ts +518 -0
  205. package/src/internal/MetricSink.ts +266 -9
  206. package/src/internal/Schema.ts +1 -1
  207. package/src/internal/Tidx.test.ts +4 -4
  208. package/src/internal/Tidx.ts +1 -3
  209. package/src/internal/Viem.test.ts +8 -0
  210. package/src/internal/Viem.ts +2 -1
  211. package/src/internal/WebhookDestination.test.ts +20 -0
  212. package/src/internal/WebhookDestination.ts +10 -0
  213. package/src/internal/WebhookTransfer.ts +426 -0
  214. package/src/internal/Webhooks.test.ts +137 -0
  215. package/src/internal/Webhooks.ts +131 -17
  216. package/src/internal/funding/Provider.test.ts +47 -7
  217. package/src/internal/funding/Provider.ts +16 -11
  218. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  219. package/src/internal/funding/Reconciliation.test.ts +1532 -54
  220. package/src/internal/funding/Reconciliation.ts +296 -53
  221. package/src/internal/funding/SourceObservation.test.ts +3 -2
  222. package/src/internal/funding/providers/relay.test.ts +1 -4
  223. package/src/internal/funding/providers/relay.ts +23 -3
@@ -17,6 +17,7 @@ import * as Evidence from './Evidence.js'
17
17
  import * as Provider from './Provider.js'
18
18
  import * as Reconciliation from './Reconciliation.js'
19
19
  import * as Relay from './providers/relay.js'
20
+ import * as SourceObservation from './SourceObservation.js'
20
21
 
21
22
  const db = TestApp.database()
22
23
  const transferTopic = AbiEvent.getSelector('event Transfer(address,address,uint256)')
@@ -474,6 +475,7 @@ describe('createTracker', () => {
474
475
  createdAt: '2026-01-06T00:01:00.000Z',
475
476
  destinationTransactionHashes: [destinationTransactionHash],
476
477
  id: 'unsubsidized-request',
478
+ refundTransactionHashes: [],
477
479
  sourceTransactionHashes: [sourceTransactionHash],
478
480
  status: 'success',
479
481
  updatedAt: '2026-01-06T00:02:00.000Z',
@@ -625,6 +627,1244 @@ describe('createTracker', () => {
625
627
  }
626
628
  })
627
629
 
630
+ test('verifies Relay refunds on the source chain before completing recovery', async () => {
631
+ const route = TestFunding.transferSnapshot()
632
+ const depositAddress = `0x${'48'.repeat(20)}`
633
+ const firstRefundTransactionHash = `0x${'ce'.repeat(32)}`
634
+ const refundAddress = `0x${'49'.repeat(20)}`
635
+ const secondRefundTransactionHash = `0x${'cd'.repeat(32)}`
636
+ const settlementTransactionHash = `0x${'cf'.repeat(32)}`
637
+ const sourceTransactionHash = `0x${'ac'.repeat(32)}`
638
+ const address = await DepositAddress.create(db, {
639
+ apiKeyId: 'key_refund_reconciliation',
640
+ deliveryStrategy: 'provider',
641
+ environment: 'production',
642
+ now: new Date('2026-01-07T00:00:00.000Z'),
643
+ orgId: 'org_refund_reconciliation',
644
+ providerOutputToken: route.destinationToken,
645
+ snapshot: {
646
+ address: depositAddress,
647
+ destinationChain: route.destinationChain,
648
+ destinationToken: route.destinationToken,
649
+ provider: { id: 'relay', name: 'Relay' },
650
+ recipient: route.recipient,
651
+ refundAddress,
652
+ sourceChain: route.sourceChain,
653
+ sourceToken: route.sourceToken,
654
+ subsidize: false,
655
+ },
656
+ })
657
+ const counts: Parameters<Metrics.Metrics['count']>[] = []
658
+ const metrics = Metrics.from({
659
+ count(...parameters) {
660
+ counts.push(parameters)
661
+ },
662
+ flush() {},
663
+ gauge() {},
664
+ histogram() {},
665
+ })
666
+ let secondRefundVisible = false
667
+ const server = await TestRelay.createServer(async (request, response) => {
668
+ response.writeHead(200, { 'content-type': 'application/json' })
669
+ if (request.method === 'GET') {
670
+ response.end(
671
+ JSON.stringify({
672
+ requests: [
673
+ {
674
+ createdAt: '2026-01-07T00:01:00.000Z',
675
+ data: {
676
+ outTxs: [
677
+ { txHash: firstRefundTransactionHash },
678
+ { txHash: firstRefundTransactionHash },
679
+ { txHash: secondRefundTransactionHash },
680
+ { txHash: settlementTransactionHash },
681
+ ],
682
+ },
683
+ depositAddress: { depositTxHash: sourceTransactionHash },
684
+ id: 'relay-refund-request',
685
+ refundCurrencyData: { amount: '900000' },
686
+ status: 'refund',
687
+ updatedAt: '2026-01-07T00:02:00.000Z',
688
+ },
689
+ ],
690
+ }),
691
+ )
692
+ return
693
+ }
694
+
695
+ const chunks: Uint8Array[] = []
696
+ for await (const chunk of request) chunks.push(chunk)
697
+ const body = JSON.parse(NodeBuffer.Buffer.concat(chunks).toString()) as {
698
+ id: number
699
+ method: string
700
+ params?: readonly string[] | undefined
701
+ }
702
+ const hash = body.params?.[0]
703
+ const result = (() => {
704
+ if (body.method === 'eth_blockNumber') return '0x100'
705
+ if (hash === secondRefundTransactionHash && !secondRefundVisible) return null
706
+ const log = (() => {
707
+ if (hash === sourceTransactionHash)
708
+ return transferLog({
709
+ amount: 1_000_000n,
710
+ recipient: depositAddress,
711
+ sender: `0x${'4a'.repeat(20)}`,
712
+ token: route.sourceToken.address,
713
+ })
714
+ if (hash === firstRefundTransactionHash)
715
+ return transferLog({
716
+ amount: 400_000n,
717
+ recipient: refundAddress,
718
+ sender: `0x${'4b'.repeat(20)}`,
719
+ token: route.sourceToken.address,
720
+ })
721
+ if (hash === secondRefundTransactionHash)
722
+ return transferLog({
723
+ amount: 500_000n,
724
+ recipient: refundAddress,
725
+ sender: `0x${'4b'.repeat(20)}`,
726
+ token: route.sourceToken.address,
727
+ })
728
+ return transferLog({
729
+ amount: 100_000n,
730
+ recipient: `0x${'4c'.repeat(20)}`,
731
+ sender: `0x${'4b'.repeat(20)}`,
732
+ token: route.sourceToken.address,
733
+ })
734
+ })()
735
+ return { blockNumber: '0x80', logs: [log], status: '0x1' }
736
+ })()
737
+ response.end(JSON.stringify({ id: body.id, jsonrpc: '2.0', result }))
738
+ })
739
+
740
+ try {
741
+ await FundingCatalog.publish(db, {
742
+ chainTokens: [],
743
+ chains: [
744
+ {
745
+ aliases: [],
746
+ id: route.destinationChain.id as Chain.Id,
747
+ name: route.destinationChain.name,
748
+ parentChainId: null,
749
+ rpcUrls: [server.url],
750
+ slug: 'destination-refund',
751
+ },
752
+ {
753
+ aliases: [],
754
+ id: route.sourceChain.id as Chain.Id,
755
+ name: route.sourceChain.name,
756
+ parentChainId: null,
757
+ rpcUrls: [server.url],
758
+ slug: 'source-refund',
759
+ },
760
+ ],
761
+ routes: [],
762
+ tokens: [],
763
+ })
764
+ const provider = Relay.relay({ apiKey: 'relay-api-key', baseUrl: server.url })
765
+ const request = (
766
+ await provider.listDepositAddressRequests!(
767
+ { address: depositAddress },
768
+ new AbortController().signal,
769
+ )
770
+ ).requests[0]
771
+ if (!request) throw new Error('Expected a Relay refund request.')
772
+ const verifyTransfers = Evidence.createVerifier({ db })
773
+ const first = await Reconciliation.reconcileRequest(db, {
774
+ address,
775
+ metrics,
776
+ request,
777
+ verifyTransfers,
778
+ })
779
+ const [firstRecord] = await FundingDeposits.listBySourceTransaction(db, {
780
+ depositAddressId: address.id,
781
+ sourceTransactionHash,
782
+ })
783
+ secondRefundVisible = true
784
+ const second = await Reconciliation.reconcileRequest(db, {
785
+ address,
786
+ metrics,
787
+ request,
788
+ verifyTransfers,
789
+ })
790
+ const duplicate = await Reconciliation.reconcileRequest(db, {
791
+ address,
792
+ metrics,
793
+ request,
794
+ verifyTransfers,
795
+ })
796
+ const [record] = await FundingDeposits.listBySourceTransaction(db, {
797
+ depositAddressId: address.id,
798
+ sourceTransactionHash,
799
+ })
800
+
801
+ expect({
802
+ counts,
803
+ duplicate,
804
+ first,
805
+ firstRecord,
806
+ record,
807
+ request,
808
+ second,
809
+ }).toMatchInlineSnapshot(
810
+ {
811
+ firstRecord: {
812
+ createdAt: expect.any(String),
813
+ depositAddressId: expect.any(String),
814
+ id: expect.any(String),
815
+ snapshot: { depositAddressId: expect.any(String) },
816
+ updatedAt: expect.any(String),
817
+ },
818
+ record: {
819
+ createdAt: expect.any(String),
820
+ depositAddressId: expect.any(String),
821
+ id: expect.any(String),
822
+ snapshot: { depositAddressId: expect.any(String) },
823
+ updatedAt: expect.any(String),
824
+ },
825
+ },
826
+ `
827
+ {
828
+ "counts": [
829
+ [
830
+ "funding_deposit_refund_count",
831
+ 1,
832
+ {
833
+ "outcome": "pending",
834
+ "provider": "relay",
835
+ },
836
+ ],
837
+ [
838
+ "funding_deposit_refund_count",
839
+ 1,
840
+ {
841
+ "outcome": "verified",
842
+ "provider": "relay",
843
+ },
844
+ ],
845
+ ],
846
+ "duplicate": {
847
+ "created": 0,
848
+ "terminal": true,
849
+ "updated": 0,
850
+ },
851
+ "first": {
852
+ "created": 1,
853
+ "followUp": "delivery",
854
+ "terminal": false,
855
+ "updated": 1,
856
+ },
857
+ "firstRecord": {
858
+ "createdAt": Any<String>,
859
+ "depositAddressId": Any<String>,
860
+ "detectionTrigger": "poll",
861
+ "environment": "production",
862
+ "id": Any<String>,
863
+ "orgId": "org_refund_reconciliation",
864
+ "pollObservedAt": null,
865
+ "projectId": null,
866
+ "providerOutputAmount": null,
867
+ "providerOutputToken": {
868
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
869
+ "currency": "USD",
870
+ "decimals": 6,
871
+ "name": "Bridged USDC (Stargate)",
872
+ "standard": "TIP-20",
873
+ "symbol": "USDC.e",
874
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
875
+ "verified": true,
876
+ },
877
+ "providerRequestId": "relay-refund-request",
878
+ "providerRequestIds": [
879
+ "relay-refund-request",
880
+ ],
881
+ "providerState": null,
882
+ "providerTransactionHashes": [
883
+ "0xcececececececececececececececececececececececececececececececece",
884
+ "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
885
+ "0xcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf",
886
+ ],
887
+ "providerTransferIndex": 0,
888
+ "retryState": null,
889
+ "settlementTransaction": null,
890
+ "settlementTransactionHash": null,
891
+ "snapshot": {
892
+ "depositAddressId": Any<String>,
893
+ "destinationChain": {
894
+ "addressFormat": "hex",
895
+ "id": "eip155:4217",
896
+ "kind": "evm",
897
+ "name": "Tempo",
898
+ },
899
+ "destinationToken": {
900
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
901
+ "currency": "USD",
902
+ "decimals": 6,
903
+ "name": "Bridged USDC (Stargate)",
904
+ "standard": "TIP-20",
905
+ "symbol": "USDC.e",
906
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
907
+ "verified": true,
908
+ },
909
+ "provider": {
910
+ "id": "relay",
911
+ "name": "Relay",
912
+ },
913
+ "recipient": "0x1111111111111111111111111111111111111111",
914
+ "refundAddress": "0x4949494949494949494949494949494949494949",
915
+ "sender": "0x4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a",
916
+ "sourceAmount": {
917
+ "baseUnits": "1000000",
918
+ "currency": "USD",
919
+ "decimals": 6,
920
+ "formatted": "1",
921
+ },
922
+ "sourceChain": {
923
+ "addressFormat": "hex",
924
+ "id": "eip155:8453",
925
+ "kind": "evm",
926
+ "name": "Base",
927
+ },
928
+ "sourceToken": {
929
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
930
+ "currency": "USD",
931
+ "decimals": 6,
932
+ "name": "USD Coin",
933
+ "standard": "ERC-20",
934
+ "symbol": "USDC",
935
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
936
+ "verified": true,
937
+ },
938
+ "sourceTransactionHashes": [
939
+ "0xacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacac",
940
+ ],
941
+ },
942
+ "sourceChainId": "eip155:8453",
943
+ "sourceTransactionHash": "0xacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacac",
944
+ "sourceTransferIndex": 0,
945
+ "status": "refunding",
946
+ "statusReason": null,
947
+ "subsidyAmount": null,
948
+ "tempoGasPaid": null,
949
+ "updatedAt": Any<String>,
950
+ "version": 3,
951
+ "webhookReceivedAt": null,
952
+ },
953
+ "record": {
954
+ "createdAt": Any<String>,
955
+ "depositAddressId": Any<String>,
956
+ "detectionTrigger": "poll",
957
+ "environment": "production",
958
+ "id": Any<String>,
959
+ "orgId": "org_refund_reconciliation",
960
+ "pollObservedAt": null,
961
+ "projectId": null,
962
+ "providerOutputAmount": null,
963
+ "providerOutputToken": {
964
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
965
+ "currency": "USD",
966
+ "decimals": 6,
967
+ "name": "Bridged USDC (Stargate)",
968
+ "standard": "TIP-20",
969
+ "symbol": "USDC.e",
970
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
971
+ "verified": true,
972
+ },
973
+ "providerRequestId": "relay-refund-request",
974
+ "providerRequestIds": [
975
+ "relay-refund-request",
976
+ ],
977
+ "providerState": null,
978
+ "providerTransactionHashes": [
979
+ "0xcececececececececececececececececececececececececececececececece",
980
+ "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
981
+ "0xcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf",
982
+ ],
983
+ "providerTransferIndex": 0,
984
+ "retryState": null,
985
+ "settlementTransaction": null,
986
+ "settlementTransactionHash": null,
987
+ "snapshot": {
988
+ "depositAddressId": Any<String>,
989
+ "destinationChain": {
990
+ "addressFormat": "hex",
991
+ "id": "eip155:4217",
992
+ "kind": "evm",
993
+ "name": "Tempo",
994
+ },
995
+ "destinationToken": {
996
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
997
+ "currency": "USD",
998
+ "decimals": 6,
999
+ "name": "Bridged USDC (Stargate)",
1000
+ "standard": "TIP-20",
1001
+ "symbol": "USDC.e",
1002
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1003
+ "verified": true,
1004
+ },
1005
+ "provider": {
1006
+ "id": "relay",
1007
+ "name": "Relay",
1008
+ },
1009
+ "recipient": "0x1111111111111111111111111111111111111111",
1010
+ "refundAddress": "0x4949494949494949494949494949494949494949",
1011
+ "refundAmount": {
1012
+ "baseUnits": "900000",
1013
+ "currency": "USD",
1014
+ "decimals": 6,
1015
+ "formatted": "0.9",
1016
+ },
1017
+ "refundTransactionHashes": [
1018
+ "0xcececececececececececececececececececececececececececececececece",
1019
+ "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
1020
+ ],
1021
+ "sender": "0x4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a",
1022
+ "sourceAmount": {
1023
+ "baseUnits": "1000000",
1024
+ "currency": "USD",
1025
+ "decimals": 6,
1026
+ "formatted": "1",
1027
+ },
1028
+ "sourceChain": {
1029
+ "addressFormat": "hex",
1030
+ "id": "eip155:8453",
1031
+ "kind": "evm",
1032
+ "name": "Base",
1033
+ },
1034
+ "sourceToken": {
1035
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1036
+ "currency": "USD",
1037
+ "decimals": 6,
1038
+ "name": "USD Coin",
1039
+ "standard": "ERC-20",
1040
+ "symbol": "USDC",
1041
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1042
+ "verified": true,
1043
+ },
1044
+ "sourceTransactionHashes": [
1045
+ "0xacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacac",
1046
+ ],
1047
+ },
1048
+ "sourceChainId": "eip155:8453",
1049
+ "sourceTransactionHash": "0xacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacac",
1050
+ "sourceTransferIndex": 0,
1051
+ "status": "refunded",
1052
+ "statusReason": null,
1053
+ "subsidyAmount": null,
1054
+ "tempoGasPaid": null,
1055
+ "updatedAt": Any<String>,
1056
+ "version": 5,
1057
+ "webhookReceivedAt": null,
1058
+ },
1059
+ "request": {
1060
+ "createdAt": "2026-01-07T00:01:00.000Z",
1061
+ "destinationTransactionHashes": [],
1062
+ "id": "relay-refund-request",
1063
+ "refundAmountExpected": "900000",
1064
+ "refundTransactionHashes": [
1065
+ "0xcececececececececececececececececececececececececececececececece",
1066
+ "0xcececececececececececececececececececececececececececececececece",
1067
+ "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
1068
+ "0xcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf",
1069
+ ],
1070
+ "sourceTransactionHashes": [
1071
+ "0xacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacacac",
1072
+ ],
1073
+ "status": "refund",
1074
+ "updatedAt": "2026-01-07T00:02:00.000Z",
1075
+ },
1076
+ "second": {
1077
+ "created": 0,
1078
+ "terminal": true,
1079
+ "updated": 1,
1080
+ },
1081
+ }
1082
+ `,
1083
+ )
1084
+ } finally {
1085
+ await server.closeAsync()
1086
+ }
1087
+ })
1088
+
1089
+ test('preserves refund allocations when retrying a partially committed request', async () => {
1090
+ const route = TestFunding.transferSnapshot()
1091
+ const depositAddress = `0x${'50'.repeat(20)}`
1092
+ const refundAddress = `0x${'51'.repeat(20)}`
1093
+ const refundTransactionHash = `0x${'d0'.repeat(32)}`
1094
+ const sourceTransactionHash = `0x${'b0'.repeat(32)}`
1095
+ const address = await DepositAddress.create(db, {
1096
+ apiKeyId: 'key_refund_retry',
1097
+ deliveryStrategy: 'provider',
1098
+ environment: 'production',
1099
+ now: new Date('2026-01-09T00:00:00.000Z'),
1100
+ orgId: 'org_refund_retry',
1101
+ providerOutputToken: route.destinationToken,
1102
+ snapshot: {
1103
+ address: depositAddress,
1104
+ destinationChain: route.destinationChain,
1105
+ destinationToken: route.destinationToken,
1106
+ provider: { id: 'relay', name: 'Relay' },
1107
+ recipient: route.recipient,
1108
+ refundAddress,
1109
+ sourceChain: route.sourceChain,
1110
+ sourceToken: route.sourceToken,
1111
+ subsidize: false,
1112
+ },
1113
+ })
1114
+ const first = await Deposit.create(db, {
1115
+ id: 'fdp_refund_retry_0',
1116
+ now: new Date('2026-01-09T00:01:00.000Z'),
1117
+ providerRequestId: 'relay-refund-retry-request',
1118
+ providerTransferIndex: 0,
1119
+ snapshot: {
1120
+ depositAddressId: address.id,
1121
+ destinationChain: route.destinationChain,
1122
+ destinationToken: route.destinationToken,
1123
+ provider: { id: 'relay', name: 'Relay' },
1124
+ recipient: route.recipient,
1125
+ refundAddress,
1126
+ sourceAmount: {
1127
+ baseUnits: '1000000',
1128
+ currency: route.sourceToken.currency,
1129
+ decimals: route.sourceToken.decimals,
1130
+ formatted: '1',
1131
+ },
1132
+ sourceChain: route.sourceChain,
1133
+ sourceToken: route.sourceToken,
1134
+ sourceTransactionHashes: [sourceTransactionHash],
1135
+ },
1136
+ sourceTransactionHash,
1137
+ sourceTransferIndex: 0,
1138
+ })
1139
+ await Deposit.create(db, {
1140
+ id: 'fdp_refund_retry_1',
1141
+ now: new Date('2026-01-09T00:01:01.000Z'),
1142
+ providerRequestId: 'relay-refund-retry-request',
1143
+ providerTransferIndex: 1,
1144
+ snapshot: {
1145
+ depositAddressId: address.id,
1146
+ destinationChain: route.destinationChain,
1147
+ destinationToken: route.destinationToken,
1148
+ provider: { id: 'relay', name: 'Relay' },
1149
+ recipient: route.recipient,
1150
+ refundAddress,
1151
+ sourceAmount: {
1152
+ baseUnits: '2000000',
1153
+ currency: route.sourceToken.currency,
1154
+ decimals: route.sourceToken.decimals,
1155
+ formatted: '2',
1156
+ },
1157
+ sourceChain: route.sourceChain,
1158
+ sourceToken: route.sourceToken,
1159
+ sourceTransactionHashes: [sourceTransactionHash],
1160
+ },
1161
+ sourceTransactionHash,
1162
+ sourceTransferIndex: 1,
1163
+ })
1164
+ const refunding = await Deposit.transition(db, {
1165
+ expectedVersion: first.version,
1166
+ id: first.id,
1167
+ providerTransactionHashes: [refundTransactionHash],
1168
+ status: 'refunding',
1169
+ })
1170
+ if (refunding.type !== 'applied') throw new Error('Expected the first deposit to refund.')
1171
+ const refunded = await Deposit.transition(db, {
1172
+ expectedVersion: refunding.record.version,
1173
+ id: refunding.record.id,
1174
+ snapshot: {
1175
+ ...refunding.record.snapshot,
1176
+ refundAmount: {
1177
+ baseUnits: '300000',
1178
+ currency: route.sourceToken.currency,
1179
+ decimals: route.sourceToken.decimals,
1180
+ formatted: '0.3',
1181
+ },
1182
+ refundTransactionHashes: [refundTransactionHash],
1183
+ },
1184
+ status: 'refunded',
1185
+ })
1186
+ if (refunded.type !== 'applied') throw new Error('Expected the first deposit to be refunded.')
1187
+
1188
+ const server = await TestRelay.createServer(async (request, response) => {
1189
+ const chunks: Uint8Array[] = []
1190
+ for await (const chunk of request) chunks.push(chunk)
1191
+ const body = JSON.parse(NodeBuffer.Buffer.concat(chunks).toString()) as {
1192
+ id: number
1193
+ method: string
1194
+ params?: readonly string[] | undefined
1195
+ }
1196
+ const hash = body.params?.[0]
1197
+ const result = (() => {
1198
+ if (body.method === 'eth_blockNumber') return '0x100'
1199
+ if (hash === sourceTransactionHash)
1200
+ return {
1201
+ blockNumber: '0x80',
1202
+ logs: [
1203
+ transferLog({
1204
+ amount: 1_000_000n,
1205
+ logIndex: 0,
1206
+ recipient: depositAddress,
1207
+ sender: `0x${'52'.repeat(20)}`,
1208
+ token: route.sourceToken.address,
1209
+ }),
1210
+ transferLog({
1211
+ amount: 2_000_000n,
1212
+ logIndex: 1,
1213
+ recipient: depositAddress,
1214
+ sender: `0x${'53'.repeat(20)}`,
1215
+ token: route.sourceToken.address,
1216
+ }),
1217
+ ],
1218
+ status: '0x1',
1219
+ }
1220
+ return {
1221
+ blockNumber: '0x80',
1222
+ logs: [
1223
+ transferLog({
1224
+ amount: 900_000n,
1225
+ recipient: refundAddress,
1226
+ sender: `0x${'54'.repeat(20)}`,
1227
+ token: route.sourceToken.address,
1228
+ }),
1229
+ ],
1230
+ status: '0x1',
1231
+ }
1232
+ })()
1233
+ response.writeHead(200, { 'content-type': 'application/json' })
1234
+ response.end(JSON.stringify({ id: body.id, jsonrpc: '2.0', result }))
1235
+ })
1236
+
1237
+ try {
1238
+ await FundingCatalog.publish(db, {
1239
+ chainTokens: [],
1240
+ chains: [
1241
+ {
1242
+ aliases: [],
1243
+ id: route.destinationChain.id as Chain.Id,
1244
+ name: route.destinationChain.name,
1245
+ parentChainId: null,
1246
+ rpcUrls: [server.url],
1247
+ slug: 'destination-refund-retry',
1248
+ },
1249
+ {
1250
+ aliases: [],
1251
+ id: route.sourceChain.id as Chain.Id,
1252
+ name: route.sourceChain.name,
1253
+ parentChainId: null,
1254
+ rpcUrls: [server.url],
1255
+ slug: 'source-refund-retry',
1256
+ },
1257
+ ],
1258
+ routes: [],
1259
+ tokens: [],
1260
+ })
1261
+ const result = await Reconciliation.reconcileRequest(db, {
1262
+ address,
1263
+ request: {
1264
+ createdAt: '2026-01-09T00:01:00.000Z',
1265
+ destinationTransactionHashes: [],
1266
+ id: 'relay-refund-retry-request',
1267
+ refundTransactionHashes: [refundTransactionHash],
1268
+ sourceTransactionHashes: [sourceTransactionHash],
1269
+ status: 'refund',
1270
+ updatedAt: '2026-01-09T00:02:00.000Z',
1271
+ },
1272
+ verifyTransfers: Evidence.createVerifier({ db }),
1273
+ })
1274
+ const records = (
1275
+ await FundingDeposits.listBySourceTransaction(db, {
1276
+ depositAddressId: address.id,
1277
+ sourceTransactionHash,
1278
+ })
1279
+ ).sort((left, right) => left.providerTransferIndex - right.providerTransferIndex)
1280
+
1281
+ expect({ records: records.map(Deposit.toPublic), result }).toMatchInlineSnapshot(
1282
+ {
1283
+ records: [
1284
+ { depositAddressId: expect.any(String), updatedAt: expect.any(String) },
1285
+ { depositAddressId: expect.any(String), updatedAt: expect.any(String) },
1286
+ ],
1287
+ },
1288
+ `
1289
+ {
1290
+ "records": [
1291
+ {
1292
+ "createdAt": "2026-01-09T00:01:00.000Z",
1293
+ "depositAddressId": Any<String>,
1294
+ "destinationChain": {
1295
+ "addressFormat": "hex",
1296
+ "id": "eip155:4217",
1297
+ "kind": "evm",
1298
+ "name": "Tempo",
1299
+ },
1300
+ "destinationToken": {
1301
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1302
+ "currency": "USD",
1303
+ "decimals": 6,
1304
+ "name": "Bridged USDC (Stargate)",
1305
+ "standard": "TIP-20",
1306
+ "symbol": "USDC.e",
1307
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1308
+ "verified": true,
1309
+ },
1310
+ "id": "fdp_refund_retry_0",
1311
+ "provider": {
1312
+ "id": "relay",
1313
+ "name": "Relay",
1314
+ },
1315
+ "recipient": "0x1111111111111111111111111111111111111111",
1316
+ "refundAddress": "0x5151515151515151515151515151515151515151",
1317
+ "refundAmount": {
1318
+ "baseUnits": "300000",
1319
+ "currency": "USD",
1320
+ "decimals": 6,
1321
+ "formatted": "0.3",
1322
+ },
1323
+ "refundTransactionHashes": [
1324
+ "0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0",
1325
+ ],
1326
+ "sourceAmount": {
1327
+ "baseUnits": "1000000",
1328
+ "currency": "USD",
1329
+ "decimals": 6,
1330
+ "formatted": "1",
1331
+ },
1332
+ "sourceChain": {
1333
+ "addressFormat": "hex",
1334
+ "id": "eip155:8453",
1335
+ "kind": "evm",
1336
+ "name": "Base",
1337
+ },
1338
+ "sourceToken": {
1339
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1340
+ "currency": "USD",
1341
+ "decimals": 6,
1342
+ "name": "USD Coin",
1343
+ "standard": "ERC-20",
1344
+ "symbol": "USDC",
1345
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1346
+ "verified": true,
1347
+ },
1348
+ "sourceTransactionHashes": [
1349
+ "0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0",
1350
+ ],
1351
+ "sourceTransferIndex": 0,
1352
+ "status": "refunded",
1353
+ "updatedAt": Any<String>,
1354
+ },
1355
+ {
1356
+ "createdAt": "2026-01-09T00:01:01.000Z",
1357
+ "depositAddressId": Any<String>,
1358
+ "destinationChain": {
1359
+ "addressFormat": "hex",
1360
+ "id": "eip155:4217",
1361
+ "kind": "evm",
1362
+ "name": "Tempo",
1363
+ },
1364
+ "destinationToken": {
1365
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1366
+ "currency": "USD",
1367
+ "decimals": 6,
1368
+ "name": "Bridged USDC (Stargate)",
1369
+ "standard": "TIP-20",
1370
+ "symbol": "USDC.e",
1371
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1372
+ "verified": true,
1373
+ },
1374
+ "id": "fdp_refund_retry_1",
1375
+ "provider": {
1376
+ "id": "relay",
1377
+ "name": "Relay",
1378
+ },
1379
+ "recipient": "0x1111111111111111111111111111111111111111",
1380
+ "refundAddress": "0x5151515151515151515151515151515151515151",
1381
+ "refundAmount": {
1382
+ "baseUnits": "600000",
1383
+ "currency": "USD",
1384
+ "decimals": 6,
1385
+ "formatted": "0.6",
1386
+ },
1387
+ "refundTransactionHashes": [
1388
+ "0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0",
1389
+ ],
1390
+ "sender": "0x5353535353535353535353535353535353535353",
1391
+ "sourceAmount": {
1392
+ "baseUnits": "2000000",
1393
+ "currency": "USD",
1394
+ "decimals": 6,
1395
+ "formatted": "2",
1396
+ },
1397
+ "sourceChain": {
1398
+ "addressFormat": "hex",
1399
+ "id": "eip155:8453",
1400
+ "kind": "evm",
1401
+ "name": "Base",
1402
+ },
1403
+ "sourceToken": {
1404
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1405
+ "currency": "USD",
1406
+ "decimals": 6,
1407
+ "name": "USD Coin",
1408
+ "standard": "ERC-20",
1409
+ "symbol": "USDC",
1410
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1411
+ "verified": true,
1412
+ },
1413
+ "sourceTransactionHashes": [
1414
+ "0xb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0",
1415
+ ],
1416
+ "sourceTransferIndex": 1,
1417
+ "status": "refunded",
1418
+ "updatedAt": Any<String>,
1419
+ },
1420
+ ],
1421
+ "result": {
1422
+ "created": 0,
1423
+ "terminal": true,
1424
+ "updated": 1,
1425
+ },
1426
+ }
1427
+ `,
1428
+ )
1429
+ } finally {
1430
+ await server.closeAsync()
1431
+ }
1432
+ })
1433
+
1434
+ test('reconciles destination evidence reported with a Relay failure', async () => {
1435
+ const route = TestFunding.transferSnapshot()
1436
+ const depositAddress = `0x${'56'.repeat(20)}`
1437
+ const destinationTransactionHash = `0x${'d1'.repeat(32)}`
1438
+ const sourceTransactionHash = `0x${'b1'.repeat(32)}`
1439
+ const address = await DepositAddress.create(db, {
1440
+ apiKeyId: 'key_failure_delivery',
1441
+ deliveryStrategy: 'provider',
1442
+ environment: 'production',
1443
+ now: new Date('2026-01-10T00:00:00.000Z'),
1444
+ orgId: 'org_failure_delivery',
1445
+ providerOutputToken: route.destinationToken,
1446
+ snapshot: {
1447
+ address: depositAddress,
1448
+ destinationChain: route.destinationChain,
1449
+ destinationToken: route.destinationToken,
1450
+ provider: { id: 'relay', name: 'Relay' },
1451
+ recipient: route.recipient,
1452
+ refundAddress: `0x${'57'.repeat(20)}`,
1453
+ sourceChain: route.sourceChain,
1454
+ sourceToken: route.sourceToken,
1455
+ subsidize: false,
1456
+ },
1457
+ })
1458
+ const server = await TestRelay.createServer(async (request, response) => {
1459
+ const chunks: Uint8Array[] = []
1460
+ for await (const chunk of request) chunks.push(chunk)
1461
+ const body = JSON.parse(NodeBuffer.Buffer.concat(chunks).toString()) as {
1462
+ id: number
1463
+ method: string
1464
+ params?: readonly string[] | undefined
1465
+ }
1466
+ const hash = body.params?.[0]
1467
+ const result = (() => {
1468
+ if (body.method === 'eth_blockNumber') return '0x100'
1469
+ if (hash === sourceTransactionHash)
1470
+ return {
1471
+ blockNumber: '0x80',
1472
+ logs: [
1473
+ transferLog({
1474
+ amount: 1_000_000n,
1475
+ recipient: depositAddress,
1476
+ sender: `0x${'58'.repeat(20)}`,
1477
+ token: route.sourceToken.address,
1478
+ }),
1479
+ ],
1480
+ status: '0x1',
1481
+ }
1482
+ return {
1483
+ blockNumber: '0x80',
1484
+ logs: [
1485
+ transferLog({
1486
+ amount: 1_000_000n,
1487
+ recipient: route.recipient,
1488
+ sender: `0x${'59'.repeat(20)}`,
1489
+ token: route.destinationToken.address,
1490
+ }),
1491
+ ],
1492
+ status: '0x1',
1493
+ }
1494
+ })()
1495
+ response.writeHead(200, { 'content-type': 'application/json' })
1496
+ response.end(JSON.stringify({ id: body.id, jsonrpc: '2.0', result }))
1497
+ })
1498
+
1499
+ try {
1500
+ await FundingCatalog.publish(db, {
1501
+ chainTokens: [],
1502
+ chains: [
1503
+ {
1504
+ aliases: [],
1505
+ id: route.destinationChain.id as Chain.Id,
1506
+ name: route.destinationChain.name,
1507
+ parentChainId: null,
1508
+ rpcUrls: [server.url],
1509
+ slug: 'destination-failure-delivery',
1510
+ },
1511
+ {
1512
+ aliases: [],
1513
+ id: route.sourceChain.id as Chain.Id,
1514
+ name: route.sourceChain.name,
1515
+ parentChainId: null,
1516
+ rpcUrls: [server.url],
1517
+ slug: 'source-failure-delivery',
1518
+ },
1519
+ ],
1520
+ routes: [],
1521
+ tokens: [],
1522
+ })
1523
+ const result = await Reconciliation.reconcileRequest(db, {
1524
+ address,
1525
+ request: {
1526
+ createdAt: '2026-01-10T00:01:00.000Z',
1527
+ destinationTransactionHashes: [destinationTransactionHash],
1528
+ id: 'relay-failure-delivery-request',
1529
+ refundTransactionHashes: [],
1530
+ sourceTransactionHashes: [sourceTransactionHash],
1531
+ status: 'failure',
1532
+ updatedAt: '2026-01-10T00:02:00.000Z',
1533
+ },
1534
+ verifyTransfers: Evidence.createVerifier({ db }),
1535
+ })
1536
+ const [record] = await FundingDeposits.listBySourceTransaction(db, {
1537
+ depositAddressId: address.id,
1538
+ sourceTransactionHash,
1539
+ })
1540
+ if (!record) throw new Error('Expected a reconciled deposit.')
1541
+
1542
+ expect({ record: Deposit.toPublic(record), result }).toMatchInlineSnapshot(
1543
+ {
1544
+ record: {
1545
+ createdAt: expect.any(String),
1546
+ depositAddressId: expect.any(String),
1547
+ id: expect.any(String),
1548
+ updatedAt: expect.any(String),
1549
+ },
1550
+ },
1551
+ `
1552
+ {
1553
+ "record": {
1554
+ "createdAt": Any<String>,
1555
+ "depositAddressId": Any<String>,
1556
+ "destinationAmount": {
1557
+ "baseUnits": "1000000",
1558
+ "currency": "USD",
1559
+ "decimals": 6,
1560
+ "formatted": "1",
1561
+ },
1562
+ "destinationAmountRequired": {
1563
+ "baseUnits": "1000000",
1564
+ "currency": "USD",
1565
+ "decimals": 6,
1566
+ "formatted": "1",
1567
+ },
1568
+ "destinationChain": {
1569
+ "addressFormat": "hex",
1570
+ "id": "eip155:4217",
1571
+ "kind": "evm",
1572
+ "name": "Tempo",
1573
+ },
1574
+ "destinationToken": {
1575
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1576
+ "currency": "USD",
1577
+ "decimals": 6,
1578
+ "name": "Bridged USDC (Stargate)",
1579
+ "standard": "TIP-20",
1580
+ "symbol": "USDC.e",
1581
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1582
+ "verified": true,
1583
+ },
1584
+ "destinationTransactionHashes": [
1585
+ "0xd1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
1586
+ ],
1587
+ "detectionTrigger": "poll",
1588
+ "id": Any<String>,
1589
+ "provider": {
1590
+ "id": "relay",
1591
+ "name": "Relay",
1592
+ },
1593
+ "recipient": "0x1111111111111111111111111111111111111111",
1594
+ "refundAddress": "0x5757575757575757575757575757575757575757",
1595
+ "sender": "0x5858585858585858585858585858585858585858",
1596
+ "sourceAmount": {
1597
+ "baseUnits": "1000000",
1598
+ "currency": "USD",
1599
+ "decimals": 6,
1600
+ "formatted": "1",
1601
+ },
1602
+ "sourceChain": {
1603
+ "addressFormat": "hex",
1604
+ "id": "eip155:8453",
1605
+ "kind": "evm",
1606
+ "name": "Base",
1607
+ },
1608
+ "sourceToken": {
1609
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1610
+ "currency": "USD",
1611
+ "decimals": 6,
1612
+ "name": "USD Coin",
1613
+ "standard": "ERC-20",
1614
+ "symbol": "USDC",
1615
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1616
+ "verified": true,
1617
+ },
1618
+ "sourceTransactionHashes": [
1619
+ "0xb1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1",
1620
+ ],
1621
+ "sourceTransferIndex": 0,
1622
+ "status": "completed",
1623
+ "updatedAt": Any<String>,
1624
+ },
1625
+ "result": {
1626
+ "created": 1,
1627
+ "terminal": true,
1628
+ "updated": 1,
1629
+ },
1630
+ }
1631
+ `,
1632
+ )
1633
+ } finally {
1634
+ await server.closeAsync()
1635
+ }
1636
+ })
1637
+
1638
+ test('keeps Relay failures with unverified destination transactions pending for recovery', async () => {
1639
+ const route = TestFunding.transferSnapshot()
1640
+ const depositAddress = `0x${'4d'.repeat(20)}`
1641
+ const sourceTransactionHash = `0x${'ad'.repeat(32)}`
1642
+ const unrelatedDestinationTransactionHash = `0x${'d2'.repeat(32)}`
1643
+ const address = await DepositAddress.create(db, {
1644
+ apiKeyId: 'key_failed_reconciliation',
1645
+ deliveryStrategy: 'provider',
1646
+ environment: 'production',
1647
+ now: new Date('2026-01-08T00:00:00.000Z'),
1648
+ orgId: 'org_failed_reconciliation',
1649
+ providerOutputToken: route.destinationToken,
1650
+ snapshot: {
1651
+ address: depositAddress,
1652
+ destinationChain: route.destinationChain,
1653
+ destinationToken: route.destinationToken,
1654
+ provider: { id: 'relay', name: 'Relay' },
1655
+ recipient: route.recipient,
1656
+ refundAddress: `0x${'4e'.repeat(20)}`,
1657
+ sourceChain: route.sourceChain,
1658
+ sourceToken: route.sourceToken,
1659
+ subsidize: false,
1660
+ },
1661
+ })
1662
+ const counts: Parameters<Metrics.Metrics['count']>[] = []
1663
+ const metrics = Metrics.from({
1664
+ count(...parameters) {
1665
+ counts.push(parameters)
1666
+ },
1667
+ flush() {},
1668
+ gauge() {},
1669
+ histogram() {},
1670
+ })
1671
+ const server = await TestRelay.createServer(async (request, response) => {
1672
+ const chunks: Uint8Array[] = []
1673
+ for await (const chunk of request) chunks.push(chunk)
1674
+ const body = JSON.parse(NodeBuffer.Buffer.concat(chunks).toString()) as {
1675
+ id: number
1676
+ method: string
1677
+ }
1678
+ const result =
1679
+ body.method === 'eth_blockNumber'
1680
+ ? '0x100'
1681
+ : {
1682
+ blockNumber: '0x80',
1683
+ logs: [
1684
+ transferLog({
1685
+ amount: 1_000_000n,
1686
+ recipient: depositAddress,
1687
+ sender: `0x${'4f'.repeat(20)}`,
1688
+ token: route.sourceToken.address,
1689
+ }),
1690
+ ],
1691
+ status: '0x1',
1692
+ }
1693
+ response.writeHead(200, { 'content-type': 'application/json' })
1694
+ response.end(JSON.stringify({ id: body.id, jsonrpc: '2.0', result }))
1695
+ })
1696
+
1697
+ try {
1698
+ await FundingCatalog.publish(db, {
1699
+ chainTokens: [],
1700
+ chains: [
1701
+ {
1702
+ aliases: [],
1703
+ id: route.destinationChain.id as Chain.Id,
1704
+ name: route.destinationChain.name,
1705
+ parentChainId: null,
1706
+ rpcUrls: [server.url],
1707
+ slug: 'destination-failed',
1708
+ },
1709
+ {
1710
+ aliases: [],
1711
+ id: route.sourceChain.id as Chain.Id,
1712
+ name: route.sourceChain.name,
1713
+ parentChainId: null,
1714
+ rpcUrls: [server.url],
1715
+ slug: 'source-failed',
1716
+ },
1717
+ ],
1718
+ routes: [],
1719
+ tokens: [],
1720
+ })
1721
+ const result = await Reconciliation.reconcileRequest(db, {
1722
+ address,
1723
+ metrics,
1724
+ request: {
1725
+ createdAt: '2026-01-08T00:01:00.000Z',
1726
+ destinationTransactionHashes: [unrelatedDestinationTransactionHash],
1727
+ id: 'relay-failed-request',
1728
+ refundTransactionHashes: [],
1729
+ sourceTransactionHashes: [sourceTransactionHash],
1730
+ status: 'failure',
1731
+ updatedAt: '2026-01-08T00:02:00.000Z',
1732
+ },
1733
+ verifyTransfers: Evidence.createVerifier({ db }),
1734
+ })
1735
+ const [record] = await FundingDeposits.listBySourceTransaction(db, {
1736
+ depositAddressId: address.id,
1737
+ sourceTransactionHash,
1738
+ })
1739
+
1740
+ expect({ counts, record, result }).toMatchInlineSnapshot(
1741
+ {
1742
+ record: {
1743
+ createdAt: expect.any(String),
1744
+ depositAddressId: expect.any(String),
1745
+ id: expect.any(String),
1746
+ snapshot: { depositAddressId: expect.any(String) },
1747
+ updatedAt: expect.any(String),
1748
+ },
1749
+ },
1750
+ `
1751
+ {
1752
+ "counts": [
1753
+ [
1754
+ "funding_deposit_refund_count",
1755
+ 1,
1756
+ {
1757
+ "outcome": "provider_failed",
1758
+ "provider": "relay",
1759
+ },
1760
+ ],
1761
+ ],
1762
+ "record": {
1763
+ "createdAt": Any<String>,
1764
+ "depositAddressId": Any<String>,
1765
+ "detectionTrigger": "poll",
1766
+ "environment": "production",
1767
+ "id": Any<String>,
1768
+ "orgId": "org_failed_reconciliation",
1769
+ "pollObservedAt": null,
1770
+ "projectId": null,
1771
+ "providerOutputAmount": null,
1772
+ "providerOutputToken": {
1773
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1774
+ "currency": "USD",
1775
+ "decimals": 6,
1776
+ "name": "Bridged USDC (Stargate)",
1777
+ "standard": "TIP-20",
1778
+ "symbol": "USDC.e",
1779
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1780
+ "verified": true,
1781
+ },
1782
+ "providerRequestId": "relay-failed-request",
1783
+ "providerRequestIds": [
1784
+ "relay-failed-request",
1785
+ ],
1786
+ "providerState": null,
1787
+ "providerTransactionHashes": [],
1788
+ "providerTransferIndex": 0,
1789
+ "retryState": null,
1790
+ "settlementTransaction": null,
1791
+ "settlementTransactionHash": null,
1792
+ "snapshot": {
1793
+ "depositAddressId": Any<String>,
1794
+ "destinationChain": {
1795
+ "addressFormat": "hex",
1796
+ "id": "eip155:4217",
1797
+ "kind": "evm",
1798
+ "name": "Tempo",
1799
+ },
1800
+ "destinationToken": {
1801
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
1802
+ "currency": "USD",
1803
+ "decimals": 6,
1804
+ "name": "Bridged USDC (Stargate)",
1805
+ "standard": "TIP-20",
1806
+ "symbol": "USDC.e",
1807
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
1808
+ "verified": true,
1809
+ },
1810
+ "provider": {
1811
+ "id": "relay",
1812
+ "name": "Relay",
1813
+ },
1814
+ "recipient": "0x1111111111111111111111111111111111111111",
1815
+ "refundAddress": "0x4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e",
1816
+ "sender": "0x4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f",
1817
+ "sourceAmount": {
1818
+ "baseUnits": "1000000",
1819
+ "currency": "USD",
1820
+ "decimals": 6,
1821
+ "formatted": "1",
1822
+ },
1823
+ "sourceChain": {
1824
+ "addressFormat": "hex",
1825
+ "id": "eip155:8453",
1826
+ "kind": "evm",
1827
+ "name": "Base",
1828
+ },
1829
+ "sourceToken": {
1830
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1831
+ "currency": "USD",
1832
+ "decimals": 6,
1833
+ "name": "USD Coin",
1834
+ "standard": "ERC-20",
1835
+ "symbol": "USDC",
1836
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
1837
+ "verified": true,
1838
+ },
1839
+ "sourceTransactionHashes": [
1840
+ "0xadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad",
1841
+ ],
1842
+ },
1843
+ "sourceChainId": "eip155:8453",
1844
+ "sourceTransactionHash": "0xadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadadad",
1845
+ "sourceTransferIndex": 0,
1846
+ "status": "refunding",
1847
+ "statusReason": null,
1848
+ "subsidyAmount": null,
1849
+ "tempoGasPaid": null,
1850
+ "updatedAt": Any<String>,
1851
+ "version": 3,
1852
+ "webhookReceivedAt": null,
1853
+ },
1854
+ "result": {
1855
+ "created": 1,
1856
+ "followUp": "delivery",
1857
+ "terminal": false,
1858
+ "updated": 1,
1859
+ },
1860
+ }
1861
+ `,
1862
+ )
1863
+ } finally {
1864
+ await server.closeAsync()
1865
+ }
1866
+ })
1867
+
628
1868
  test('persists a detected deposit before source verification succeeds', async () => {
629
1869
  const route = TestFunding.transferSnapshot()
630
1870
  const depositAddress = `0x${'20'.repeat(20)}`
@@ -1506,7 +2746,7 @@ describe('createTracker', () => {
1506
2746
  }
1507
2747
  })
1508
2748
 
1509
- test('completes subsidized Relay delivery from authenticated chain evidence', async () => {
2749
+ test('completes chain-detected Relay delivery before provider attribution', async () => {
1510
2750
  const route = TestFunding.transferSnapshot()
1511
2751
  const ambiguousOrderId = `0x${'3d'.repeat(32)}`
1512
2752
  const ambiguousSweepTransactionHash = `0x${'3e'.repeat(32)}`
@@ -1548,8 +2788,10 @@ describe('createTracker', () => {
1548
2788
  histogram() {},
1549
2789
  })
1550
2790
  const rpcMethods: string[] = []
1551
- let correlated = false
2791
+ let ambiguous = true
2792
+ let correlateDestination = false
1552
2793
  let delivered = false
2794
+ let indexed = false
1553
2795
  const server = await TestRelay.createServer(async (request, response) => {
1554
2796
  if (request.method === 'GET') {
1555
2797
  const url = new URL(request.url ?? '/', server.url)
@@ -1571,26 +2813,23 @@ describe('createTracker', () => {
1571
2813
  ],
1572
2814
  }
1573
2815
  : {
1574
- requests: [
1575
- {
1576
- createdAt: '2026-01-04T00:01:00.000Z',
1577
- data: { outTxs: [] },
1578
- depositAddress: { depositTxHash: sourceTransactionHash },
1579
- id: 'relay-observed-request',
1580
- ...(() => {
1581
- if (!correlated) return {}
1582
- return {
2816
+ requests: indexed
2817
+ ? [
2818
+ {
2819
+ createdAt: '2026-01-04T00:01:00.000Z',
2820
+ data: { outTxs: [] },
2821
+ depositAddress: { depositTxHash: sourceTransactionHash },
2822
+ id: 'relay-observed-request',
1583
2823
  protocol: {
1584
2824
  deposit: { origin: { depositor: refundAddress, depository } },
1585
2825
  orderId,
1586
2826
  solver: { address: solver },
1587
2827
  },
1588
- }
1589
- })(),
1590
- status: 'pending',
1591
- updatedAt: '2026-01-04T00:02:00.000Z',
1592
- },
1593
- ],
2828
+ status: 'pending',
2829
+ updatedAt: '2026-01-04T00:02:00.000Z',
2830
+ },
2831
+ ]
2832
+ : [],
1594
2833
  },
1595
2834
  ),
1596
2835
  )
@@ -1614,10 +2853,14 @@ describe('createTracker', () => {
1614
2853
  blockNumber: 0xf0,
1615
2854
  from: solver,
1616
2855
  hash: destinationTransactionHash,
1617
- input: AbiFunction.encodeData(
1618
- AbiFunction.from('function transfer(address recipient,uint256 amount)'),
1619
- [route.recipient as Address.Address, 1_000_000n],
1620
- ),
2856
+ input: (() => {
2857
+ const input = AbiFunction.encodeData(
2858
+ AbiFunction.from('function transfer(address recipient,uint256 amount)'),
2859
+ [route.recipient as Address.Address, 1_000_000n],
2860
+ )
2861
+ if (!correlateDestination) return input
2862
+ return Hex.concat(input, orderId as Hex.Hex)
2863
+ })(),
1621
2864
  to: route.destinationToken.address,
1622
2865
  })
1623
2866
  : rpcTransaction({
@@ -1657,15 +2900,19 @@ describe('createTracker', () => {
1657
2900
  const filter = parameter as { address?: string | undefined }
1658
2901
  if (filter.address?.toLowerCase() === depository.toLowerCase())
1659
2902
  return [
1660
- relayDepositLog({
1661
- amount: 1_000_000n,
1662
- blockNumber: 0x81,
1663
- depository,
1664
- from: refundAddress,
1665
- orderId: ambiguousOrderId,
1666
- token: route.sourceToken.address,
1667
- transactionHash: ambiguousSweepTransactionHash,
1668
- }),
2903
+ ...(ambiguous
2904
+ ? [
2905
+ relayDepositLog({
2906
+ amount: 1_000_000n,
2907
+ blockNumber: 0x81,
2908
+ depository,
2909
+ from: refundAddress,
2910
+ orderId: ambiguousOrderId,
2911
+ token: route.sourceToken.address,
2912
+ transactionHash: ambiguousSweepTransactionHash,
2913
+ }),
2914
+ ]
2915
+ : []),
1669
2916
  relayDepositLog({
1670
2917
  amount: 1_000_000n,
1671
2918
  blockNumber: 0x82,
@@ -1747,6 +2994,50 @@ describe('createTracker', () => {
1747
2994
  now: () => new Date('2026-01-04T00:03:00.000Z'),
1748
2995
  observation: { db },
1749
2996
  })
2997
+ if (!Provider.canObserveDepositAddressRequest(provider))
2998
+ throw new Error('Expected Relay delivery observation.')
2999
+ const ambiguousObservation = await provider.observeDepositAddressRequest(
3000
+ {
3001
+ address: depositAddress,
3002
+ destinationChain: route.destinationChain,
3003
+ destinationToken: route.destinationToken,
3004
+ recipient: route.recipient,
3005
+ refundAddress,
3006
+ source: { amount: '1000000', transactionHash: sourceTransactionHash },
3007
+ sourceChain: route.sourceChain,
3008
+ sourceToken: route.sourceToken,
3009
+ },
3010
+ new AbortController().signal,
3011
+ )
3012
+ ambiguous = false
3013
+ delivered = true
3014
+ const uncorrelatedObservation = await provider.observeDepositAddressRequest(
3015
+ {
3016
+ address: depositAddress,
3017
+ destinationChain: route.destinationChain,
3018
+ destinationToken: route.destinationToken,
3019
+ recipient: route.recipient,
3020
+ refundAddress,
3021
+ source: { amount: '1000000', transactionHash: sourceTransactionHash },
3022
+ sourceChain: route.sourceChain,
3023
+ sourceToken: route.sourceToken,
3024
+ },
3025
+ new AbortController().signal,
3026
+ )
3027
+ correlateDestination = true
3028
+ delivered = false
3029
+ const observed = await SourceObservation.observe(db, {
3030
+ addressId: address.id,
3031
+ amount: '1000000',
3032
+ chainId: route.sourceChain.id,
3033
+ now: new Date('2026-01-04T00:00:30.000Z'),
3034
+ recipient: depositAddress,
3035
+ sender: refundAddress,
3036
+ tokenAddress: route.sourceToken.address,
3037
+ transactionHash: sourceTransactionHash,
3038
+ transferIndex: 0,
3039
+ })
3040
+ if (observed.type === 'ignored') throw new Error('Expected source observation.')
1750
3041
  const tracker = Reconciliation.createTracker({
1751
3042
  db,
1752
3043
  dispatch: async (...parameters) => {
@@ -1757,25 +3048,30 @@ describe('createTracker', () => {
1757
3048
  providers: [provider],
1758
3049
  verifyTransfers: Evidence.createVerifier({ db }),
1759
3050
  })
1760
- const message = {
3051
+ const pending = await tracker.reconcile({
1761
3052
  addressId: address.id,
1762
- requestId: 'relay-observed-request',
1763
- trigger: 'webhook',
3053
+ trigger: 'chain',
1764
3054
  type: 'funding:deposit-address:reconcile',
1765
- } satisfies Provider.Webhook.receive.Dispatchable
1766
- const pending = await tracker.reconcile(message)
1767
- const [followUp] = dispatches
1768
- if (!followUp) throw new Error('Expected a delivery follow-up message.')
1769
- correlated = true
3055
+ })
3056
+ const [deliveryFollowUp] = dispatches
3057
+ if (!deliveryFollowUp) throw new Error('Expected a delivery follow-up message.')
1770
3058
  delivered = true
1771
- const completed = await tracker.reconcile(followUp[0])
3059
+ const completed = await tracker.reconcile(deliveryFollowUp[0])
3060
+ const completedRecord = await FundingDeposits.get(db, observed.record.id)
3061
+ const [, providerFollowUp] = dispatches
3062
+ if (!providerFollowUp) throw new Error('Expected a provider follow-up message.')
3063
+ indexed = true
3064
+ const attributed = await tracker.reconcile(providerFollowUp[0])
1772
3065
  const [record] = await FundingDeposits.listBySourceTransaction(db, {
1773
3066
  depositAddressId: address.id,
1774
3067
  sourceTransactionHash,
1775
3068
  })
1776
3069
 
1777
3070
  expect({
3071
+ ambiguousObservation,
3072
+ attributed,
1778
3073
  completed,
3074
+ completedRecord,
1779
3075
  counts,
1780
3076
  dispatches: dispatches.map(([message, dispatchOptions]) => [
1781
3077
  { ...message, addressId: '<address-id>' },
@@ -1784,6 +3080,8 @@ describe('createTracker', () => {
1784
3080
  pending,
1785
3081
  record,
1786
3082
  rpcMethods,
3083
+ sourceObservation: observed.type,
3084
+ uncorrelatedObservation,
1787
3085
  }).toMatchInlineSnapshot(
1788
3086
  {
1789
3087
  record: {
@@ -1793,14 +3091,144 @@ describe('createTracker', () => {
1793
3091
  snapshot: { depositAddressId: expect.any(String) },
1794
3092
  updatedAt: expect.any(String),
1795
3093
  },
3094
+ completedRecord: {
3095
+ createdAt: expect.any(String),
3096
+ depositAddressId: expect.any(String),
3097
+ id: expect.any(String),
3098
+ snapshot: { depositAddressId: expect.any(String) },
3099
+ updatedAt: expect.any(String),
3100
+ },
1796
3101
  },
1797
3102
  `
1798
3103
  {
3104
+ "ambiguousObservation": {
3105
+ "type": "pending",
3106
+ },
3107
+ "attributed": {
3108
+ "created": 0,
3109
+ "type": "completed",
3110
+ "updated": 1,
3111
+ },
1799
3112
  "completed": {
1800
3113
  "created": 0,
1801
3114
  "type": "completed",
1802
3115
  "updated": 1,
1803
3116
  },
3117
+ "completedRecord": {
3118
+ "createdAt": Any<String>,
3119
+ "depositAddressId": Any<String>,
3120
+ "detectionTrigger": "chain",
3121
+ "environment": "production",
3122
+ "id": Any<String>,
3123
+ "orgId": "org_delivery_observation",
3124
+ "pollObservedAt": null,
3125
+ "projectId": null,
3126
+ "providerOutputAmount": {
3127
+ "baseUnits": "1000000",
3128
+ "currency": "USD",
3129
+ "decimals": 6,
3130
+ "formatted": "1",
3131
+ },
3132
+ "providerOutputToken": {
3133
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
3134
+ "currency": "USD",
3135
+ "decimals": 6,
3136
+ "name": "Bridged USDC (Stargate)",
3137
+ "standard": "TIP-20",
3138
+ "symbol": "USDC.e",
3139
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
3140
+ "verified": true,
3141
+ },
3142
+ "providerRequestId": null,
3143
+ "providerRequestIds": [],
3144
+ "providerState": {
3145
+ "relay": {
3146
+ "destinationObservedAt": "2026-01-04T00:03:00.000Z",
3147
+ "orderId": "0x3434343434343434343434343434343434343434343434343434343434343434",
3148
+ "sourceDepository": "0x3232323232323232323232323232323232323232",
3149
+ "sourceObservedAt": "2026-01-04T00:03:00.000Z",
3150
+ "sourceSweepTransactionHash": "0x3636363636363636363636363636363636363636363636363636363636363636",
3151
+ },
3152
+ },
3153
+ "providerTransactionHashes": [
3154
+ "0x3636363636363636363636363636363636363636363636363636363636363636",
3155
+ "0x3333333333333333333333333333333333333333333333333333333333333333",
3156
+ ],
3157
+ "providerTransferIndex": 0,
3158
+ "retryState": null,
3159
+ "settlementTransaction": null,
3160
+ "settlementTransactionHash": null,
3161
+ "snapshot": {
3162
+ "depositAddressId": Any<String>,
3163
+ "destinationAmount": {
3164
+ "baseUnits": "1000000",
3165
+ "currency": "USD",
3166
+ "decimals": 6,
3167
+ "formatted": "1",
3168
+ },
3169
+ "destinationChain": {
3170
+ "addressFormat": "hex",
3171
+ "id": "eip155:4217",
3172
+ "kind": "evm",
3173
+ "name": "Tempo",
3174
+ },
3175
+ "destinationToken": {
3176
+ "address": "0x20c000000000000000000000b9537d11c60e8b50",
3177
+ "currency": "USD",
3178
+ "decimals": 6,
3179
+ "name": "Bridged USDC (Stargate)",
3180
+ "standard": "TIP-20",
3181
+ "symbol": "USDC.e",
3182
+ "tokenKey": "eip155:4217/erc20:0x20c000000000000000000000b9537d11c60e8b50",
3183
+ "verified": true,
3184
+ },
3185
+ "destinationTransactionHashes": [
3186
+ "0x3333333333333333333333333333333333333333333333333333333333333333",
3187
+ ],
3188
+ "provider": {
3189
+ "id": "relay",
3190
+ "name": "Relay",
3191
+ },
3192
+ "recipient": "0x1111111111111111111111111111111111111111",
3193
+ "refundAddress": "0x3838383838383838383838383838383838383838",
3194
+ "sender": "0x3838383838383838383838383838383838383838",
3195
+ "sourceAmount": {
3196
+ "baseUnits": "1000000",
3197
+ "currency": "USD",
3198
+ "decimals": 6,
3199
+ "formatted": "1",
3200
+ },
3201
+ "sourceChain": {
3202
+ "addressFormat": "hex",
3203
+ "id": "eip155:8453",
3204
+ "kind": "evm",
3205
+ "name": "Base",
3206
+ },
3207
+ "sourceToken": {
3208
+ "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
3209
+ "currency": "USD",
3210
+ "decimals": 6,
3211
+ "name": "USD Coin",
3212
+ "standard": "ERC-20",
3213
+ "symbol": "USDC",
3214
+ "tokenKey": "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
3215
+ "verified": true,
3216
+ },
3217
+ "sourceTransactionHashes": [
3218
+ "0x3737373737373737373737373737373737373737373737373737373737373737",
3219
+ ],
3220
+ },
3221
+ "sourceChainId": "eip155:8453",
3222
+ "sourceTransactionHash": "0x3737373737373737373737373737373737373737373737373737373737373737",
3223
+ "sourceTransferIndex": 0,
3224
+ "status": "completed",
3225
+ "statusReason": null,
3226
+ "subsidyAmount": null,
3227
+ "tempoGasPaid": null,
3228
+ "updatedAt": Any<String>,
3229
+ "version": 3,
3230
+ "webhookReceivedAt": null,
3231
+ },
1804
3232
  "counts": [
1805
3233
  [
1806
3234
  "funding_deposit_delivery_observation_count",
@@ -1811,37 +3239,47 @@ describe('createTracker', () => {
1811
3239
  },
1812
3240
  ],
1813
3241
  [
1814
- "funding_deposit_detection_count",
3242
+ "funding_deposit_reconciliation_count",
1815
3243
  1,
1816
3244
  {
3245
+ "outcome": "follow_up_enqueued",
1817
3246
  "provider": "relay",
1818
- "trigger": "webhook",
3247
+ "trigger": "chain",
1819
3248
  },
1820
3249
  ],
1821
3250
  [
1822
3251
  "funding_deposit_reconciliation_count",
1823
3252
  1,
1824
3253
  {
1825
- "outcome": "follow_up_enqueued",
3254
+ "outcome": "completed",
3255
+ "provider": "relay",
3256
+ "trigger": "chain",
3257
+ },
3258
+ ],
3259
+ [
3260
+ "funding_deposit_delivery_observation_count",
3261
+ 1,
3262
+ {
3263
+ "outcome": "verified",
1826
3264
  "provider": "relay",
1827
- "trigger": "webhook",
1828
3265
  },
1829
3266
  ],
1830
3267
  [
1831
3268
  "funding_deposit_reconciliation_count",
1832
3269
  1,
1833
3270
  {
1834
- "outcome": "completed",
3271
+ "outcome": "follow_up_enqueued",
1835
3272
  "provider": "relay",
1836
- "trigger": "webhook",
3273
+ "trigger": "chain",
1837
3274
  },
1838
3275
  ],
1839
3276
  [
1840
- "funding_deposit_delivery_observation_count",
3277
+ "funding_deposit_reconciliation_count",
1841
3278
  1,
1842
3279
  {
1843
- "outcome": "verified",
3280
+ "outcome": "completed",
1844
3281
  "provider": "relay",
3282
+ "trigger": "chain",
1845
3283
  },
1846
3284
  ],
1847
3285
  [
@@ -1850,7 +3288,7 @@ describe('createTracker', () => {
1850
3288
  {
1851
3289
  "outcome": "completed",
1852
3290
  "provider": "relay",
1853
- "trigger": "webhook",
3291
+ "trigger": "chain",
1854
3292
  },
1855
3293
  ],
1856
3294
  ],
@@ -1862,8 +3300,21 @@ describe('createTracker', () => {
1862
3300
  "attempt": 0,
1863
3301
  "kind": "delivery",
1864
3302
  },
1865
- "requestId": "relay-observed-request",
1866
- "trigger": "webhook",
3303
+ "trigger": "chain",
3304
+ "type": "funding:deposit-address:reconcile",
3305
+ },
3306
+ {
3307
+ "delaySeconds": 1,
3308
+ },
3309
+ ],
3310
+ [
3311
+ {
3312
+ "addressId": "<address-id>",
3313
+ "followUp": {
3314
+ "attempt": 0,
3315
+ "kind": "provider",
3316
+ },
3317
+ "trigger": "chain",
1867
3318
  "type": "funding:deposit-address:reconcile",
1868
3319
  },
1869
3320
  {
@@ -1872,14 +3323,14 @@ describe('createTracker', () => {
1872
3323
  ],
1873
3324
  ],
1874
3325
  "pending": {
1875
- "created": 1,
3326
+ "created": 0,
1876
3327
  "type": "completed",
1877
3328
  "updated": 1,
1878
3329
  },
1879
3330
  "record": {
1880
3331
  "createdAt": Any<String>,
1881
3332
  "depositAddressId": Any<String>,
1882
- "detectionTrigger": "webhook",
3333
+ "detectionTrigger": "chain",
1883
3334
  "environment": "production",
1884
3335
  "id": Any<String>,
1885
3336
  "orgId": "org_delivery_observation",
@@ -1961,7 +3412,7 @@ describe('createTracker', () => {
1961
3412
  },
1962
3413
  "recipient": "0x1111111111111111111111111111111111111111",
1963
3414
  "refundAddress": "0x3838383838383838383838383838383838383838",
1964
- "sender": "0x3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c",
3415
+ "sender": "0x3838383838383838383838383838383838383838",
1965
3416
  "sourceAmount": {
1966
3417
  "baseUnits": "1000000",
1967
3418
  "currency": "USD",
@@ -1996,16 +3447,21 @@ describe('createTracker', () => {
1996
3447
  "subsidyAmount": null,
1997
3448
  "tempoGasPaid": null,
1998
3449
  "updatedAt": Any<String>,
1999
- "version": 5,
3450
+ "version": 4,
2000
3451
  "webhookReceivedAt": null,
2001
3452
  },
2002
3453
  "rpcMethods": [
2003
3454
  "eth_getTransactionReceipt",
2004
3455
  "eth_blockNumber",
3456
+ "eth_getLogs",
3457
+ "eth_getTransactionByHash",
3458
+ "eth_getTransactionByHash",
2005
3459
  "eth_getTransactionReceipt",
2006
3460
  "eth_blockNumber",
2007
3461
  "eth_getLogs",
2008
3462
  "eth_getTransactionByHash",
3463
+ "eth_blockNumber",
3464
+ "eth_getLogs",
2009
3465
  "eth_getTransactionByHash",
2010
3466
  "eth_getTransactionReceipt",
2011
3467
  "eth_blockNumber",
@@ -2015,10 +3471,31 @@ describe('createTracker', () => {
2015
3471
  "eth_getTransactionByHash",
2016
3472
  "eth_blockNumber",
2017
3473
  "eth_getLogs",
3474
+ "eth_getTransactionReceipt",
3475
+ "eth_blockNumber",
3476
+ "eth_blockNumber",
3477
+ "eth_getLogs",
2018
3478
  "eth_getTransactionByHash",
2019
3479
  "eth_getTransactionReceipt",
2020
3480
  "eth_blockNumber",
3481
+ "eth_getTransactionReceipt",
3482
+ "eth_blockNumber",
2021
3483
  ],
3484
+ "sourceObservation": "created",
3485
+ "uncorrelatedObservation": {
3486
+ "providerState": {
3487
+ "relay": {
3488
+ "orderId": "0x3434343434343434343434343434343434343434343434343434343434343434",
3489
+ "sourceDepository": "0x3232323232323232323232323232323232323232",
3490
+ "sourceObservedAt": "2026-01-04T00:03:00.000Z",
3491
+ "sourceSweepTransactionHash": "0x3636363636363636363636363636363636363636363636363636363636363636",
3492
+ },
3493
+ },
3494
+ "providerTransactionHashes": [
3495
+ "0x3636363636363636363636363636363636363636363636363636363636363636",
3496
+ ],
3497
+ "type": "pending",
3498
+ },
2022
3499
  }
2023
3500
  `,
2024
3501
  )
@@ -2262,6 +3739,7 @@ describe('createTracker', () => {
2262
3739
  createdAt: '2026-01-05T00:01:00.000Z',
2263
3740
  destinationTransactionHashes: [destinationTransactionHash],
2264
3741
  id: 'source-evidence-request',
3742
+ refundTransactionHashes: [],
2265
3743
  sourceTransactionHashes: [firstSourceTransactionHash, secondSourceTransactionHash],
2266
3744
  status: 'success',
2267
3745
  updatedAt: '2026-01-05T00:02:00.000Z',