@gibs/bridge-sdk 1.13.1 → 1.17.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 (217) hide show
  1. package/README.md +244 -18
  2. package/abis/package.json +6 -0
  3. package/bridge-limits/package.json +6 -0
  4. package/chain-info/package.json +6 -0
  5. package/chain-names/package.json +6 -0
  6. package/chains/package.json +6 -0
  7. package/claims/package.json +6 -0
  8. package/config/package.json +6 -0
  9. package/consolidated-quote/package.json +6 -0
  10. package/destination-calldata/package.json +6 -0
  11. package/dist/chain-info.d.ts +1 -1
  12. package/dist/chain-info.js +11 -12
  13. package/dist/chains.d.ts +25 -0
  14. package/dist/chains.js +55 -20
  15. package/dist/cjs/abis.d.ts +1 -0
  16. package/dist/cjs/abis.js +19 -0
  17. package/dist/cjs/bridge-limits.d.ts +103 -0
  18. package/dist/cjs/bridge-limits.js +89 -0
  19. package/dist/cjs/chain-info.d.ts +169 -0
  20. package/dist/cjs/chain-info.js +452 -0
  21. package/dist/cjs/chain-names.d.ts +160 -0
  22. package/dist/cjs/chain-names.js +207 -0
  23. package/dist/cjs/chains.d.ts +90 -0
  24. package/dist/cjs/chains.js +357 -0
  25. package/dist/cjs/claims.d.ts +198 -0
  26. package/dist/cjs/claims.js +182 -0
  27. package/dist/cjs/config.d.ts +240 -0
  28. package/dist/cjs/config.js +537 -0
  29. package/dist/cjs/consolidated-quote.d.ts +648 -0
  30. package/dist/cjs/consolidated-quote.js +1048 -0
  31. package/dist/cjs/destination-calldata.d.ts +118 -0
  32. package/dist/cjs/destination-calldata.js +156 -0
  33. package/dist/cjs/ecosystems.d.ts +217 -0
  34. package/dist/cjs/ecosystems.js +298 -0
  35. package/dist/cjs/execution.d.ts +346 -0
  36. package/dist/cjs/execution.js +468 -0
  37. package/dist/cjs/fee-type.d.ts +10 -0
  38. package/dist/cjs/fee-type.js +10 -0
  39. package/dist/cjs/hyperliquid-execution.d.ts +152 -0
  40. package/dist/cjs/hyperliquid-execution.js +175 -0
  41. package/dist/cjs/image-links.d.ts +117 -0
  42. package/dist/cjs/image-links.js +153 -0
  43. package/dist/cjs/index.d.ts +23 -0
  44. package/dist/cjs/index.js +39 -0
  45. package/dist/cjs/lifi-execution-guard.d.ts +210 -0
  46. package/dist/cjs/lifi-execution-guard.js +409 -0
  47. package/dist/cjs/near-intents-conformance.d.ts +85 -0
  48. package/dist/cjs/near-intents-conformance.js +337 -0
  49. package/dist/cjs/near-intents-route.d.ts +47 -0
  50. package/dist/cjs/near-intents-route.js +303 -0
  51. package/dist/cjs/near-intents.d.ts +524 -0
  52. package/dist/cjs/near-intents.js +1071 -0
  53. package/dist/cjs/origin-deposit.d.ts +326 -0
  54. package/dist/cjs/origin-deposit.js +82 -0
  55. package/dist/cjs/package.json +3 -0
  56. package/dist/cjs/pre-quote.d.ts +198 -0
  57. package/dist/cjs/pre-quote.js +180 -0
  58. package/dist/cjs/prices.d.ts +107 -0
  59. package/dist/cjs/prices.js +172 -0
  60. package/dist/cjs/providers.d.ts +238 -0
  61. package/dist/cjs/providers.js +171 -0
  62. package/dist/cjs/quote-fees.d.ts +102 -0
  63. package/dist/cjs/quote-fees.js +201 -0
  64. package/dist/cjs/referral.d.ts +85 -0
  65. package/dist/cjs/referral.js +85 -0
  66. package/dist/cjs/relay-currencies.d.ts +47 -0
  67. package/dist/cjs/relay-currencies.js +95 -0
  68. package/dist/cjs/relay-depository.d.ts +2 -0
  69. package/dist/cjs/relay-depository.js +85 -0
  70. package/dist/cjs/relay-quote.d.ts +301 -0
  71. package/dist/cjs/relay-quote.js +996 -0
  72. package/dist/cjs/route-steps.d.ts +138 -0
  73. package/dist/cjs/route-steps.js +310 -0
  74. package/dist/cjs/routing/amounts.d.ts +80 -0
  75. package/dist/cjs/routing/amounts.js +126 -0
  76. package/dist/cjs/routing/candidate-route.d.ts +43 -0
  77. package/dist/cjs/routing/candidate-route.js +50 -0
  78. package/dist/cjs/routing/candidate.d.ts +237 -0
  79. package/dist/cjs/routing/candidate.js +100 -0
  80. package/dist/cjs/routing/carriers/ask-answer.d.ts +77 -0
  81. package/dist/cjs/routing/carriers/ask-answer.js +2 -0
  82. package/dist/cjs/routing/carriers/omnibridge.d.ts +137 -0
  83. package/dist/cjs/routing/carriers/omnibridge.js +459 -0
  84. package/dist/cjs/routing/carriers/pulsex-paths.d.ts +80 -0
  85. package/dist/cjs/routing/carriers/pulsex-paths.js +97 -0
  86. package/dist/cjs/routing/carriers/pulsex-split.d.ts +175 -0
  87. package/dist/cjs/routing/carriers/pulsex-split.js +193 -0
  88. package/dist/cjs/routing/carriers/pulsex.d.ts +159 -0
  89. package/dist/cjs/routing/carriers/pulsex.js +565 -0
  90. package/dist/cjs/routing/carriers/wrap.d.ts +25 -0
  91. package/dist/cjs/routing/carriers/wrap.js +119 -0
  92. package/dist/cjs/routing/composed-route-quality.d.ts +125 -0
  93. package/dist/cjs/routing/composed-route-quality.js +144 -0
  94. package/dist/cjs/routing/entry-chain-anchor.d.ts +88 -0
  95. package/dist/cjs/routing/entry-chain-anchor.js +59 -0
  96. package/dist/cjs/routing/format.d.ts +31 -0
  97. package/dist/cjs/routing/format.js +64 -0
  98. package/dist/cjs/routing/index.d.ts +38 -0
  99. package/dist/cjs/routing/index.js +134 -0
  100. package/dist/cjs/routing/producers.d.ts +770 -0
  101. package/dist/cjs/routing/producers.js +1617 -0
  102. package/dist/cjs/routing/providers.d.ts +31 -0
  103. package/dist/cjs/routing/providers.js +48 -0
  104. package/dist/cjs/routing/route-data.d.ts +355 -0
  105. package/dist/cjs/routing/route-data.js +2 -0
  106. package/dist/cjs/routing/search/compose.d.ts +168 -0
  107. package/dist/cjs/routing/search/compose.js +104 -0
  108. package/dist/cjs/routing/search/graph.d.ts +668 -0
  109. package/dist/cjs/routing/search/graph.js +461 -0
  110. package/dist/cjs/routing/search/plan-route-data.d.ts +269 -0
  111. package/dist/cjs/routing/search/plan-route-data.js +848 -0
  112. package/dist/cjs/routing/search/score.d.ts +356 -0
  113. package/dist/cjs/routing/search/score.js +445 -0
  114. package/dist/cjs/routing/swap-transactions.d.ts +91 -0
  115. package/dist/cjs/routing/swap-transactions.js +131 -0
  116. package/dist/cjs/routing/usd-value.d.ts +17 -0
  117. package/dist/cjs/routing/usd-value.js +29 -0
  118. package/dist/cjs/settings.d.ts +785 -0
  119. package/dist/cjs/settings.js +1074 -0
  120. package/dist/cjs/ton-connect.d.ts +98 -0
  121. package/dist/cjs/ton-connect.js +206 -0
  122. package/dist/cjs/ton-execution.d.ts +7 -0
  123. package/dist/cjs/ton-execution.js +23 -0
  124. package/dist/cjs/ton.d.ts +37 -0
  125. package/dist/cjs/ton.js +83 -0
  126. package/dist/cjs/tron-execution.d.ts +146 -0
  127. package/dist/cjs/tron-execution.js +248 -0
  128. package/dist/cjs/types.d.ts +51 -0
  129. package/dist/cjs/types.js +2 -0
  130. package/dist/claims.d.ts +198 -0
  131. package/dist/claims.js +140 -0
  132. package/dist/config.d.ts +24 -0
  133. package/dist/config.js +39 -5
  134. package/dist/consolidated-quote.d.ts +109 -15
  135. package/dist/consolidated-quote.js +310 -52
  136. package/dist/execution.d.ts +346 -0
  137. package/dist/execution.js +428 -0
  138. package/dist/hyperliquid-execution.js +2 -3
  139. package/dist/index.d.ts +3 -0
  140. package/dist/index.js +3 -0
  141. package/dist/lifi-execution-guard.d.ts +13 -2
  142. package/dist/lifi-execution-guard.js +4 -0
  143. package/dist/near-intents.js +2 -3
  144. package/dist/pre-quote.d.ts +6 -6
  145. package/dist/pre-quote.js +10 -14
  146. package/dist/providers.d.ts +17 -0
  147. package/dist/providers.js +6 -0
  148. package/dist/referral.d.ts +12 -4
  149. package/dist/referral.js +12 -4
  150. package/dist/relay-depository.d.ts +2 -0
  151. package/dist/relay-depository.js +82 -0
  152. package/dist/relay-quote.js +2 -3
  153. package/dist/routing/candidate.d.ts +3 -3
  154. package/dist/routing/candidate.js +3 -3
  155. package/dist/routing/carriers/ask-answer.d.ts +17 -0
  156. package/dist/routing/carriers/omnibridge.js +23 -21
  157. package/dist/routing/carriers/pulsex-paths.d.ts +80 -0
  158. package/dist/routing/carriers/pulsex-paths.js +92 -0
  159. package/dist/routing/carriers/pulsex-split.d.ts +175 -0
  160. package/dist/routing/carriers/pulsex-split.js +185 -0
  161. package/dist/routing/carriers/pulsex.d.ts +31 -21
  162. package/dist/routing/carriers/pulsex.js +141 -71
  163. package/dist/routing/composed-route-quality.d.ts +28 -6
  164. package/dist/routing/composed-route-quality.js +36 -5
  165. package/dist/routing/entry-chain-anchor.d.ts +1 -1
  166. package/dist/routing/entry-chain-anchor.js +1 -1
  167. package/dist/routing/index.d.ts +6 -2
  168. package/dist/routing/index.js +6 -2
  169. package/dist/routing/producers.js +1 -1
  170. package/dist/routing/providers.d.ts +5 -0
  171. package/dist/routing/providers.js +5 -0
  172. package/dist/routing/search/graph.d.ts +15 -0
  173. package/dist/routing/search/plan-route-data.d.ts +25 -0
  174. package/dist/routing/search/plan-route-data.js +43 -7
  175. package/dist/routing/search/score.d.ts +3 -0
  176. package/dist/routing/search/score.js +11 -3
  177. package/dist/routing/swap-transactions.d.ts +91 -0
  178. package/dist/routing/swap-transactions.js +125 -0
  179. package/dist/settings.d.ts +94 -1
  180. package/dist/settings.js +95 -3
  181. package/dist/ton-connect.d.ts +98 -0
  182. package/dist/ton-connect.js +200 -0
  183. package/dist/ton-execution.d.ts +4 -114
  184. package/dist/ton-execution.js +4 -244
  185. package/dist/ton.d.ts +37 -0
  186. package/dist/ton.js +65 -0
  187. package/docs/examples/example-a-ether-to-pulse.ts +300 -0
  188. package/docs/examples/example-b-token-pulsechain-to-ethereum.ts +270 -0
  189. package/docs/examples/shared.ts +261 -0
  190. package/docs/examples/tsconfig.json +18 -0
  191. package/docs/integration-guide.md +617 -0
  192. package/ecosystems/package.json +6 -0
  193. package/execution/package.json +6 -0
  194. package/fee-type/package.json +6 -0
  195. package/hyperliquid-execution/package.json +6 -0
  196. package/image-links/package.json +6 -0
  197. package/lifi-execution-guard/package.json +6 -0
  198. package/llms.txt +75 -10
  199. package/near-intents/package.json +6 -0
  200. package/near-intents-route/package.json +6 -0
  201. package/origin-deposit/package.json +6 -0
  202. package/package.json +817 -10
  203. package/pre-quote/package.json +6 -0
  204. package/prices/package.json +6 -0
  205. package/providers/package.json +6 -0
  206. package/quote-fees/package.json +6 -0
  207. package/referral/package.json +6 -0
  208. package/relay-currencies/package.json +6 -0
  209. package/relay-quote/package.json +6 -0
  210. package/route-steps/package.json +6 -0
  211. package/routing/package.json +6 -0
  212. package/settings/package.json +6 -0
  213. package/ton/package.json +6 -0
  214. package/ton-connect/package.json +6 -0
  215. package/ton-execution/package.json +6 -0
  216. package/tron-execution/package.json +6 -0
  217. package/types/package.json +6 -0
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Example B: an ERC-20 token from PulseChain to Ethereum, with delivery off.
3
+ *
4
+ * 1. Read how the token maps across the bridge, and search for the route.
5
+ * 2. Plan the direct crossing with `shouldDeliver: false`: an exact approval,
6
+ * then `relayTokens`.
7
+ * 3. Poll `fetchTransferStatus` until the crossing is `claimable`.
8
+ * 4. Read the bridge request with `fetchClaimRecords` and build the claim
9
+ * with `buildClaimTransaction`, handling every typed refusal.
10
+ * 5. Send the claim on Ethereum, then poll until the transfer is delivered.
11
+ *
12
+ * Typechecked only (`docs/examples/tsconfig.json` in `@gibs/bridge-sdk`). Never run against mainnet.
13
+ */
14
+ import { type ClaimRecord, fetchClaimRecords, type TransferStatus } from '@gibs/bridge-indexer';
15
+ import { tokenBridgeInfo } from '@gibs/bridge-sdk/chain-info';
16
+ import { buildClaimTransaction, type ClaimRefusal } from '@gibs/bridge-sdk/claims';
17
+ import { Chains, Providers } from '@gibs/bridge-sdk/config';
18
+ import { planCandidateExecution } from '@gibs/bridge-sdk/execution';
19
+ import type { BridgeKey, Token } from '@gibs/bridge-sdk/types';
20
+ import { searchBridgeRoutes } from '@gibs/quotes/search';
21
+ import type { Hex } from 'viem';
22
+ import {
23
+ allowanceReaderWith,
24
+ createClientLookup,
25
+ type RpcUrlByChainId,
26
+ requireClientWith,
27
+ sendPlannedTransactions,
28
+ sleep,
29
+ type WalletSigner,
30
+ waitForTransferStatus,
31
+ } from './shared.js';
32
+
33
+ const pulsechainChainId = 369;
34
+
35
+ /** PulseX (PLSX), an ERC-20 token native to PulseChain. */
36
+ const plsx: Token = {
37
+ chainId: pulsechainChainId,
38
+ address: '0x95B303987A60C71504D99Aa1b13B4DA07b0790ab',
39
+ decimals: 18,
40
+ symbol: 'PLSX',
41
+ name: 'PulseX',
42
+ logoURI: null,
43
+ };
44
+
45
+ const pulsechainToEthereum: BridgeKey = [Providers.PULSECHAIN, Chains.PLS, Chains.ETH];
46
+
47
+ /** What the example needs from the wallet. */
48
+ export type TokenToEthereumOptions = {
49
+ readonly signer: WalletSigner;
50
+ readonly rpcUrlByChainId: RpcUrlByChainId;
51
+ /** The amount to send, in the token's base units. */
52
+ readonly amount: bigint;
53
+ /** Cancels the search and every poll. */
54
+ readonly signal: AbortSignal;
55
+ };
56
+
57
+ /** A claim the wallet must not send, and must not retry by waiting. */
58
+ export class ClaimRefusedError extends Error {
59
+ constructor(readonly refusal: ClaimRefusal) {
60
+ super(`the claim was refused: ${refusal.reason}`);
61
+ this.name = 'ClaimRefusedError';
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Reads the one bridge request a crossing transaction started.
67
+ * `fetchClaimRecords` asks the indexer for exactly the fields a claim reads,
68
+ * lowercases the hash (the indexer matches hex in lowercase only), and never
69
+ * reads a relation the indexer leaves null.
70
+ *
71
+ * @param options.txHash - the crossing transaction on PulseChain
72
+ * @param options.signal - cancels the request
73
+ * @returns the record, in the shape `buildClaimTransaction` takes
74
+ * @throws on a network or GraphQL failure, or when the transaction started other than one request
75
+ */
76
+ const readClaimRecord = async (options: {
77
+ readonly txHash: Hex;
78
+ readonly signal: AbortSignal;
79
+ }): Promise<ClaimRecord> => {
80
+ const records = await fetchClaimRecords({
81
+ source: { chainId: pulsechainChainId, txHash: options.txHash },
82
+ signal: options.signal,
83
+ });
84
+ const [record] = records;
85
+ if (record === undefined || records.length !== 1) {
86
+ throw new Error(`expected one bridge request for ${options.txHash}, found ${records.length}`);
87
+ }
88
+ return record;
89
+ };
90
+
91
+ /** What to do with a refused claim. */
92
+ type RefusalAction =
93
+ | { readonly kind: 'done' }
94
+ | { readonly kind: 'wait' }
95
+ | { readonly kind: 'stop'; readonly error: Error };
96
+
97
+ /**
98
+ * Decides what each typed refusal means for the wallet. The switch is
99
+ * exhaustive: a new refusal reason fails the typecheck here.
100
+ *
101
+ * @param refusal - the refusal from `buildClaimTransaction`
102
+ * @returns the action
103
+ */
104
+ const actionForRefusal = (refusal: ClaimRefusal): RefusalAction => {
105
+ switch (refusal.reason) {
106
+ case 'already-delivered':
107
+ // Somebody executed the release already. Nothing to send.
108
+ return { kind: 'done' };
109
+ case 'not-signed-yet':
110
+ // The validators are still signing. Ask again later.
111
+ return { kind: 'wait' };
112
+ case 'signatures-incomplete':
113
+ // The indexer stored fewer signatures than the bridge needs (see
114
+ // "Known limits": the submitSignature case). Waiting does not fix it.
115
+ // Tell the user, and report the transfer to Gibs.
116
+ return { kind: 'stop', error: new ClaimRefusedError(refusal) };
117
+ case 'wrong-type':
118
+ // Not a signature request. The validators execute an affirmation themselves.
119
+ return { kind: 'stop', error: new ClaimRefusedError(refusal) };
120
+ case 'malformed-request':
121
+ // A field is not the shape the indexer documents. Report it.
122
+ return { kind: 'stop', error: new ClaimRefusedError(refusal) };
123
+ default: {
124
+ const unhandled: never = refusal;
125
+ return { kind: 'stop', error: new Error(`unhandled refusal ${JSON.stringify(unhandled)}`) };
126
+ }
127
+ }
128
+ };
129
+
130
+ /**
131
+ * Sends an ERC-20 token from PulseChain to Ethereum and claims it there by hand.
132
+ *
133
+ * @param options - see {@link TokenToEthereumOptions}
134
+ * @returns the crossing hash, the claim hash (null when somebody else released it), and the final status
135
+ */
136
+ export const sendTokenToEthereum = async (
137
+ options: TokenToEthereumOptions,
138
+ ): Promise<{
139
+ readonly crossingHash: Hex;
140
+ readonly claimHash: Hex | null;
141
+ readonly finalStatus: TransferStatus;
142
+ }> => {
143
+ const { signer, signal } = options;
144
+ const clientForChain = createClientLookup(options.rpcUrlByChainId);
145
+ const requireClient = requireClientWith(clientForChain);
146
+ const pulsechainClient = requireClient(pulsechainChainId);
147
+ const ethereumClient = requireClient(Number(Chains.ETH));
148
+
149
+ // 1. How PLSX maps onto Ethereum. A null `assetOutAddress` means the token
150
+ // has not crossed before; the first transfer creates it. The search needs
151
+ // a destination address, so this example stops there.
152
+ const assetLink = await tokenBridgeInfo({
153
+ bridgeKey: pulsechainToEthereum,
154
+ assetIn: plsx,
155
+ isProd: true,
156
+ fromChainClient: pulsechainClient,
157
+ toChainClient: ethereumClient,
158
+ });
159
+ if (assetLink === null || assetLink.assetOutAddress === null) {
160
+ throw new Error('PLSX has no bridged address on Ethereum yet');
161
+ }
162
+
163
+ const search = await searchBridgeRoutes({
164
+ origin: {
165
+ chainId: pulsechainChainId,
166
+ token: plsx,
167
+ amount: options.amount,
168
+ account: signer.address,
169
+ },
170
+ destination: {
171
+ chainId: Number(Chains.ETH),
172
+ token: { address: assetLink.assetOutAddress, decimals: plsx.decimals, symbol: plsx.symbol },
173
+ recipient: signer.address,
174
+ },
175
+ clientForChain,
176
+ fetch: (input, init) => globalThis.fetch(input, init),
177
+ signal,
178
+ });
179
+
180
+ // The direct crossing: one section, one omnibridge hop.
181
+ const crossing = search.routes.find(
182
+ ({ plan }) =>
183
+ plan.signatures.length === 1 && plan.hops[0]?.edge.carrier === Providers.PULSECHAIN,
184
+ );
185
+ if (crossing === undefined)
186
+ throw new Error('the search served no direct crossing for this token');
187
+
188
+ // 2. Plan it. Delivery is off: nobody is paid to release it on Ethereum, so
189
+ // the user claims it there and pays the Ethereum gas.
190
+ const plan = await planCandidateExecution({
191
+ candidate: crossing.candidate,
192
+ account: signer.address,
193
+ readAllowance: allowanceReaderWith(requireClient),
194
+ resolveToken: search.resolveToken,
195
+ sections: [
196
+ {
197
+ amountIn: options.amount,
198
+ recipient: signer.address,
199
+ omnibridge: { isProd: true, assetLink, delivery: { shouldDeliver: false }, unwrap: false },
200
+ },
201
+ ],
202
+ });
203
+ if (plan.kind === 'refused') throw new Error(`cannot plan the crossing: ${plan.refusal.reason}`);
204
+ // plan.transactions is [approve (when the allowance is short), bridge], both on PulseChain.
205
+
206
+ const sent = await sendPlannedTransactions({
207
+ transactions: plan.transactions,
208
+ signer,
209
+ requireClient,
210
+ // Every transaction is on PulseChain, so this is never called.
211
+ waitForArrival: async () => {},
212
+ });
213
+ const bridge = sent.find(({ transaction }) => transaction.kind === 'bridge');
214
+ if (bridge === undefined) throw new Error('the plan held no bridge transaction');
215
+ const source = { chainId: pulsechainChainId, txHash: bridge.hash } as const;
216
+ const statusOptions = { signal, originClient: pulsechainClient } as const;
217
+
218
+ // 3. Wait until the validators have signed. `claimable` is true in both
219
+ // ready states. `delivered` means somebody released it already.
220
+ const ready = await waitForTransferStatus({
221
+ source,
222
+ statusOptions,
223
+ isDone: (status) => status.claimable || status.state === 'delivered',
224
+ intervalMs: 30_000,
225
+ });
226
+ if (ready.state === 'delivered')
227
+ return { crossingHash: bridge.hash, claimHash: null, finalStatus: ready };
228
+
229
+ // 4. Build the claim from the indexer's record. Retry only on `not-signed-yet`.
230
+ for (;;) {
231
+ const request = await readClaimRecord({ txHash: bridge.hash, signal });
232
+ // Delivery was off, so the claim goes straight to the destination bridge and pays no tip.
233
+ const claim = buildClaimTransaction({ request, via: { kind: 'direct' } });
234
+ if (claim.kind === 'refused') {
235
+ const action = actionForRefusal(claim.refusal);
236
+ if (action.kind === 'stop') throw action.error;
237
+ if (action.kind === 'done') {
238
+ const finalStatus = await waitForTransferStatus({
239
+ source,
240
+ statusOptions,
241
+ isDone: (status) => status.state === 'delivered',
242
+ intervalMs: 15_000,
243
+ });
244
+ return { crossingHash: bridge.hash, claimHash: null, finalStatus };
245
+ }
246
+ await sleep(30_000, signal);
247
+ continue;
248
+ }
249
+
250
+ // 5. Send the claim on Ethereum (`claim.transaction.chainId` is 1).
251
+ const [claimSent] = await sendPlannedTransactions({
252
+ transactions: [claim.transaction],
253
+ signer,
254
+ requireClient,
255
+ waitForArrival: async () => {},
256
+ });
257
+ if (claimSent === undefined) throw new Error('the claim was not sent');
258
+ const receipt = await requireClient(claim.transaction.chainId).waitForTransactionReceipt({
259
+ hash: claimSent.hash,
260
+ });
261
+ if (receipt.status !== 'success') throw new Error(`the claim ${claimSent.hash} reverted`);
262
+ const finalStatus = await waitForTransferStatus({
263
+ source,
264
+ statusOptions,
265
+ isDone: (status) => status.state === 'delivered',
266
+ intervalMs: 15_000,
267
+ });
268
+ return { crossingHash: bridge.hash, claimHash: claimSent.hash, finalStatus };
269
+ }
270
+ };
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Helpers both wallet examples share: the signer the examples expect, one viem
3
+ * client per chain from the wallet's own RPC endpoints, an allowance reader,
4
+ * a sender that honours `waitForReceipt`, and a status poller.
5
+ *
6
+ * The packages do not depend on web3.js, so the signer is a small
7
+ * interface. A web3.js v4 adapter for it is shown in a comment below.
8
+ *
9
+ * These files are typechecked only. They are never run against a live chain.
10
+ */
11
+ import {
12
+ type FetchTransferStatusOptions,
13
+ fetchTransferStatus,
14
+ type TransferSource,
15
+ type TransferStatus,
16
+ } from '@gibs/bridge-indexer';
17
+ import type { AllowanceReader, PlannedTx } from '@gibs/bridge-sdk/execution';
18
+ import { type Chain, createPublicClient, erc20Abi, type Hex, http, type PublicClient, zeroAddress } from 'viem';
19
+ import { mainnet, pulsechain } from 'viem/chains';
20
+
21
+ /** One transaction for the wallet to sign and broadcast. */
22
+ export type WalletTransactionRequest = {
23
+ readonly chainId: number;
24
+ readonly from: Hex;
25
+ readonly to: Hex;
26
+ readonly data: Hex;
27
+ readonly value: bigint;
28
+ /** Set only when the plan names a gas limit. Otherwise the wallet estimates it. */
29
+ readonly gas?: bigint;
30
+ };
31
+
32
+ /**
33
+ * The signer the examples expect. It broadcasts one transaction on the chain
34
+ * the request names and resolves with the transaction hash, before the
35
+ * transaction is mined. The examples wait for receipts themselves, through
36
+ * the viem clients, only where the plan says `waitForReceipt`.
37
+ *
38
+ * A web3.js v4 adapter, with one `Web3` instance per chain:
39
+ *
40
+ * ```ts
41
+ * import { Web3 } from 'web3';
42
+ *
43
+ * const web3ByChainId = new Map<number, Web3>([[1, ethereumWeb3], [369, pulsechainWeb3]]);
44
+ * const signer: WalletSigner = {
45
+ * address: account,
46
+ * sendTransaction: (request) =>
47
+ * new Promise((resolve, reject) => {
48
+ * const web3 = web3ByChainId.get(request.chainId);
49
+ * if (web3 === undefined) return reject(new Error(`no web3 for chain ${request.chainId}`));
50
+ * // web3.eth.sendTransaction returns a PromiEvent. Awaiting it resolves
51
+ * // with the mined receipt; the 'transactionHash' event fires first.
52
+ * web3.eth
53
+ * .sendTransaction({
54
+ * from: request.from,
55
+ * to: request.to,
56
+ * data: request.data,
57
+ * value: request.value,
58
+ * ...(request.gas === undefined ? {} : { gas: request.gas }),
59
+ * })
60
+ * .on('transactionHash', (hash) => resolve(hash as Hex))
61
+ * .on('error', reject);
62
+ * }),
63
+ * };
64
+ * ```
65
+ */
66
+ export type WalletSigner = {
67
+ readonly address: Hex;
68
+ readonly sendTransaction: (request: WalletTransactionRequest) => Promise<Hex>;
69
+ };
70
+
71
+ /** The wallet's own RPC endpoint for each chain it supports, by decimal chain id. */
72
+ export type RpcUrlByChainId = ReadonlyMap<number, string>;
73
+
74
+ /** The chains these examples touch. A real wallet lists every chain it supports. */
75
+ const viemChainById: ReadonlyMap<number, Chain> = new Map<number, Chain>([
76
+ [mainnet.id, mainnet],
77
+ [pulsechain.id, pulsechain],
78
+ ]);
79
+
80
+ /**
81
+ * Builds one viem public client per chain, from the wallet's own endpoints.
82
+ * Never the demo endpoints (`demoRpcUrls` in `@gibs/bridge-sdk/chains`).
83
+ *
84
+ * @param rpcUrlByChainId - the wallet's endpoint for each chain
85
+ * @returns a lookup that answers null for a chain the wallet has no endpoint for
86
+ */
87
+ export const createClientLookup = (
88
+ rpcUrlByChainId: RpcUrlByChainId,
89
+ ): ((chainId: number) => PublicClient | null) => {
90
+ const clients = new Map<number, PublicClient>();
91
+ for (const [chainId, url] of rpcUrlByChainId) {
92
+ const chain = viemChainById.get(chainId);
93
+ if (chain === undefined) continue;
94
+ clients.set(chainId, createPublicClient({ chain, transport: http(url) }));
95
+ }
96
+ return (chainId) => clients.get(chainId) ?? null;
97
+ };
98
+
99
+ /**
100
+ * Wraps a client lookup so that a missing chain fails fast with a clear message.
101
+ *
102
+ * @param clientForChain - the lookup from {@link createClientLookup}
103
+ * @returns a lookup that throws for a chain with no client
104
+ */
105
+ export const requireClientWith =
106
+ (clientForChain: (chainId: number) => PublicClient | null) =>
107
+ (chainId: number): PublicClient => {
108
+ const client = clientForChain(chainId);
109
+ if (client === null) throw new Error(`the wallet has no RPC endpoint for chain ${chainId}`);
110
+ return client;
111
+ };
112
+
113
+ /**
114
+ * An allowance reader for `planCandidateExecution`, on the wallet's own clients.
115
+ *
116
+ * @param requireClient - see {@link requireClientWith}
117
+ * @returns the reader
118
+ */
119
+ export const allowanceReaderWith =
120
+ (requireClient: (chainId: number) => PublicClient): AllowanceReader =>
121
+ (chainId, token, owner, spender) =>
122
+ requireClient(chainId).readContract({
123
+ address: token,
124
+ abi: erc20Abi,
125
+ functionName: 'allowance',
126
+ args: [owner, spender],
127
+ });
128
+
129
+ /**
130
+ * Reads what an account holds of one token, or of the native coin when the
131
+ * token is the zero address. The examples read it before and after a
132
+ * crossing, so a later section spends what actually arrived.
133
+ *
134
+ * @param options.client - a client on the token's chain
135
+ * @param options.token - the token, or the zero address for the native coin
136
+ * @param options.owner - the account
137
+ * @returns the balance, in the token's base units
138
+ */
139
+ export const readBalance = (options: {
140
+ readonly client: PublicClient;
141
+ readonly token: string;
142
+ readonly owner: Hex;
143
+ }): Promise<bigint> => {
144
+ const { client, token, owner } = options;
145
+ if (token.toLowerCase() === zeroAddress) return client.getBalance({ address: owner });
146
+ return client.readContract({ address: token as Hex, abi: erc20Abi, functionName: 'balanceOf', args: [owner] });
147
+ };
148
+
149
+ /** One transaction the sender broadcast, with its hash. */
150
+ export type SentTransaction = { readonly transaction: PlannedTx; readonly hash: Hex };
151
+
152
+ /**
153
+ * Sends a plan's transactions in order.
154
+ *
155
+ * - It waits for a receipt only where the plan sets `waitForReceipt`.
156
+ * - A receipt is not arrival. When the next transaction is on another chain,
157
+ * it calls `waitForArrival` with the transaction that started the crossing,
158
+ * and sends nothing more until that resolves.
159
+ *
160
+ * @param options.transactions - the plan's transactions, in plan order
161
+ * @param options.signer - the wallet's signer
162
+ * @param options.requireClient - see {@link requireClientWith}
163
+ * @param options.waitForArrival - resolves once the crossing's funds arrived on the next chain
164
+ * @returns every transaction sent, with its hash, in order
165
+ * @throws when a receipt reports a revert
166
+ */
167
+ export const sendPlannedTransactions = async (options: {
168
+ readonly transactions: readonly PlannedTx[];
169
+ readonly signer: WalletSigner;
170
+ readonly requireClient: (chainId: number) => PublicClient;
171
+ readonly waitForArrival: (crossing: SentTransaction) => Promise<void>;
172
+ }): Promise<readonly SentTransaction[]> => {
173
+ const { transactions, signer, requireClient, waitForArrival } = options;
174
+ const sent: SentTransaction[] = [];
175
+ for (const [index, transaction] of transactions.entries()) {
176
+ const hash = await signer.sendTransaction({
177
+ chainId: transaction.chainId,
178
+ from: signer.address,
179
+ to: transaction.to,
180
+ data: transaction.data,
181
+ value: transaction.value,
182
+ ...(transaction.gasLimit === null ? {} : { gas: transaction.gasLimit }),
183
+ });
184
+ sent.push({ transaction, hash });
185
+ if (!transaction.waitForReceipt) continue;
186
+ const receipt = await requireClient(transaction.chainId).waitForTransactionReceipt({ hash });
187
+ if (receipt.status !== 'success') {
188
+ throw new Error(
189
+ `${transaction.kind} transaction ${hash} reverted on chain ${transaction.chainId}`,
190
+ );
191
+ }
192
+ const next = transactions[index + 1];
193
+ if (next !== undefined && next.chainId !== transaction.chainId) {
194
+ await waitForArrival({ transaction, hash });
195
+ }
196
+ }
197
+ return sent;
198
+ };
199
+
200
+ /**
201
+ * Waits for a time, or rejects when the signal fires.
202
+ *
203
+ * @param milliseconds - how long to wait
204
+ * @param signal - cancels the wait
205
+ */
206
+ export const sleep = (milliseconds: number, signal?: AbortSignal): Promise<void> =>
207
+ new Promise((resolve, reject) => {
208
+ if (signal?.aborted) return reject(signal.reason);
209
+ const timer = setTimeout(resolve, milliseconds);
210
+ signal?.addEventListener(
211
+ 'abort',
212
+ () => {
213
+ clearTimeout(timer);
214
+ reject(signal.reason);
215
+ },
216
+ { once: true },
217
+ );
218
+ });
219
+
220
+ /** A transfer that ended badly. Waiting longer does not change it. */
221
+ export class TransferEndedError extends Error {
222
+ constructor(readonly status: TransferStatus) {
223
+ super(`the transfer ended as ${status.state}`);
224
+ this.name = 'TransferEndedError';
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Polls `fetchTransferStatus` until `isDone` accepts an answer.
230
+ *
231
+ * @param options.source - the transaction that started the transfer
232
+ * @param options.statusOptions - passed through to `fetchTransferStatus`
233
+ * @param options.isDone - true for the answer the caller waits for
234
+ * @param options.intervalMs - the pause between polls
235
+ * @returns the first answer `isDone` accepts
236
+ * @throws {TransferEndedError} on `failed` or `stranded`
237
+ */
238
+ export const waitForTransferStatus = async (options: {
239
+ readonly source: TransferSource;
240
+ readonly statusOptions: FetchTransferStatusOptions;
241
+ readonly isDone: (status: TransferStatus) => boolean;
242
+ readonly intervalMs: number;
243
+ }): Promise<TransferStatus> => {
244
+ const { source, statusOptions, isDone, intervalMs } = options;
245
+ for (;;) {
246
+ const status = await fetchTransferStatus(source, statusOptions);
247
+ if (status.state === 'failed' || status.state === 'stranded')
248
+ throw new TransferEndedError(status);
249
+ if (isDone(status)) return status;
250
+ await sleep(intervalMs, statusOptions.signal);
251
+ }
252
+ };
253
+
254
+ /**
255
+ * The unix-seconds deadline a PulseX swap carries, `seconds` from now.
256
+ *
257
+ * @param seconds - how long the swap stays valid
258
+ * @returns the deadline
259
+ */
260
+ export const deadlineIn = (seconds: number): bigint =>
261
+ BigInt(Math.floor(Date.now() / 1000) + seconds);
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "exactOptionalPropertyTypes": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noEmit": true,
13
+ "skipLibCheck": true,
14
+ "isolatedModules": true,
15
+ "types": []
16
+ },
17
+ "include": ["*.ts"]
18
+ }