tapimo 0.9.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 (198) hide show
  1. package/dist/App.d.ts +4887 -5074
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/Webhooks.d.ts +21 -20
  4. package/dist/Webhooks.d.ts.map +1 -1
  5. package/dist/Webhooks.js +66 -23
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  8. package/dist/apps/Zones.d.ts +34 -22
  9. package/dist/apps/Zones.d.ts.map +1 -1
  10. package/dist/apps/Zones.js +9 -8
  11. package/dist/apps/Zones.js.map +1 -1
  12. package/dist/apps/data/App.d.ts +5198 -5099
  13. package/dist/apps/data/App.d.ts.map +1 -1
  14. package/dist/apps/data/FxOracle.d.ts +4 -5
  15. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.js +19 -4
  17. package/dist/apps/data/FxOracle.js.map +1 -1
  18. package/dist/apps/data/routes/activities.d.ts +270 -270
  19. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  20. package/dist/apps/data/routes/activities.js +32 -27
  21. package/dist/apps/data/routes/activities.js.map +1 -1
  22. package/dist/apps/data/routes/addresses.d.ts +52 -52
  23. package/dist/apps/data/routes/balances.d.ts +36 -36
  24. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.js +135 -81
  26. package/dist/apps/data/routes/balances.js.map +1 -1
  27. package/dist/apps/data/routes/blocks.d.ts +125 -125
  28. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  29. package/dist/apps/data/routes/earn.d.ts +512 -58
  30. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  31. package/dist/apps/data/routes/earn.js +368 -44
  32. package/dist/apps/data/routes/earn.js.map +1 -1
  33. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  34. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  35. package/dist/apps/data/routes/exchanges.js +114 -118
  36. package/dist/apps/data/routes/exchanges.js.map +1 -1
  37. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  38. package/dist/apps/data/routes/receipts.js +68 -47
  39. package/dist/apps/data/routes/receipts.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts +31 -31
  41. package/dist/apps/data/routes/tokens.d.ts +461 -459
  42. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  43. package/dist/apps/data/routes/transactions.d.ts +59 -51
  44. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  45. package/dist/apps/data/routes/transactions.js +25 -23
  46. package/dist/apps/data/routes/transactions.js.map +1 -1
  47. package/dist/apps/data/routes/transfers.d.ts +108 -86
  48. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  49. package/dist/apps/data/routes/transfers.js +618 -105
  50. package/dist/apps/data/routes/transfers.js.map +1 -1
  51. package/dist/apps/data/routes/valuation.d.ts +21 -21
  52. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  53. package/dist/apps/data/routes/valuation.js +2 -2
  54. package/dist/apps/data/routes/valuation.js.map +1 -1
  55. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  56. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  57. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  58. package/dist/apps/data/routes/webhooks.js +484 -342
  59. package/dist/apps/data/routes/webhooks.js.map +1 -1
  60. package/dist/apps/data/routes/zones.d.ts +12 -12
  61. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  62. package/dist/apps/data/routes/zones.js +3 -1
  63. package/dist/apps/data/routes/zones.js.map +1 -1
  64. package/dist/apps/funding/App.d.ts +96 -96
  65. package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
  66. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  67. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  68. package/dist/apps/management/App.d.ts +429 -727
  69. package/dist/apps/management/App.d.ts.map +1 -1
  70. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  71. package/dist/apps/management/routes/billing.d.ts +72 -72
  72. package/dist/apps/management/routes/faucet.d.ts +6 -6
  73. package/dist/apps/management/routes/invitations.d.ts +134 -134
  74. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  75. package/dist/apps/management/routes/me.d.ts +6 -6
  76. package/dist/apps/management/routes/members.d.ts +30 -30
  77. package/dist/apps/management/routes/orgs.d.ts +98 -98
  78. package/dist/apps/management/routes/projects.d.ts +20 -20
  79. package/dist/apps/management/routes/usage.d.ts +24 -24
  80. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  81. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  82. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/mpp/App.d.ts +56 -44
  84. package/dist/apps/mpp/App.d.ts.map +1 -1
  85. package/dist/apps/mpp/Mpp.d.ts +3 -0
  86. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  87. package/dist/apps/mpp/Mpp.js +3 -0
  88. package/dist/apps/mpp/Mpp.js.map +1 -1
  89. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  90. package/dist/apps/mpp/internal/Adapter.js +9 -2
  91. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  92. package/dist/db/Db.d.ts +2 -2
  93. package/dist/db/Db.d.ts.map +1 -1
  94. package/dist/db/Db.js +17 -15
  95. package/dist/db/Db.js.map +1 -1
  96. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  97. package/dist/db/tables/sponsoredTransactions.js +4 -1
  98. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  99. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  100. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  101. package/dist/db/tables/webhookSubscriptions.js +76 -3
  102. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  103. package/dist/internal/MetricSink.d.ts +112 -3
  104. package/dist/internal/MetricSink.d.ts.map +1 -1
  105. package/dist/internal/MetricSink.js +116 -3
  106. package/dist/internal/MetricSink.js.map +1 -1
  107. package/dist/internal/Schema.js +1 -1
  108. package/dist/internal/Schema.js.map +1 -1
  109. package/dist/internal/Tidx.d.ts +1 -3
  110. package/dist/internal/Tidx.d.ts.map +1 -1
  111. package/dist/internal/Tidx.js +1 -3
  112. package/dist/internal/Tidx.js.map +1 -1
  113. package/dist/internal/Viem.d.ts.map +1 -1
  114. package/dist/internal/Viem.js +2 -1
  115. package/dist/internal/Viem.js.map +1 -1
  116. package/dist/internal/WebhookDestination.d.ts +2 -0
  117. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  118. package/dist/internal/WebhookDestination.js +11 -0
  119. package/dist/internal/WebhookDestination.js.map +1 -1
  120. package/dist/internal/WebhookTransfer.d.ts +55 -0
  121. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  122. package/dist/internal/WebhookTransfer.js +296 -0
  123. package/dist/internal/WebhookTransfer.js.map +1 -0
  124. package/dist/internal/Webhooks.d.ts +37 -0
  125. package/dist/internal/Webhooks.d.ts.map +1 -1
  126. package/dist/internal/Webhooks.js +87 -13
  127. package/dist/internal/Webhooks.js.map +1 -1
  128. package/dist/internal/funding/Provider.d.ts +4 -0
  129. package/dist/internal/funding/Provider.d.ts.map +1 -1
  130. package/dist/internal/funding/Provider.js +11 -7
  131. package/dist/internal/funding/Provider.js.map +1 -1
  132. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  133. package/dist/internal/funding/Reconciliation.js +128 -0
  134. package/dist/internal/funding/Reconciliation.js.map +1 -1
  135. package/dist/internal/funding/providers/relay.d.ts +3 -0
  136. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  137. package/dist/internal/funding/providers/relay.js +12 -1
  138. package/dist/internal/funding/providers/relay.js.map +1 -1
  139. package/package.json +3 -3
  140. package/src/Client.test-d.ts +60 -5
  141. package/src/Webhooks.bench.ts +714 -0
  142. package/src/Webhooks.memory.bench.ts +250 -0
  143. package/src/Webhooks.test.ts +77 -6
  144. package/src/Webhooks.ts +83 -29
  145. package/src/apps/Zones.test.ts +5 -4
  146. package/src/apps/Zones.ts +9 -8
  147. package/src/apps/data/FxOracle.test.ts +18 -0
  148. package/src/apps/data/FxOracle.ts +28 -5
  149. package/src/apps/data/routes/activities.test.ts +19 -0
  150. package/src/apps/data/routes/activities.ts +34 -30
  151. package/src/apps/data/routes/balances.test.ts +119 -10
  152. package/src/apps/data/routes/balances.ts +154 -95
  153. package/src/apps/data/routes/blocks.test.ts +12 -6
  154. package/src/apps/data/routes/earn.test.ts +197 -0
  155. package/src/apps/data/routes/earn.ts +486 -44
  156. package/src/apps/data/routes/exchanges.test.ts +7 -6
  157. package/src/apps/data/routes/exchanges.ts +124 -117
  158. package/src/apps/data/routes/receipts.test.ts +24 -7
  159. package/src/apps/data/routes/receipts.ts +67 -50
  160. package/src/apps/data/routes/tokens.test.ts +51 -0
  161. package/src/apps/data/routes/transactions.test.ts +42 -0
  162. package/src/apps/data/routes/transactions.ts +44 -39
  163. package/src/apps/data/routes/transfers.test.ts +402 -24
  164. package/src/apps/data/routes/transfers.ts +708 -101
  165. package/src/apps/data/routes/valuation.test.ts +26 -0
  166. package/src/apps/data/routes/valuation.ts +6 -2
  167. package/src/apps/data/routes/webhooks.test.ts +414 -130
  168. package/src/apps/data/routes/webhooks.ts +603 -419
  169. package/src/apps/data/routes/zones.ts +6 -1
  170. package/src/apps/funding/routes/quotes.test.ts +1 -1
  171. package/src/apps/mpp/Mpp.test.ts +13 -1
  172. package/src/apps/mpp/Mpp.ts +8 -0
  173. package/src/apps/mpp/internal/Adapter.ts +10 -2
  174. package/src/db/Db.test.ts +40 -6
  175. package/src/db/Db.ts +33 -19
  176. package/src/db/tables/sponsoredTransactions.ts +9 -2
  177. package/src/db/tables/webhookSubscriptions.ts +116 -4
  178. package/src/internal/EarnVaults.test.ts +2 -2
  179. package/src/internal/MetricSink.test.ts +518 -0
  180. package/src/internal/MetricSink.ts +266 -9
  181. package/src/internal/Schema.ts +1 -1
  182. package/src/internal/Tidx.test.ts +4 -4
  183. package/src/internal/Tidx.ts +1 -3
  184. package/src/internal/Viem.test.ts +8 -0
  185. package/src/internal/Viem.ts +2 -1
  186. package/src/internal/WebhookDestination.test.ts +20 -0
  187. package/src/internal/WebhookDestination.ts +10 -0
  188. package/src/internal/WebhookTransfer.ts +426 -0
  189. package/src/internal/Webhooks.test.ts +137 -0
  190. package/src/internal/Webhooks.ts +131 -17
  191. package/src/internal/funding/Provider.test.ts +47 -7
  192. package/src/internal/funding/Provider.ts +12 -7
  193. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  194. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  195. package/src/internal/funding/Reconciliation.ts +136 -0
  196. package/src/internal/funding/SourceObservation.test.ts +1 -0
  197. package/src/internal/funding/providers/relay.test.ts +1 -4
  198. package/src/internal/funding/providers/relay.ts +16 -1
@@ -475,6 +475,7 @@ describe('createTracker', () => {
475
475
  createdAt: '2026-01-06T00:01:00.000Z',
476
476
  destinationTransactionHashes: [destinationTransactionHash],
477
477
  id: 'unsubsidized-request',
478
+ refundTransactionHashes: [],
478
479
  sourceTransactionHashes: [sourceTransactionHash],
479
480
  status: 'success',
480
481
  updatedAt: '2026-01-06T00:02:00.000Z',
@@ -626,6 +627,1244 @@ describe('createTracker', () => {
626
627
  }
627
628
  })
628
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
+
629
1868
  test('persists a detected deposit before source verification succeeds', async () => {
630
1869
  const route = TestFunding.transferSnapshot()
631
1870
  const depositAddress = `0x${'20'.repeat(20)}`
@@ -2500,6 +3739,7 @@ describe('createTracker', () => {
2500
3739
  createdAt: '2026-01-05T00:01:00.000Z',
2501
3740
  destinationTransactionHashes: [destinationTransactionHash],
2502
3741
  id: 'source-evidence-request',
3742
+ refundTransactionHashes: [],
2503
3743
  sourceTransactionHashes: [firstSourceTransactionHash, secondSourceTransactionHash],
2504
3744
  status: 'success',
2505
3745
  updatedAt: '2026-01-05T00:02:00.000Z',