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
@@ -13,12 +13,16 @@ import * as Subsidy from './Subsidy.js'
13
13
  const defaultBatchSize = 25
14
14
  const defaultLeaseMs = 2 * 60_000
15
15
  const defaultPollIntervalMs = 60_000
16
+ const chainProviderFollowUpDelaySeconds = 1
17
+ const chainProviderMaximumFollowUpAttempts = 30
16
18
  const followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 } as const satisfies Record<
17
19
  Provider.Webhook.receive.FollowUp['kind'],
18
20
  number
19
21
  >
20
22
  const maximumPages = 100
21
23
  const maximumFollowUpAttempts = 6
24
+ const providerFailureStatuses = ['failure'] as const
25
+ const providerRefundStatuses = ['refund', 'refunded'] as const
22
26
 
23
27
  /** Verified token transfer returned by an authoritative chain reader. */
24
28
  export type TransferEvidence = {
@@ -141,6 +145,18 @@ export function createTracker(options: createTracker.Options): createTracker.Tra
141
145
  followUpKind = result.followUp ?? followUpKind ?? 'provider'
142
146
  }
143
147
  }
148
+ const unattributed = await reconcileUnattributed(database, {
149
+ address: claimed,
150
+ metrics: options.metrics,
151
+ provider,
152
+ signal: options.signal,
153
+ subsidies: options.subsidies,
154
+ verifyTransfers: options.verifyTransfers,
155
+ })
156
+ heartbeat.assert()
157
+ updated += unattributed.updatedIds.size
158
+ if (!unattributed.terminal)
159
+ followUpKind = unattributed.followUp ?? followUpKind ?? 'provider'
144
160
  const awaitsProvider = await FundingDeposits.hasUnattributed(database, claimed.id)
145
161
  if (message.requestId && message.webhookTiming && !webhookUpdateObserved)
146
162
  pendingRequestIds.add(message.requestId)
@@ -570,10 +586,19 @@ type VerifiedDeposit = {
570
586
  sourceAmount: bigint
571
587
  }
572
588
 
573
- async function observeProviderDelivery(
574
- options: reconcileRequest.Options,
575
- verified: readonly VerifiedDeposit[],
576
- ) {
589
+ type ReconcileDeliveryOptions = {
590
+ address: FundingDepositAddresses.Record
591
+ metrics?: Metrics.Metrics | undefined
592
+ provider?: Provider.Provider | undefined
593
+ request?: Provider.listDepositAddressRequests.ReturnType['requests'][number] | undefined
594
+ signal?: AbortSignal | undefined
595
+ subsidies?: Subsidy.Settler | undefined
596
+ verified: readonly VerifiedDeposit[]
597
+ verifyTransfers: VerifyTransfers
598
+ }
599
+
600
+ async function observeProviderDelivery(options: ReconcileDeliveryOptions) {
601
+ const { verified } = options
577
602
  const [entry] = verified
578
603
  if (
579
604
  !entry ||
@@ -581,7 +606,7 @@ async function observeProviderDelivery(
581
606
  options.address.deliveryStrategy !== 'provider' ||
582
607
  options.address.providerOutputToken.tokenKey !==
583
608
  options.address.snapshot.destinationToken.tokenKey ||
584
- (options.request.status === 'success' &&
609
+ (options.request?.status === 'success' &&
585
610
  options.request.destinationTransactionHashes.length > 0) ||
586
611
  !options.provider ||
587
612
  !Provider.canObserveDepositAddressRequest(options.provider)
@@ -597,7 +622,7 @@ async function observeProviderDelivery(
597
622
  providerState: entry.deposit.providerState ?? undefined,
598
623
  recipient: options.address.recipient,
599
624
  refundAddress: options.address.refundAddress,
600
- request: options.request,
625
+ ...(options.request ? { request: options.request } : {}),
601
626
  source: entry.source,
602
627
  sourceChain: options.address.snapshot.sourceChain,
603
628
  sourceToken: options.address.snapshot.sourceToken,
@@ -615,6 +640,63 @@ async function observeProviderDelivery(
615
640
  return result
616
641
  }
617
642
 
643
+ async function reconcileUnattributed(db: Db.Db, options: reconcileUnattributed.Options) {
644
+ const deposits = await FundingDeposits.listUnattributed(db, options.address.id)
645
+ if (deposits.length === 0) return { terminal: true, updatedIds: new Set<string>() }
646
+
647
+ const transfersByTransaction = new Map(
648
+ await Promise.all(
649
+ unique(deposits.map((deposit) => deposit.sourceTransactionHash)).map(
650
+ async (transactionHash) =>
651
+ [
652
+ transactionHash,
653
+ await options.verifyTransfers({
654
+ chain: options.address.snapshot.sourceChain,
655
+ recipient: options.address.address,
656
+ token: options.address.snapshot.sourceToken,
657
+ transactionHash,
658
+ }),
659
+ ] as const,
660
+ ),
661
+ ),
662
+ )
663
+ let followUp: reconcileRequest.ReturnType['followUp']
664
+ let terminal = true
665
+ const updatedIds = new Set<string>()
666
+ for (const deposit of deposits) {
667
+ const source = transfersByTransaction
668
+ .get(deposit.sourceTransactionHash)
669
+ ?.find((transfer) => transfer.transferIndex === deposit.sourceTransferIndex)
670
+ if (!source) {
671
+ terminal = false
672
+ continue
673
+ }
674
+ const delivery = await reconcileDelivery(db, {
675
+ ...options,
676
+ verified: [
677
+ {
678
+ deposit,
679
+ required: destinationBaseUnits(
680
+ BigInt(source.amount),
681
+ options.address.snapshot.sourceToken.decimals,
682
+ options.address.snapshot.destinationToken.decimals,
683
+ ),
684
+ source,
685
+ sourceAmount: BigInt(source.amount),
686
+ },
687
+ ],
688
+ })
689
+ if (delivery.followUp === 'delivery' || !followUp) followUp = delivery.followUp
690
+ if (!delivery.terminal) terminal = false
691
+ for (const id of delivery.updatedIds) updatedIds.add(id)
692
+ }
693
+ return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
694
+ }
695
+
696
+ declare namespace reconcileUnattributed {
697
+ type Options = Omit<ReconcileDeliveryOptions, 'request' | 'verified'>
698
+ }
699
+
618
700
  /** Reconciles one provider request against verified chain evidence. */
619
701
  export async function reconcileRequest(
620
702
  db: Db.Db,
@@ -635,7 +717,6 @@ export async function reconcileRequest(
635
717
  : {}),
636
718
  })
637
719
  let created = 0
638
- let followUp: reconcileRequest.ReturnType['followUp']
639
720
  const observed: ObservedSourceTransaction[] = []
640
721
  // Persist provider observations first so transient chain failures do not hide acknowledged deposits.
641
722
  for (const transactionHash of unique(options.request.sourceTransactionHashes)) {
@@ -699,7 +780,6 @@ export async function reconcileRequest(
699
780
 
700
781
  const sourceEvidenceComplete =
701
782
  sources.length > 0 && sources.every((source) => source.transfers.length > 0)
702
- let terminal = sourceEvidenceComplete
703
783
  const updatedIds = new Set<string>()
704
784
  const verified: VerifiedDeposit[] = []
705
785
  for (const source of sources) {
@@ -758,7 +838,11 @@ export async function reconcileRequest(
758
838
  source.deposits.push(deposit)
759
839
  created += 1
760
840
  }
761
- if (!Deposit.isTerminal(deposit.status)) {
841
+ if (
842
+ !Deposit.isTerminal(deposit.status) ||
843
+ deposit.providerRequestId === null ||
844
+ !deposit.providerRequestIds.includes(options.request.id)
845
+ ) {
762
846
  const result = await Deposit.transition(db, {
763
847
  expectedVersion: deposit.version,
764
848
  id: deposit.id,
@@ -784,11 +868,195 @@ export async function reconcileRequest(
784
868
  }
785
869
  if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
786
870
  if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
871
+ const recovery = await reconcileProviderRecovery(db, { ...options, verified })
872
+ if (recovery) {
873
+ for (const id of recovery.updatedIds) updatedIds.add(id)
874
+ return {
875
+ created,
876
+ ...(recovery.followUp ? { followUp: recovery.followUp } : {}),
877
+ terminal: recovery.terminal,
878
+ updated: updatedIds.size,
879
+ }
880
+ }
881
+ const delivery = await reconcileDelivery(db, { ...options, verified })
882
+ for (const id of delivery.updatedIds) updatedIds.add(id)
883
+ return {
884
+ created,
885
+ ...(delivery.followUp ? { followUp: delivery.followUp } : {}),
886
+ terminal: delivery.terminal,
887
+ updated: updatedIds.size,
888
+ }
889
+ }
890
+
891
+ export declare namespace reconcileRequest {
892
+ /** Provider request, address, and settlement dependencies. */
893
+ type Options = {
894
+ /** Reusable deposit address attributed by the provider. */
895
+ address: FundingDepositAddresses.Record
896
+ /** Optional bounded operational metrics sink. */
897
+ metrics?: Metrics.Metrics | undefined
898
+ /** Provider integration that discovered the request. */
899
+ provider?: Provider.Provider | undefined
900
+ /** Provider request containing discovered chain transactions. */
901
+ request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
902
+ /** Abort signal forwarded to provider delivery observation. */
903
+ signal?: AbortSignal | undefined
904
+ /** Optional pathUSD-funded subsidy settler. */
905
+ subsidies?: Subsidy.Settler | undefined
906
+ /** Mechanism that requested reconciliation. */
907
+ trigger?: Deposit.DetectionTrigger | undefined
908
+ /** Authoritative source and destination transfer verifier. */
909
+ verifyTransfers: VerifyTransfers
910
+ }
911
+
912
+ /** Material changes produced by one provider request. */
913
+ type ReturnType = {
914
+ /** Number of newly detected source transfers. */
915
+ created: number
916
+ /** Work that needs a bounded delayed follow-up. */
917
+ followUp?: 'delivery' | 'settlement' | undefined
918
+ /** Whether every deposit from this request reached a terminal state. */
919
+ terminal: boolean
920
+ /** Number of existing or new deposits materially changed. */
921
+ updated: number
922
+ }
923
+ }
924
+
925
+ async function reconcileProviderRecovery(db: Db.Db, options: ReconcileDeliveryOptions) {
926
+ const failed = (providerFailureStatuses as readonly string[]).includes(
927
+ options.request?.status ?? '',
928
+ )
929
+ const refunded = (providerRefundStatuses as readonly string[]).includes(
930
+ options.request?.status ?? '',
931
+ )
932
+ if (!refunded && !failed) return undefined
933
+ if (failed) {
934
+ const destinationEvidence = await Promise.all(
935
+ unique(options.request?.destinationTransactionHashes ?? []).map((transactionHash) =>
936
+ options.verifyTransfers({
937
+ chain: options.address.snapshot.destinationChain,
938
+ recipient: options.address.recipient,
939
+ token: options.address.providerOutputToken,
940
+ transactionHash,
941
+ }),
942
+ ),
943
+ )
944
+ if (destinationEvidence.some((transfers) => transfers.length > 0)) return undefined
945
+ }
787
946
 
788
- const deliveryObservation = await observeProviderDelivery(options, verified)
947
+ const updatedIds = new Set<string>()
948
+ if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
949
+ return { terminal: true, updatedIds }
950
+
951
+ // Relay auto-refunds may include an internal settlement transaction; only matching source-token transfers become public evidence.
952
+ const providerRefundTransactionHashes = unique(options.request?.refundTransactionHashes ?? [])
953
+ const refundEvidence = await Promise.all(
954
+ providerRefundTransactionHashes.map((transactionHash) =>
955
+ options.verifyTransfers({
956
+ chain: options.address.snapshot.sourceChain,
957
+ recipient: options.address.refundAddress,
958
+ token: options.address.snapshot.sourceToken,
959
+ transactionHash,
960
+ }),
961
+ ),
962
+ )
963
+ const refundTransfers = refundEvidence.flat()
964
+ const refundOutput = refundTransfers.reduce(
965
+ (total, transfer) => total + BigInt(transfer.amount),
966
+ 0n,
967
+ )
968
+ const refundTransactionHashes = unique(
969
+ refundTransfers.map((transfer) => transfer.transactionHash),
970
+ )
971
+ // Relay can include internal transactions, so its reported amount proves when all matching refund transfers are visible.
972
+ const expectedRefundOutput = options.request?.refundAmountExpected
973
+ ? BigInt(options.request.refundAmountExpected)
974
+ : undefined
975
+ const refundEvidenceComplete = (() => {
976
+ if (refundOutput === 0n) return false
977
+ if (expectedRefundOutput !== undefined) return refundOutput === expectedRefundOutput
978
+ return refundEvidence.length > 0 && refundEvidence.every((transfers) => transfers.length > 0)
979
+ })()
980
+ const totalSource = options.verified.reduce((total, entry) => total + entry.sourceAmount, 0n)
981
+ let allocated = 0n
982
+ let cumulativeSource = 0n
983
+ let terminal = refundEvidenceComplete
984
+
985
+ // Include terminal rows in proportional allocation so retries preserve the original split without reallocating their verified refunds.
986
+ for (const entry of options.verified) {
987
+ cumulativeSource += entry.sourceAmount
988
+ const nextAllocated = totalSource === 0n ? 0n : (refundOutput * cumulativeSource) / totalSource
989
+ const amount = nextAllocated - allocated
990
+ allocated = nextAllocated
991
+ if (Deposit.isTerminal(entry.deposit.status)) continue
992
+ let deposit = entry.deposit
993
+ if (deposit.status !== 'refunding') {
994
+ const result = await Deposit.transition(db, {
995
+ expectedVersion: deposit.version,
996
+ id: deposit.id,
997
+ providerTransactionHashes: unique([
998
+ ...deposit.providerTransactionHashes,
999
+ ...providerRefundTransactionHashes,
1000
+ ]),
1001
+ status: 'refunding',
1002
+ })
1003
+ if (result.type === 'applied') {
1004
+ deposit = result.record
1005
+ updatedIds.add(deposit.id)
1006
+ } else if (result.type === 'stale') deposit = result.current
1007
+ }
1008
+ if (!refundEvidenceComplete) continue
1009
+ if (Deposit.isTerminal(deposit.status)) continue
1010
+ if (deposit.status !== 'refunding') {
1011
+ terminal = false
1012
+ continue
1013
+ }
1014
+ const result = await Deposit.transition(db, {
1015
+ expectedVersion: deposit.version,
1016
+ id: deposit.id,
1017
+ snapshot: {
1018
+ ...deposit.snapshot,
1019
+ refundAmount: Value.tokenAmount({
1020
+ baseUnits: amount,
1021
+ currency: options.address.snapshot.sourceToken.currency,
1022
+ decimals: options.address.snapshot.sourceToken.decimals,
1023
+ }),
1024
+ refundTransactionHashes,
1025
+ },
1026
+ status: 'refunded',
1027
+ })
1028
+ if (result.type === 'applied') updatedIds.add(deposit.id)
1029
+ else if (result.type !== 'stale' || !Deposit.isTerminal(result.current.status)) terminal = false
1030
+ }
1031
+
1032
+ const outcome = (() => {
1033
+ if (refundEvidenceComplete) return 'verified'
1034
+ // Relay failure can be followed by an automatic refund; durable exhaustion owns escalation to action-required.
1035
+ if (failed) return 'provider_failed'
1036
+ return 'pending'
1037
+ })()
1038
+ options.metrics?.count('funding_deposit_refund_count', 1, {
1039
+ outcome,
1040
+ provider: options.address.providerId,
1041
+ })
1042
+ return {
1043
+ ...(!refundEvidenceComplete ? { followUp: 'delivery' as const } : {}),
1044
+ terminal,
1045
+ updatedIds,
1046
+ }
1047
+ }
1048
+
1049
+ async function reconcileDelivery(db: Db.Db, options: ReconcileDeliveryOptions) {
1050
+ let followUp: reconcileRequest.ReturnType['followUp']
1051
+ let terminal = true
1052
+ const updatedIds = new Set<string>()
1053
+ if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
1054
+ return { terminal, updatedIds }
1055
+ const deliveryObservation = await observeProviderDelivery(options)
789
1056
  if (deliveryObservation?.type === 'pending') followUp = 'delivery'
1057
+ const providerTransactionHashes = options.request?.destinationTransactionHashes ?? []
790
1058
  const destinationTransactionHashes = unique([
791
- ...options.request.destinationTransactionHashes,
1059
+ ...providerTransactionHashes,
792
1060
  ...(deliveryObservation?.type === 'verified'
793
1061
  ? deliveryObservation.destinationTransactionHashes
794
1062
  : []),
@@ -805,18 +1073,18 @@ export async function reconcileRequest(
805
1073
  )
806
1074
  const destinationTransfers = destinationEvidence.flat()
807
1075
  const providerEvidenceComplete =
808
- (options.request.status === 'success' || deliveryObservation?.type === 'verified') &&
1076
+ (options.request?.status === 'success' || deliveryObservation?.type === 'verified') &&
809
1077
  destinationEvidence.length > 0 &&
810
1078
  destinationEvidence.every((transfers) => transfers.length > 0)
811
1079
  const providerOutput = destinationTransfers.reduce(
812
1080
  (total, transfer) => total + BigInt(transfer.amount),
813
1081
  0n,
814
1082
  )
815
- const totalRequired = verified.reduce((total, entry) => total + entry.required, 0n)
1083
+ const totalRequired = options.verified.reduce((total, entry) => total + entry.required, 0n)
816
1084
  let allocated = 0n
817
1085
  let cumulativeRequired = 0n
818
1086
 
819
- for (const entry of verified) {
1087
+ for (const entry of options.verified) {
820
1088
  const { required, sourceAmount } = entry
821
1089
  cumulativeRequired += required
822
1090
  const nextAllocated =
@@ -860,7 +1128,9 @@ export async function reconcileRequest(
860
1128
  }),
861
1129
  }
862
1130
  : {}),
863
- providerRequestIds: unique([...deposit.providerRequestIds, options.request.id]),
1131
+ providerRequestIds: options.request
1132
+ ? unique([...deposit.providerRequestIds, options.request.id])
1133
+ : deposit.providerRequestIds,
864
1134
  ...(deliveryObservation?.providerState
865
1135
  ? {
866
1136
  providerState: {
@@ -871,7 +1141,7 @@ export async function reconcileRequest(
871
1141
  : {}),
872
1142
  providerTransactionHashes: unique([
873
1143
  ...deposit.providerTransactionHashes,
874
- ...options.request.destinationTransactionHashes,
1144
+ ...providerTransactionHashes,
875
1145
  ...(deliveryObservation?.providerTransactionHashes ?? []),
876
1146
  ]),
877
1147
  ...(settlement.settlementTransaction
@@ -914,41 +1184,7 @@ export async function reconcileRequest(
914
1184
  }
915
1185
  }
916
1186
  }
917
- return { created, ...(followUp ? { followUp } : {}), terminal, updated: updatedIds.size }
918
- }
919
-
920
- export declare namespace reconcileRequest {
921
- /** Provider request, address, and settlement dependencies. */
922
- type Options = {
923
- /** Reusable deposit address attributed by the provider. */
924
- address: FundingDepositAddresses.Record
925
- /** Optional bounded operational metrics sink. */
926
- metrics?: Metrics.Metrics | undefined
927
- /** Provider integration that discovered the request. */
928
- provider?: Provider.Provider | undefined
929
- /** Provider request containing discovered chain transactions. */
930
- request: Provider.listDepositAddressRequests.ReturnType['requests'][number]
931
- /** Abort signal forwarded to provider delivery observation. */
932
- signal?: AbortSignal | undefined
933
- /** Optional pathUSD-funded subsidy settler. */
934
- subsidies?: Subsidy.Settler | undefined
935
- /** Mechanism that requested reconciliation. */
936
- trigger?: Deposit.DetectionTrigger | undefined
937
- /** Authoritative source and destination transfer verifier. */
938
- verifyTransfers: VerifyTransfers
939
- }
940
-
941
- /** Material changes produced by one provider request. */
942
- type ReturnType = {
943
- /** Number of newly detected source transfers. */
944
- created: number
945
- /** Work that needs a bounded delayed follow-up. */
946
- followUp?: 'delivery' | 'settlement' | undefined
947
- /** Whether every deposit from this request reached a terminal state. */
948
- terminal: boolean
949
- /** Number of existing or new deposits materially changed. */
950
- updated: number
951
- }
1187
+ return { ...(followUp ? { followUp } : {}), terminal, updatedIds }
952
1188
  }
953
1189
 
954
1190
  type ReconcileSubsidyOptions = {
@@ -1149,8 +1385,15 @@ function nextFollowUp(
1149
1385
  kind: Provider.Webhook.receive.FollowUp['kind'],
1150
1386
  ) {
1151
1387
  const attempt = message.followUp?.kind === kind ? message.followUp.attempt + 1 : 0
1152
- if (attempt >= maximumFollowUpAttempts) return undefined
1153
- return { delaySeconds: followUpDelaySeconds[kind], state: { attempt, kind } }
1388
+ const chainProvider = kind === 'provider' && message.trigger === 'chain'
1389
+ const maximumAttempts = chainProvider
1390
+ ? chainProviderMaximumFollowUpAttempts
1391
+ : maximumFollowUpAttempts
1392
+ if (attempt >= maximumAttempts) return undefined
1393
+ return {
1394
+ delaySeconds: chainProvider ? chainProviderFollowUpDelaySeconds : followUpDelaySeconds[kind],
1395
+ state: { attempt, kind },
1396
+ }
1154
1397
  }
1155
1398
 
1156
1399
  type RecordWebhookLatencyOptions = {
@@ -188,6 +188,7 @@ describe('observe', () => {
188
188
  createdAt: '2026-01-01T00:00:02.000Z',
189
189
  destinationTransactionHashes: [],
190
190
  id: 'relay-source-request',
191
+ refundTransactionHashes: [],
191
192
  sourceTransactionHashes: [sourceTransactionHash],
192
193
  status: 'pending',
193
194
  updatedAt: '2026-01-01T00:00:03.000Z',
@@ -230,7 +231,7 @@ describe('observe', () => {
230
231
  })(),
231
232
  }).toMatchInlineSnapshot(
232
233
  {
233
- dispatches: [[{ addressId: expect.any(String) }, { delaySeconds: 5 }]],
234
+ dispatches: [[{ addressId: expect.any(String) }, { delaySeconds: 1 }]],
234
235
  },
235
236
  `
236
237
  {
@@ -246,7 +247,7 @@ describe('observe', () => {
246
247
  "type": "funding:deposit-address:reconcile",
247
248
  },
248
249
  {
249
- "delaySeconds": 5,
250
+ "delaySeconds": 1,
250
251
  },
251
252
  ],
252
253
  ],
@@ -151,11 +151,8 @@ describe.skipIf(!apiKey)('relay.prepareTransfer (live)', () => {
151
151
  expect(result.correlation.requestId).toMatch(/^0x[0-9a-f]{64}$/)
152
152
  expect(result.correlation.checkEndpoint).toContain('/intents/status/v3')
153
153
  expect(BigInt(result.destinationAmount) > 0n).toBe(true)
154
+ expect(BigInt(result.destinationAmountMin) > 0n).toBe(true)
154
155
  expect(BigInt(result.destinationAmountMin) <= BigInt(result.destinationAmount)).toBe(true)
155
- // The forwarded slippage bound holds on the quoted minimum.
156
- expect(
157
- BigInt(result.destinationAmountMin) >= (BigInt(result.destinationAmount) * 9_700n) / 10_000n,
158
- ).toBe(true)
159
156
  // Relay returns no quote validity window; the fan-out applies the default.
160
157
  expect(result.expiresAt).toBeUndefined()
161
158
  expect(Number.isNaN(Date.parse(result.sampledAt))).toBe(false)
@@ -17,6 +17,7 @@ const defaultBaseUrl = 'https://api.relay.link'
17
17
  const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init)
18
18
  const deliveryBackfillBlocks = 256n
19
19
  const relayChainsCacheTtlMs = 60_000
20
+ const relayRefundStatuses = ['refund', 'refunded'] as const
20
21
  const sourceSweepForwardBlocks = 256n
21
22
  const depositErc20FullAllowance = AbiFunction.from(
22
23
  'function depositErc20(address depositor,address token,bytes32 id)',
@@ -46,6 +47,7 @@ const webhookStatuses = [
46
47
  'fallback',
47
48
  'pending',
48
49
  'refund',
50
+ 'refunded',
49
51
  'submitted',
50
52
  'success',
51
53
  'waiting',
@@ -196,6 +198,13 @@ namespace schema {
196
198
  ),
197
199
  ),
198
200
  id: z.string().check(z.minLength(1)),
201
+ refundCurrencyData: z.optional(
202
+ z.nullable(
203
+ z.object({
204
+ amount: z.string().check(z.regex(/^\d+$/)),
205
+ }),
206
+ ),
207
+ ),
199
208
  protocol: z.optional(
200
209
  z.nullable(
201
210
  z.object({
@@ -481,6 +490,8 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
481
490
  return {
482
491
  ...(parsed.continuation ? { continuation: parsed.continuation } : {}),
483
492
  requests: parsed.requests.map((request) => {
493
+ const outputTransactionHashes = request.data?.outTxs?.map((tx) => tx.txHash) ?? []
494
+ const refunded = (relayRefundStatuses as readonly string[]).includes(request.status)
484
495
  const correlation = (() => {
485
496
  const { protocol } = request
486
497
  if (!protocol) return undefined
@@ -499,9 +510,13 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
499
510
  })()
500
511
  return {
501
512
  createdAt: new Date(request.createdAt).toISOString(),
502
- destinationTransactionHashes: request.data?.outTxs?.map((tx) => tx.txHash) ?? [],
513
+ destinationTransactionHashes: refunded ? [] : outputTransactionHashes,
503
514
  id: request.id,
504
515
  ...(correlation ? { providerState: correlation } : {}),
516
+ ...(refunded && request.refundCurrencyData
517
+ ? { refundAmountExpected: request.refundCurrencyData.amount }
518
+ : {}),
519
+ refundTransactionHashes: refunded ? outputTransactionHashes : [],
505
520
  sourceTransactionHashes: request.depositAddress?.depositTxHash
506
521
  ? [request.depositAddress.depositTxHash]
507
522
  : [],
@@ -709,7 +724,10 @@ function createDepositAddressObserver(
709
724
  }
710
725
 
711
726
  return async (parameters, signal) => {
712
- if (!(deliveryObservationStatuses as readonly string[]).includes(parameters.request.status))
727
+ if (
728
+ parameters.request &&
729
+ !(deliveryObservationStatuses as readonly string[]).includes(parameters.request.status)
730
+ )
713
731
  return { type: 'unsupported' }
714
732
  if (parameters.sourceChain.kind !== 'evm' || parameters.destinationChain.kind !== 'evm')
715
733
  return { type: 'unsupported' }
@@ -739,7 +757,7 @@ function createDepositAddressObserver(
739
757
  )
740
758
  return { type: 'unsupported' }
741
759
 
742
- const parsedCorrelation = schema.RequestCorrelation.safeParse(parameters.request.providerState)
760
+ const parsedCorrelation = schema.RequestCorrelation.safeParse(parameters.request?.providerState)
743
761
  const correlation = parsedCorrelation.success ? parsedCorrelation.data.relay : undefined
744
762
  if (
745
763
  (correlation?.depository &&
@@ -854,6 +872,8 @@ function createDepositAddressObserver(
854
872
  }
855
873
  const destinationTransactionHashes = (() => {
856
874
  if (correlatedTransactionHashes.length > 0) return correlatedTransactionHashes
875
+ // Provider-independent completion requires the Relay order id to bind both chain legs.
876
+ if (!parameters.request) return []
857
877
  if (uncorrelatedTransactionHashes.length === 1) return uncorrelatedTransactionHashes
858
878
  return []
859
879
  })()