tapimo 0.13.0 → 0.14.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 (230) hide show
  1. package/dist/App.d.ts +6064 -4829
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +1 -0
  4. package/dist/App.js.map +1 -1
  5. package/dist/Webhooks.js +2 -2
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/App.d.ts +1008 -917
  8. package/dist/admin/App.d.ts.map +1 -1
  9. package/dist/admin/apps/earn-vaults.d.ts +48 -0
  10. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  11. package/dist/admin/apps/earn-vaults.js +2 -1
  12. package/dist/admin/apps/earn-vaults.js.map +1 -1
  13. package/dist/admin/apps/organizations.d.ts +92 -3
  14. package/dist/admin/apps/organizations.d.ts.map +1 -1
  15. package/dist/admin/apps/organizations.js +69 -7
  16. package/dist/admin/apps/organizations.js.map +1 -1
  17. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  18. package/dist/admin/ui.generated.d.ts +1 -1
  19. package/dist/admin/ui.generated.js +69 -69
  20. package/dist/admin/ui.generated.js.map +1 -1
  21. package/dist/apps/Zones.d.ts +12 -12
  22. package/dist/apps/Zones.js +3 -3
  23. package/dist/apps/data/App.d.ts +2909 -2523
  24. package/dist/apps/data/App.d.ts.map +1 -1
  25. package/dist/apps/data/App.js +11 -6
  26. package/dist/apps/data/App.js.map +1 -1
  27. package/dist/apps/data/routes/addresses.d.ts +22 -22
  28. package/dist/apps/data/routes/earn.d.ts +183 -7
  29. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  30. package/dist/apps/data/routes/earn.js +434 -75
  31. package/dist/apps/data/routes/earn.js.map +1 -1
  32. package/dist/apps/data/routes/exchanges.d.ts +15 -0
  33. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  34. package/dist/apps/data/routes/exchanges.js +180 -114
  35. package/dist/apps/data/routes/exchanges.js.map +1 -1
  36. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  37. package/dist/apps/data/routes/fee-amm.js +18 -19
  38. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  39. package/dist/apps/data/routes/tokens.d.ts +93 -93
  40. package/dist/apps/data/routes/transfers.d.ts +12 -12
  41. package/dist/apps/data/routes/valuation.d.ts +30 -30
  42. package/dist/apps/data/routes/verified-tokens.d.ts +5 -5
  43. package/dist/apps/data/routes/webhooks.d.ts +805 -102
  44. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  45. package/dist/apps/data/routes/webhooks.js +47 -5
  46. package/dist/apps/data/routes/webhooks.js.map +1 -1
  47. package/dist/apps/funding/App.d.ts +456 -45
  48. package/dist/apps/funding/App.d.ts.map +1 -1
  49. package/dist/apps/funding/App.js +1 -1
  50. package/dist/apps/funding/App.js.map +1 -1
  51. package/dist/apps/funding/routes/deposit-addresses.d.ts +17 -17
  52. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  53. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  54. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  55. package/dist/apps/funding/routes/transfers.d.ts +654 -73
  56. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  57. package/dist/apps/funding/routes/transfers.js +211 -5
  58. package/dist/apps/funding/routes/transfers.js.map +1 -1
  59. package/dist/apps/management/App.d.ts +831 -387
  60. package/dist/apps/management/App.d.ts.map +1 -1
  61. package/dist/apps/management/Billing.d.ts +1 -1
  62. package/dist/apps/management/Billing.d.ts.map +1 -1
  63. package/dist/apps/management/Billing.js +17 -1
  64. package/dist/apps/management/Billing.js.map +1 -1
  65. package/dist/apps/management/routes/api-keys.d.ts +76 -76
  66. package/dist/apps/management/routes/billing.d.ts +76 -76
  67. package/dist/apps/management/routes/faucet.d.ts +12 -12
  68. package/dist/apps/management/routes/invitations.d.ts +24 -24
  69. package/dist/apps/management/routes/invite-links.d.ts +46 -46
  70. package/dist/apps/management/routes/members.d.ts +46 -46
  71. package/dist/apps/management/routes/orgs.d.ts +20 -20
  72. package/dist/apps/management/routes/projects.d.ts +44 -44
  73. package/dist/apps/management/routes/usage.d.ts +32 -32
  74. package/dist/apps/management/routes/verified-token-requests.d.ts +16 -16
  75. package/dist/apps/management/routes/webhooks.d.ts +707 -57
  76. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  77. package/dist/apps/management/routes/webhooks.js +21 -21
  78. package/dist/apps/management/routes/webhooks.js.map +1 -1
  79. package/dist/apps/relay/App.d.ts.map +1 -1
  80. package/dist/apps/relay/App.js +115 -23
  81. package/dist/apps/relay/App.js.map +1 -1
  82. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  83. package/dist/apps/relay/Sponsorships.js +27 -1
  84. package/dist/apps/relay/Sponsorships.js.map +1 -1
  85. package/dist/db/Db.d.ts.map +1 -1
  86. package/dist/db/Db.js +22 -0
  87. package/dist/db/Db.js.map +1 -1
  88. package/dist/db/Schema.d.ts +8 -3
  89. package/dist/db/Schema.d.ts.map +1 -1
  90. package/dist/db/Schema.js +16 -1
  91. package/dist/db/Schema.js.map +1 -1
  92. package/dist/db/tables/fundingTransferTransactions.d.ts +38 -0
  93. package/dist/db/tables/fundingTransferTransactions.d.ts.map +1 -1
  94. package/dist/db/tables/fundingTransferTransactions.js +44 -3
  95. package/dist/db/tables/fundingTransferTransactions.js.map +1 -1
  96. package/dist/db/tables/fundingTransfers.d.ts +17 -0
  97. package/dist/db/tables/fundingTransfers.d.ts.map +1 -1
  98. package/dist/db/tables/fundingTransfers.js +21 -0
  99. package/dist/db/tables/fundingTransfers.js.map +1 -1
  100. package/dist/db/tables/organizations.d.ts +24 -0
  101. package/dist/db/tables/organizations.d.ts.map +1 -1
  102. package/dist/db/tables/organizations.js +27 -0
  103. package/dist/db/tables/organizations.js.map +1 -1
  104. package/dist/db/tables/projects.d.ts +25 -1
  105. package/dist/db/tables/projects.d.ts.map +1 -1
  106. package/dist/db/tables/projects.js +26 -0
  107. package/dist/db/tables/projects.js.map +1 -1
  108. package/dist/db/tables/sponsoredTransactions.d.ts +44 -0
  109. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  110. package/dist/db/tables/sponsoredTransactions.js +146 -13
  111. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  112. package/dist/db/tables/webhookSubscriptions.d.ts +10 -6
  113. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  114. package/dist/db/tables/webhookSubscriptions.js +9 -6
  115. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  116. package/dist/internal/EarnVaults.js +5 -5
  117. package/dist/internal/EarnVaults.js.map +1 -1
  118. package/dist/internal/Log.d.ts +1 -1
  119. package/dist/internal/Log.d.ts.map +1 -1
  120. package/dist/internal/Log.js.map +1 -1
  121. package/dist/internal/Viem.d.ts +16 -1
  122. package/dist/internal/Viem.d.ts.map +1 -1
  123. package/dist/internal/Viem.js +12 -2
  124. package/dist/internal/Viem.js.map +1 -1
  125. package/dist/internal/funding/DepositWebhook.d.ts.map +1 -1
  126. package/dist/internal/funding/DepositWebhook.js +2 -1
  127. package/dist/internal/funding/DepositWebhook.js.map +1 -1
  128. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  129. package/dist/internal/funding/Evidence.js +2 -10
  130. package/dist/internal/funding/Evidence.js.map +1 -1
  131. package/dist/internal/funding/Provider.d.ts +40 -0
  132. package/dist/internal/funding/Provider.d.ts.map +1 -1
  133. package/dist/internal/funding/Provider.js +5 -0
  134. package/dist/internal/funding/Provider.js.map +1 -1
  135. package/dist/internal/funding/SourceObservation.d.ts.map +1 -1
  136. package/dist/internal/funding/SourceObservation.js +5 -7
  137. package/dist/internal/funding/SourceObservation.js.map +1 -1
  138. package/dist/internal/funding/Transfer.d.ts +101 -6
  139. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  140. package/dist/internal/funding/Transfer.js +162 -34
  141. package/dist/internal/funding/Transfer.js.map +1 -1
  142. package/dist/internal/funding/TransferReconciliation.d.ts +52 -0
  143. package/dist/internal/funding/TransferReconciliation.d.ts.map +1 -0
  144. package/dist/internal/funding/TransferReconciliation.js +192 -0
  145. package/dist/internal/funding/TransferReconciliation.js.map +1 -0
  146. package/dist/internal/funding/TransferWebhook.d.ts +21 -0
  147. package/dist/internal/funding/TransferWebhook.d.ts.map +1 -0
  148. package/dist/internal/funding/TransferWebhook.js +43 -0
  149. package/dist/internal/funding/TransferWebhook.js.map +1 -0
  150. package/dist/internal/funding/index.d.ts +1 -0
  151. package/dist/internal/funding/index.d.ts.map +1 -1
  152. package/dist/internal/funding/index.js +1 -0
  153. package/dist/internal/funding/index.js.map +1 -1
  154. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  155. package/dist/internal/funding/providers/relay.js +3 -12
  156. package/dist/internal/funding/providers/relay.js.map +1 -1
  157. package/dist/internal/funding/providers/stargate.d.ts +99 -0
  158. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  159. package/dist/internal/funding/providers/stargate.js +337 -35
  160. package/dist/internal/funding/providers/stargate.js.map +1 -1
  161. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -1
  162. package/dist/internal/webhookDestinations/slack.js +16 -0
  163. package/dist/internal/webhookDestinations/slack.js.map +1 -1
  164. package/package.json +1 -1
  165. package/src/App.ts +1 -0
  166. package/src/Client.test-d.ts +83 -2
  167. package/src/Webhooks.ts +2 -2
  168. package/src/admin/apps/earn-vaults.ts +2 -1
  169. package/src/admin/apps/organizations.test.ts +53 -1
  170. package/src/admin/apps/organizations.ts +90 -5
  171. package/src/admin/ui/src/lib/organizations.test.ts +112 -1
  172. package/src/admin/ui/src/lib/organizations.ts +55 -0
  173. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +116 -0
  174. package/src/admin/ui.generated.ts +69 -69
  175. package/src/apps/Zones.test.ts +3 -3
  176. package/src/apps/Zones.ts +3 -3
  177. package/src/apps/composition.test.ts +1 -0
  178. package/src/apps/data/App.ts +15 -6
  179. package/src/apps/data/routes/earn.test.ts +369 -14
  180. package/src/apps/data/routes/earn.ts +548 -79
  181. package/src/apps/data/routes/exchanges.test.ts +740 -15
  182. package/src/apps/data/routes/exchanges.ts +954 -753
  183. package/src/apps/data/routes/fee-amm.ts +17 -18
  184. package/src/apps/data/routes/webhooks.test.ts +21 -5
  185. package/src/apps/data/routes/webhooks.ts +63 -8
  186. package/src/apps/funding/App.ts +12 -2
  187. package/src/apps/funding/routes/deposit-addresses.test.ts +4 -4
  188. package/src/apps/funding/routes/deposit-addresses.ts +2 -2
  189. package/src/apps/funding/routes/transfers.test.ts +510 -19
  190. package/src/apps/funding/routes/transfers.ts +271 -5
  191. package/src/apps/management/Billing.ts +18 -5
  192. package/src/apps/management/routes/webhooks.test.ts +3 -3
  193. package/src/apps/management/routes/webhooks.ts +24 -25
  194. package/src/apps/relay/App.test.ts +212 -1
  195. package/src/apps/relay/App.ts +158 -29
  196. package/src/apps/relay/Sponsorships.ts +24 -1
  197. package/src/db/Db.ts +35 -0
  198. package/src/db/Schema.ts +17 -2
  199. package/src/db/tables/fundingTransferTransactions.test.ts +43 -0
  200. package/src/db/tables/fundingTransferTransactions.ts +79 -3
  201. package/src/db/tables/fundingTransfers.test.ts +26 -0
  202. package/src/db/tables/fundingTransfers.ts +36 -0
  203. package/src/db/tables/organizations.test.ts +41 -0
  204. package/src/db/tables/organizations.ts +49 -0
  205. package/src/db/tables/projects.test.ts +55 -0
  206. package/src/db/tables/projects.ts +46 -1
  207. package/src/db/tables/sponsoredTransactions.test.ts +338 -0
  208. package/src/db/tables/sponsoredTransactions.ts +197 -13
  209. package/src/db/tables/webhookSubscriptions.ts +18 -11
  210. package/src/internal/EarnVaults.ts +5 -5
  211. package/src/internal/Log.test-d.ts +2 -0
  212. package/src/internal/Log.ts +2 -0
  213. package/src/internal/Viem.ts +39 -3
  214. package/src/internal/Webhooks.test.ts +45 -0
  215. package/src/internal/funding/DepositWebhook.ts +2 -1
  216. package/src/internal/funding/Evidence.ts +2 -13
  217. package/src/internal/funding/Provider.ts +43 -0
  218. package/src/internal/funding/SourceObservation.ts +5 -9
  219. package/src/internal/funding/Transfer.test.ts +155 -0
  220. package/src/internal/funding/Transfer.ts +254 -37
  221. package/src/internal/funding/TransferReconciliation.test.ts +147 -0
  222. package/src/internal/funding/TransferReconciliation.ts +264 -0
  223. package/src/internal/funding/TransferWebhook.test.ts +128 -0
  224. package/src/internal/funding/TransferWebhook.ts +67 -0
  225. package/src/internal/funding/index.test.ts +1 -0
  226. package/src/internal/funding/index.ts +1 -0
  227. package/src/internal/funding/providers/relay.ts +3 -15
  228. package/src/internal/funding/providers/stargate.test.ts +169 -4
  229. package/src/internal/funding/providers/stargate.ts +498 -51
  230. package/src/internal/webhookDestinations/slack.ts +15 -0
@@ -1,16 +1,34 @@
1
- import { AbiFunction, Address, Hex, RpcTransport } from 'ox'
2
- import { createClient, custom, fallback, publicActions } from 'viem'
1
+ import { AbiEvent, AbiFunction, Address, Hash, Hex } from 'ox'
2
+ import {
3
+ BlockNotFoundError,
4
+ parseEventLogs,
5
+ publicActions,
6
+ TransactionReceiptNotFoundError,
7
+ } from 'viem'
3
8
  import * as z from 'zod/mini'
4
9
 
10
+ import * as Viem from '../../Viem.js'
5
11
  import * as Action from '../Action.js'
6
12
  import type * as Catalog from '../Catalog.js'
13
+ import * as Chain from '../Chain.js'
7
14
  import * as FundingProvider from '../Provider.js'
8
15
 
9
16
  const approve = AbiFunction.from('function approve(address spender, uint256 amount)')
10
17
  const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init)
11
18
  const destinationChainId = 'eip155:4217'
12
19
  const destinationEid = 30_410
20
+ const destinationPool = '0x8c76e2f6c5ceda9aa7772e7eff30280226c44392'
13
21
  const destinationToken = '0x20C000000000000000000000b9537d11c60E8b50'
22
+ const layerZeroEndpoint = '0x1a44076050125825900e736c501f859c50fE728c'
23
+ const oftReceived = AbiEvent.from(
24
+ 'event OFTReceived(bytes32 indexed guid, uint32 srcEid, address indexed toAddress, uint256 amountReceivedLD)',
25
+ )
26
+ const oftSent = AbiEvent.from(
27
+ 'event OFTSent(bytes32 indexed guid, uint32 dstEid, address indexed fromAddress, uint256 amountSentLD, uint256 amountReceivedLD)',
28
+ )
29
+ const packetSent = AbiEvent.from(
30
+ 'event PacketSent(bytes encodedPayload, bytes options, address sendLibrary)',
31
+ )
14
32
  const quoteOft = AbiFunction.from(
15
33
  'function quoteOFT((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam) view returns ((uint256 minAmountLD, uint256 maxAmountLD) limit, (int256 feeAmountLD, string description)[] oftFeeDetails, (uint256 amountSentLD, uint256 amountReceivedLD) receipt)',
16
34
  )
@@ -21,21 +39,48 @@ const sendToken = AbiFunction.from(
21
39
  'function sendToken((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, (uint256 nativeFee, uint256 lzTokenFee) fee, address refundAddress) payable returns ((bytes32 guid, uint64 nonce, (uint256 nativeFee, uint256 lzTokenFee) fee) msgReceipt, (uint256 amountSentLD, uint256 amountReceivedLD) oftReceipt, (uint72 ticketId, bytes passengerBytes) ticket)',
22
40
  )
23
41
  const quoteTtlMs = 60_000
42
+ const maximumLogBlockRange = 100_000n
43
+ const sourceEids = {
44
+ 'eip155:1': 30_101,
45
+ 'eip155:8453': 30_184,
46
+ } as const
47
+ const transferEvent = AbiEvent.from(
48
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
49
+ )
24
50
  const zeroAddress = '0x0000000000000000000000000000000000000000'
25
51
 
26
- const configuration = z
27
- .strictObject({
28
- poolAddress: z
29
- .string()
30
- .check(
31
- z.regex(/^0x[0-9a-fA-F]{40}$/),
32
- z.describe('Stargate pool contract on the source chain.'),
52
+ /** Stargate provider schemas. */
53
+ export namespace schema {
54
+ /** Configuration captured from one selected Stargate route. */
55
+ export const Configuration = z
56
+ .strictObject({
57
+ destinationPoolAddress: z.optional(
58
+ z
59
+ .string()
60
+ .check(
61
+ z.regex(/^0x[0-9a-fA-F]{40}$/),
62
+ z.describe('Stargate pool contract on the Tempo destination chain.'),
63
+ ),
33
64
  ),
34
- })
35
- .check(z.describe('Stargate configuration for one funding route.'))
65
+ poolAddress: z
66
+ .string()
67
+ .check(
68
+ z.regex(/^0x[0-9a-fA-F]{40}$/),
69
+ z.describe('Stargate pool contract on the source chain.'),
70
+ ),
71
+ sourceEid: z.optional(
72
+ z
73
+ .number()
74
+ .check(z.int(), z.positive(), z.describe('LayerZero endpoint ID for the source chain.')),
75
+ ),
76
+ })
77
+ .check(z.describe('Stargate configuration for one funding route.'))
78
+ }
36
79
 
37
80
  type Deployment = {
81
+ destinationPool: Address.Address
38
82
  pool: Address.Address
83
+ sourceEid: number
39
84
  sourceToken: Address.Address
40
85
  }
41
86
 
@@ -72,7 +117,11 @@ export function stargate(options: stargate.Options = {}) {
72
117
  amount: candidate.sourceAmount.amount,
73
118
  recipient: zeroAddress,
74
119
  })
75
- const [limit, feeDetails, receipt] = await client({ fetch, route, signal }).readContract({
120
+ const [limit, feeDetails, receipt] = await rpcClient({
121
+ fetch,
122
+ signal,
123
+ urls: route.source.chain.rpcUrls,
124
+ }).readContract({
76
125
  abi: [quoteOft],
77
126
  address: deployment.pool,
78
127
  args: [sendParam],
@@ -107,8 +156,8 @@ export function stargate(options: stargate.Options = {}) {
107
156
  })
108
157
  if (!deployment) throw new FundingProvider.ProviderUnavailableError()
109
158
  const sendParam = createSendParam({ amount: candidate.sourceAmount.amount, recipient })
110
- const rpc = client({ fetch, route: candidate.route, signal })
111
- const [[limit, feeDetails, receipt], messagingFee] = await Promise.all([
159
+ const rpc = rpcClient({ fetch, signal, urls: candidate.route.source.chain.rpcUrls })
160
+ const [[limit, feeDetails, receipt], messagingFee, destinationBlock] = await Promise.all([
112
161
  rpc.readContract({
113
162
  abi: [quoteOft],
114
163
  address: deployment.pool,
@@ -122,6 +171,11 @@ export function stargate(options: stargate.Options = {}) {
122
171
  args: [sendParam, false],
123
172
  functionName: 'quoteSend',
124
173
  }),
174
+ rpcClient({
175
+ fetch,
176
+ signal,
177
+ urls: candidate.route.destination.chain.rpcUrls,
178
+ }).getBlock({ blockTag: 'finalized', includeTransactions: false }),
125
179
  ])
126
180
  if (!isExactQuote({ amount: candidate.sourceAmount.amount, feeDetails, limit, receipt }))
127
181
  throw new FundingProvider.ProviderUnavailableError()
@@ -172,6 +226,10 @@ export function stargate(options: stargate.Options = {}) {
172
226
  token: nativeToken(candidate.sourceChain.id),
173
227
  },
174
228
  ],
229
+ providerState: {
230
+ destinationBlockNumber: destinationBlock.number.toString(),
231
+ routeConfiguration: candidate.configuration,
232
+ },
175
233
  quality: quality(),
176
234
  sampledAt: sampledAt.toISOString(),
177
235
  }
@@ -190,6 +248,62 @@ export function stargate(options: stargate.Options = {}) {
190
248
  }),
191
249
  }
192
250
  },
251
+ async verifySourceTransaction(
252
+ {
253
+ destinationChain,
254
+ providerState,
255
+ routeConfiguration,
256
+ sourceChain,
257
+ transactionHash,
258
+ transfer,
259
+ },
260
+ signal,
261
+ ) {
262
+ if (transfer.method !== 'transaction' || transfer.mode !== 'exactSource' || !transfer.sender)
263
+ return { type: 'invalid' }
264
+ const configuration = providerState?.['routeConfiguration'] ?? routeConfiguration
265
+ const evidence = await verifySourceTransaction({
266
+ configuration,
267
+ destinationTokenAddress: transfer.destinationToken.address,
268
+ fetch,
269
+ recipient: transfer.recipient,
270
+ sender: transfer.sender,
271
+ signal,
272
+ sourceAmount: transfer.sourceAmount.baseUnits,
273
+ sourceChain,
274
+ sourceTokenAddress: transfer.sourceToken.address,
275
+ transactionHash,
276
+ validAfter: transfer.quote.sampledAt,
277
+ })
278
+ if (evidence.type === 'invalid') return evidence
279
+ if (evidence.type !== 'verified') return { type: 'pending' }
280
+ const destinationBlockNumber =
281
+ typeof providerState?.['destinationBlockNumber'] === 'string' &&
282
+ /^\d+$/.test(providerState['destinationBlockNumber'])
283
+ ? providerState['destinationBlockNumber']
284
+ : await blockNumberBefore({
285
+ chain: destinationChain,
286
+ fetch,
287
+ signal,
288
+ timestamp: transfer.quote.sampledAt,
289
+ })
290
+ return {
291
+ providerState: {
292
+ ...providerState,
293
+ destinationBlockNumber,
294
+ routeConfiguration: configuration,
295
+ stargate: {
296
+ guid: evidence.guid,
297
+ sourceBlock: {
298
+ hash: evidence.sourceBlock.hash,
299
+ number: evidence.sourceBlock.number.toString(),
300
+ },
301
+ sourceEid: evidence.sourceEid,
302
+ },
303
+ },
304
+ type: 'verified',
305
+ }
306
+ },
193
307
  })
194
308
  }
195
309
 
@@ -218,32 +332,25 @@ export function validateAction(options: validateAction.Options): readonly Action
218
332
  const [approval, execution] = action.calls
219
333
  if (
220
334
  !Address.isEqual(approval!.to as Address.Address, deployment.sourceToken) ||
221
- BigInt(approval!.value) !== 0n ||
222
- !Address.isEqual(execution!.to as Address.Address, deployment.pool)
335
+ BigInt(approval!.value) !== 0n
223
336
  )
224
337
  throw new FundingProvider.ProviderPayloadError()
225
338
 
226
339
  const [spender, approvalAmount] = AbiFunction.decodeData(approve, approval!.data as Hex.Hex)
227
- const [sendParam, messagingFee, refundAddress] = AbiFunction.decodeData(
228
- sendToken,
229
- execution!.data as Hex.Hex,
230
- )
231
- const nativeValue = BigInt(execution!.value)
232
340
  if (
233
341
  !Address.isEqual(spender, deployment.pool) ||
234
- approvalAmount !== BigInt(options.sourceAmount) ||
235
- sendParam.amountLD !== BigInt(options.sourceAmount) ||
236
- sendParam.composeMsg !== '0x' ||
237
- sendParam.dstEid !== destinationEid ||
238
- sendParam.extraOptions !== '0x' ||
239
- sendParam.minAmountLD !== BigInt(options.sourceAmount) ||
240
- sendParam.oftCmd !== '0x' ||
241
- !Hex.isEqual(sendParam.to, recipientBytes32(options.recipient)) ||
242
- messagingFee.lzTokenFee !== 0n ||
243
- messagingFee.nativeFee !== nativeValue ||
244
- !Address.isEqual(refundAddress, options.sender as Address.Address)
342
+ approvalAmount !== BigInt(options.sourceAmount)
245
343
  )
246
344
  throw new FundingProvider.ProviderPayloadError()
345
+ validateSend({
346
+ data: execution!.data as Hex.Hex,
347
+ nativeValue: BigInt(execution!.value),
348
+ pool: deployment.pool,
349
+ recipient: options.recipient,
350
+ sender: options.sender,
351
+ sourceAmount: options.sourceAmount,
352
+ to: execution!.to as Address.Address,
353
+ })
247
354
  return [{ to: execution!.to, value: execution!.value }]
248
355
  } catch (cause) {
249
356
  if (cause instanceof FundingProvider.ProviderPayloadError) throw cause
@@ -273,28 +380,331 @@ export declare namespace validateAction {
273
380
  }
274
381
  }
275
382
 
276
- function client(options: client.Options) {
277
- if (options.route.source.chain.rpcUrls.length === 0)
383
+ /** Verifies a submitted Stargate source transaction against stored transfer terms. */
384
+ export async function verifySourceTransaction(
385
+ options: verifySourceTransaction.Options,
386
+ ): Promise<verifySourceTransaction.Result> {
387
+ const deployment = resolveDeployment({
388
+ configuration: options.configuration,
389
+ destinationChainId,
390
+ destinationTokenAddress: options.destinationTokenAddress,
391
+ sourceChainId: options.sourceChain.id,
392
+ sourceTokenAddress: options.sourceTokenAddress,
393
+ })
394
+ if (
395
+ !deployment ||
396
+ options.sourceChain.kind !== 'evm' ||
397
+ !Hash.validate(options.transactionHash) ||
398
+ !Address.validate(options.recipient, { strict: false }) ||
399
+ !Address.validate(options.sender, { strict: false })
400
+ )
401
+ return { type: 'invalid' }
402
+
403
+ const rpc = rpcClient({
404
+ fetch: options.fetch ?? defaultFetch,
405
+ signal: options.signal,
406
+ urls: options.sourceChain.rpcUrls,
407
+ })
408
+ const hash = options.transactionHash as Hex.Hex
409
+ const [chainId, receipt] = await Promise.all([
410
+ rpc.getChainId(),
411
+ rpc.getTransactionReceipt({ hash }).catch((cause) => {
412
+ if (cause instanceof TransactionReceiptNotFoundError) return undefined
413
+ throw cause
414
+ }),
415
+ ])
416
+ if (chainId !== Number(Chain.eip155Id(options.sourceChain))) return { type: 'invalid' }
417
+ const block =
418
+ !receipt || receipt.blockNumber === null
419
+ ? undefined
420
+ : await rpc
421
+ .getBlock({ blockNumber: receipt.blockNumber, includeTransactions: false })
422
+ .catch((cause) => {
423
+ if (cause instanceof BlockNotFoundError) return undefined
424
+ throw cause
425
+ })
426
+ if (
427
+ !receipt ||
428
+ receipt.blockNumber === null ||
429
+ !block ||
430
+ !Hex.isEqual(block.hash, receipt.blockHash)
431
+ ) {
432
+ if (!options.sourceBlock) return { type: 'pending' }
433
+ const finalized = await rpc.getBlock({ blockTag: 'finalized', includeTransactions: false })
434
+ if (finalized.number < options.sourceBlock.number) return { type: 'pending' }
435
+ const canonical = await rpc
436
+ .getBlock({ blockNumber: options.sourceBlock.number, includeTransactions: false })
437
+ .catch((cause) => {
438
+ if (cause instanceof BlockNotFoundError) return undefined
439
+ throw cause
440
+ })
441
+ if (!canonical || Hex.isEqual(canonical.hash, options.sourceBlock.hash))
442
+ return { type: 'pending' }
443
+ return { type: 'reorged' }
444
+ }
445
+ if (receipt.status !== 'success') return { type: 'invalid' }
446
+ const validAfter = Math.floor(Date.parse(options.validAfter) / 1_000)
447
+ if (!Number.isSafeInteger(validAfter) || block.timestamp < BigInt(validAfter))
448
+ return { type: 'invalid' }
449
+
450
+ const amount = BigInt(options.sourceAmount)
451
+ const sourceEvents = parseEventLogs({ abi: [oftSent], logs: receipt.logs, strict: true }).filter(
452
+ ({ address, args }) =>
453
+ Address.isEqual(address, deployment.pool) &&
454
+ args.dstEid === destinationEid &&
455
+ Address.isEqual(args.fromAddress, options.sender as Address.Address) &&
456
+ args.amountSentLD === amount &&
457
+ args.amountReceivedLD === amount,
458
+ )
459
+ const transfers = parseEventLogs({
460
+ abi: [transferEvent],
461
+ logs: receipt.logs,
462
+ strict: true,
463
+ }).filter(
464
+ ({ address, args }) =>
465
+ Address.isEqual(address, deployment.sourceToken) &&
466
+ Address.isEqual(args.from, options.sender as Address.Address) &&
467
+ Address.isEqual(args.to, deployment.pool) &&
468
+ args.value === amount,
469
+ )
470
+ if (sourceEvents.length !== 1 || transfers.length !== 1) return { type: 'invalid' }
471
+ const guid = sourceEvents[0]!.args.guid
472
+ // PacketV1's 113-byte header is followed by Stargate's type, asset id, recipient, and amount.
473
+ const packets = parseEventLogs({ abi: [packetSent], logs: receipt.logs, strict: true }).filter(
474
+ ({ address, args }) => {
475
+ const packet = args.encodedPayload
476
+ return (
477
+ Address.isEqual(address, layerZeroEndpoint) &&
478
+ Hex.size(packet) === 156 &&
479
+ Hex.toNumber(Hex.slice(packet, 0, 1)) === 1 &&
480
+ Hex.toNumber(Hex.slice(packet, 9, 13)) === deployment.sourceEid &&
481
+ Hex.toNumber(Hex.slice(packet, 45, 49)) === destinationEid &&
482
+ Hex.isEqual(Hex.slice(packet, 81, 113), guid) &&
483
+ Hex.toNumber(Hex.slice(packet, 113, 114)) === 1 &&
484
+ Hex.isEqual(Hex.slice(packet, 116, 148), recipientBytes32(options.recipient)) &&
485
+ Hex.toBigInt(Hex.slice(packet, 148, 156)) === amount
486
+ )
487
+ },
488
+ )
489
+ if (packets.length !== 1) return { type: 'invalid' }
490
+ return {
491
+ guid,
492
+ sourceBlock: { hash: block.hash, number: block.number },
493
+ sourceEid: deployment.sourceEid,
494
+ type: 'verified',
495
+ }
496
+ }
497
+
498
+ export declare namespace verifySourceTransaction {
499
+ /** Canonical source block that originally contained the transaction. */
500
+ type Block = {
501
+ /** Source block hash. */
502
+ hash: Hex.Hex
503
+ /** Source block number. */
504
+ number: bigint
505
+ }
506
+
507
+ /** Stored terms and chain access needed to verify one source transaction. */
508
+ type Options = {
509
+ /** Stargate configuration for the selected route. */
510
+ configuration: unknown
511
+ /** Curated Tempo destination token. */
512
+ destinationTokenAddress: string
513
+ /** Fetch implementation override for chain RPC requests. */
514
+ fetch?: FundingProvider.Fetch | undefined
515
+ /** Tempo beneficiary encoded in the Stargate send. */
516
+ recipient: string
517
+ /** Source account and LayerZero refund recipient. */
518
+ sender: string
519
+ /** Optional request cancellation signal. */
520
+ signal?: AbortSignal | undefined
521
+ /** Exact source amount in base units. */
522
+ sourceAmount: string
523
+ /** Canonical block captured during initial verification. */
524
+ sourceBlock?: Block | undefined
525
+ /** Source chain and RPC endpoints. */
526
+ sourceChain: Chain.Chain
527
+ /** Curated source token. */
528
+ sourceTokenAddress: string
529
+ /** Submitted EVM transaction hash. */
530
+ transactionHash: string
531
+ /** Earliest accepted source block timestamp. */
532
+ validAfter: string
533
+ }
534
+
535
+ /** Source transaction verification outcome. */
536
+ type Result =
537
+ | { type: 'invalid' }
538
+ | { type: 'pending' }
539
+ | { type: 'reorged' }
540
+ | { guid: Hex.Hex; sourceBlock: Block; sourceEid: number; type: 'verified' }
541
+ }
542
+
543
+ /** Finds and verifies the finalized Stargate delivery directly on Tempo. */
544
+ export async function verifyDestinationDelivery(
545
+ options: verifyDestinationDelivery.Options,
546
+ ): Promise<verifyDestinationDelivery.Result> {
547
+ const deployment = resolveDeployment({
548
+ configuration: options.configuration,
549
+ destinationChainId: options.destinationChain.id,
550
+ destinationTokenAddress: options.destinationTokenAddress,
551
+ sourceChainId: options.sourceChainId,
552
+ sourceTokenAddress: options.sourceTokenAddress,
553
+ })
554
+ if (
555
+ !deployment ||
556
+ options.destinationChain.kind !== 'evm' ||
557
+ !/^\d+$/.test(options.destinationBlockNumber) ||
558
+ !Hash.validate(options.guid) ||
559
+ !Address.validate(options.recipient, { strict: false })
560
+ )
561
+ return { type: 'invalid' }
562
+
563
+ const rpc = rpcClient({
564
+ fetch: options.fetch ?? defaultFetch,
565
+ urls: options.destinationChain.rpcUrls,
566
+ })
567
+ const [chainId, finalized] = await Promise.all([
568
+ rpc.getChainId(),
569
+ rpc.getBlock({ blockTag: 'finalized', includeTransactions: false }),
570
+ ])
571
+ if (chainId !== Number(Chain.eip155Id(options.destinationChain))) return { type: 'invalid' }
572
+ const amount = BigInt(options.destinationAmount)
573
+ const discovered = await (async () => {
574
+ for (
575
+ let fromBlock = BigInt(options.destinationBlockNumber);
576
+ fromBlock <= finalized.number;
577
+ fromBlock += maximumLogBlockRange
578
+ ) {
579
+ const toBlock = fromBlock + maximumLogBlockRange - 1n
580
+ const logs = await rpc.getLogs({
581
+ address: deployment.destinationPool,
582
+ args: { guid: options.guid, toAddress: options.recipient as Address.Address },
583
+ event: oftReceived,
584
+ fromBlock,
585
+ strict: true,
586
+ toBlock: toBlock > finalized.number ? finalized.number : toBlock,
587
+ })
588
+ const matches = logs.filter(
589
+ ({ args, transactionHash }) =>
590
+ transactionHash &&
591
+ args.srcEid === deployment.sourceEid &&
592
+ args.amountReceivedLD === amount,
593
+ )
594
+ if (logs.length !== matches.length || matches.length > 1) return { type: 'invalid' as const }
595
+ if (matches[0]?.transactionHash)
596
+ return { transactionHash: matches[0].transactionHash, type: 'found' as const }
597
+ }
598
+ return { type: 'pending' as const }
599
+ })()
600
+ if (discovered.type !== 'found') return discovered
601
+ const { transactionHash } = discovered
602
+ const receipt = await rpc.getTransactionReceipt({ hash: transactionHash }).catch((cause) => {
603
+ if (cause instanceof TransactionReceiptNotFoundError) return undefined
604
+ throw cause
605
+ })
606
+ if (!receipt || receipt.blockNumber === null) return { type: 'pending' }
607
+ if (receipt.status !== 'success' || receipt.blockNumber > finalized.number)
608
+ return { type: 'invalid' }
609
+
610
+ const transfers = parseEventLogs({
611
+ abi: [transferEvent],
612
+ logs: receipt.logs,
613
+ strict: true,
614
+ }).filter(
615
+ ({ address, args }) =>
616
+ Address.isEqual(address, destinationToken as Address.Address) &&
617
+ Address.isEqual(args.from, zeroAddress) &&
618
+ Address.isEqual(args.to, options.recipient as Address.Address) &&
619
+ args.value === amount,
620
+ )
621
+ if (transfers.length !== 1) return { type: 'invalid' }
622
+ return { transactionHash, type: 'verified' }
623
+ }
624
+
625
+ export declare namespace verifyDestinationDelivery {
626
+ /** Stored terms and chain access needed to discover and verify one delivery. */
627
+ type Options = {
628
+ /** Stargate configuration for the selected route. */
629
+ configuration: unknown
630
+ /** Exact destination amount in base units. */
631
+ destinationAmount: string
632
+ /** Tempo block captured before the source transaction was broadcast. */
633
+ destinationBlockNumber: string
634
+ /** Tempo chain and RPC endpoints. */
635
+ destinationChain: Chain.Chain
636
+ /** Curated Tempo destination token. */
637
+ destinationTokenAddress: string
638
+ /** Fetch implementation override for Tempo RPC requests. */
639
+ fetch?: FundingProvider.Fetch | undefined
640
+ /** GUID emitted by the verified source Stargate pool. */
641
+ guid: Hex.Hex
642
+ /** Tempo beneficiary. */
643
+ recipient: string
644
+ /** Source chain CAIP-2 id. */
645
+ sourceChainId: string
646
+ /** Curated source token. */
647
+ sourceTokenAddress: string
648
+ }
649
+
650
+ /** Destination delivery verification outcome. */
651
+ type Result =
652
+ | { type: 'invalid' }
653
+ | { type: 'pending' }
654
+ | { transactionHash: Hex.Hex; type: 'verified' }
655
+ }
656
+
657
+ function rpcClient(options: rpcClient.Options) {
658
+ if (options.urls.length === 0) throw new FundingProvider.ProviderConfigurationError()
659
+ return Viem.createEvmClient(options).extend(publicActions)
660
+ }
661
+
662
+ declare namespace rpcClient {
663
+ type Options = {
664
+ fetch: FundingProvider.Fetch
665
+ signal?: AbortSignal | undefined
666
+ urls: readonly string[]
667
+ }
668
+ }
669
+
670
+ async function blockNumberBefore(options: blockNumberBefore.Options) {
671
+ if (options.chain.kind !== 'evm') throw new FundingProvider.ProviderConfigurationError()
672
+ const timestamp = Date.parse(options.timestamp)
673
+ if (!Number.isFinite(timestamp)) throw new FundingProvider.ProviderPayloadError()
674
+ const rpc = rpcClient({
675
+ fetch: options.fetch,
676
+ signal: options.signal,
677
+ urls: options.chain.rpcUrls,
678
+ })
679
+ const [chainId, finalized] = await Promise.all([
680
+ rpc.getChainId(),
681
+ rpc.getBlock({ blockTag: 'finalized', includeTransactions: false }),
682
+ ])
683
+ if (chainId !== Number(Chain.eip155Id(options.chain)))
278
684
  throw new FundingProvider.ProviderConfigurationError()
279
- return createClient({
280
- transport: fallback(
281
- options.route.source.chain.rpcUrls.map((url) =>
282
- custom(
283
- RpcTransport.fromHttp(url, {
284
- fetchFn: (input, init) => options.fetch(input, { ...init, signal: options.signal }),
285
- }),
286
- { retryCount: 0 },
287
- ),
288
- ),
289
- ),
290
- }).extend(publicActions)
685
+
686
+ const target = BigInt(Math.floor(timestamp / 1_000) - 1)
687
+ let lower = 0n
688
+ let upper = finalized.number
689
+ while (lower < upper) {
690
+ const middle = (lower + upper + 1n) / 2n
691
+ const block = await rpc.getBlock({ blockNumber: middle, includeTransactions: false })
692
+ if (block.timestamp <= target) lower = middle
693
+ else upper = middle - 1n
694
+ }
695
+ return lower.toString()
291
696
  }
292
697
 
293
- declare namespace client {
698
+ declare namespace blockNumberBefore {
294
699
  type Options = {
700
+ /** Destination chain whose block history is searched. */
701
+ chain: Chain.Chain
702
+ /** Fetch implementation used for RPC requests. */
295
703
  fetch: FundingProvider.Fetch
296
- route: FundingProvider.QuoteCandidate['route']
297
- signal: AbortSignal
704
+ /** Optional request cancellation signal. */
705
+ signal?: AbortSignal | undefined
706
+ /** Exclusive ISO 8601 timestamp upper bound. */
707
+ timestamp: string
298
708
  }
299
709
  }
300
710
 
@@ -365,10 +775,45 @@ function recipientBytes32(recipient: string) {
365
775
  return Hex.padLeft(recipient as Hex.Hex, 32)
366
776
  }
367
777
 
778
+ function validateSend(options: validateSend.Options) {
779
+ const [sendParam, messagingFee, refundAddress] = AbiFunction.decodeData(sendToken, options.data)
780
+ const amount = BigInt(options.sourceAmount)
781
+ if (
782
+ !Address.isEqual(options.to, options.pool) ||
783
+ sendParam.amountLD !== amount ||
784
+ sendParam.composeMsg !== '0x' ||
785
+ sendParam.dstEid !== destinationEid ||
786
+ sendParam.extraOptions !== '0x' ||
787
+ sendParam.minAmountLD !== amount ||
788
+ sendParam.oftCmd !== '0x' ||
789
+ !Hex.isEqual(sendParam.to, recipientBytes32(options.recipient)) ||
790
+ messagingFee.lzTokenFee !== 0n ||
791
+ messagingFee.nativeFee !== options.nativeValue ||
792
+ !Address.isEqual(refundAddress, options.sender as Address.Address)
793
+ )
794
+ throw new FundingProvider.ProviderPayloadError()
795
+ }
796
+
797
+ declare namespace validateSend {
798
+ type Options = {
799
+ data: Hex.Hex
800
+ nativeValue: bigint
801
+ pool: Address.Address
802
+ recipient: string
803
+ sender: string
804
+ sourceAmount: string
805
+ to: Address.Address
806
+ }
807
+ }
808
+
368
809
  function resolveDeployment(options: resolveDeployment.Options): Deployment | undefined {
369
- const parsed = configuration.safeParse(options.configuration)
810
+ const parsed = schema.Configuration.safeParse(options.configuration)
811
+ const sourceEid =
812
+ parsed.success &&
813
+ (parsed.data.sourceEid ?? sourceEids[options.sourceChainId as keyof typeof sourceEids])
370
814
  if (
371
815
  !parsed.success ||
816
+ !sourceEid ||
372
817
  options.destinationChainId !== destinationChainId ||
373
818
  !options.sourceChainId.startsWith('eip155:') ||
374
819
  !Address.validate(options.destinationTokenAddress, { strict: false }) ||
@@ -377,14 +822,16 @@ function resolveDeployment(options: resolveDeployment.Options): Deployment | und
377
822
  )
378
823
  return undefined
379
824
  return {
825
+ destinationPool: Address.checksum(parsed.data.destinationPoolAddress ?? destinationPool),
380
826
  pool: Address.checksum(parsed.data.poolAddress),
827
+ sourceEid,
381
828
  sourceToken: Address.checksum(options.sourceTokenAddress),
382
829
  }
383
830
  }
384
831
 
385
832
  declare namespace resolveDeployment {
386
833
  type Options = {
387
- configuration?: Catalog.Configuration | undefined
834
+ configuration: unknown
388
835
  destinationChainId: string
389
836
  destinationTokenAddress: string
390
837
  sourceChainId: string
@@ -42,6 +42,7 @@ type Block =
42
42
  const friendlyName: Record<Webhooks.Envelope['type'], string> = {
43
43
  'block:created': ':link: Block created',
44
44
  'funding:deposit.updated': ':inbox_tray: Funding deposit updated',
45
+ 'funding:transfer.updated': ':arrows_counterclockwise: Funding transfer updated',
45
46
  'log:emitted': ':label: Log emitted',
46
47
  ping: ':bell: Webhook test ping',
47
48
  'token:transfer': ':money_with_wings: Token transfer',
@@ -152,6 +153,7 @@ function renderEvent(
152
153
  if (type === 'log:emitted') return logLayout(data, base)
153
154
  if (type === 'block:created') return blockLayout(data, base)
154
155
  if (type === 'funding:deposit.updated') return fundingDepositLayout(data)
156
+ if (type === 'funding:transfer.updated') return fundingTransferLayout(data)
155
157
  return pingLayout()
156
158
  }
157
159
 
@@ -168,6 +170,19 @@ function fundingDepositLayout(data: Record<string, unknown>): Layout {
168
170
  }
169
171
  }
170
172
 
173
+ function fundingTransferLayout(data: Record<string, unknown>): Layout {
174
+ const fields: Field[] = []
175
+ field(fields, 'Transfer', escape(str(data['id']) ?? 'unknown'))
176
+ field(fields, 'Status', escape(str(data['status']) ?? 'unknown'))
177
+ const recipient = str(data['recipient'])
178
+ if (recipient !== undefined) field(fields, 'Recipient', escape(recipient))
179
+ return {
180
+ fields,
181
+ header: friendlyName['funding:transfer.updated'],
182
+ text: `Funding transfer updated · ${str(data['status']) ?? 'unknown'}`,
183
+ }
184
+ }
185
+
171
186
  function transferLayout(data: Record<string, unknown>, base: string | undefined): Layout {
172
187
  const amount = str(data['amount'])
173
188
  const token =