@gibs/bridge-sdk 1.13.0 → 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 +19 -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 +69 -21
  162. package/dist/routing/carriers/pulsex.js +184 -65
  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 +7 -3
  168. package/dist/routing/index.js +7 -3
  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 +29 -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 +33 -1
  176. package/dist/routing/search/score.js +41 -6
  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 +234 -0
  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 +818 -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,300 @@
1
+ /**
2
+ * Example A: ETH on Ethereum to PLS on PulseChain.
3
+ *
4
+ * 1. Build viem clients from the wallet's own RPC endpoints.
5
+ * 2. Search with `searchBridgeRoutes` and pick the best served route.
6
+ * 3. Plan ONE section at a time with `planCandidateExecution`
7
+ * (`firstSectionIndex`), so a later section spends what actually arrived,
8
+ * with a fresh swap floor and deadline, or a fresh aggregator quote.
9
+ * 4. Send each section's transactions, honouring `waitForReceipt`, and wait
10
+ * for the crossing to arrive before planning the next section.
11
+ * 5. Poll `fetchTransferStatus` until the last crossing is delivered.
12
+ *
13
+ * Typechecked only (`docs/examples/tsconfig.json` in `@gibs/bridge-sdk`). Never run against mainnet.
14
+ */
15
+ import type { AggregatorStatusSource, FetchTransferStatusOptions } from '@gibs/bridge-indexer';
16
+ import { tokenBridgeInfo } from '@gibs/bridge-sdk/chain-info';
17
+ import { type Provider, Providers, toChain } from '@gibs/bridge-sdk/config';
18
+ import type { ConsolidatedQuote } from '@gibs/bridge-sdk/consolidated-quote';
19
+ import {
20
+ type CandidateExecutionPlan,
21
+ planCandidateExecution,
22
+ readPulseXSectionQuote,
23
+ type SectionExecutionInputs,
24
+ } from '@gibs/bridge-sdk/execution';
25
+ import type { PlannedHop, ResolveNodeToken, RoutePlan } from '@gibs/bridge-sdk/routing';
26
+ import { fetchLiveSectionQuote, type ServedBridgeRoute, searchBridgeRoutes } from '@gibs/quotes/search';
27
+ import { type Hex, type PublicClient, zeroAddress } from 'viem';
28
+ import {
29
+ allowanceReaderWith,
30
+ createClientLookup,
31
+ deadlineIn,
32
+ type RpcUrlByChainId,
33
+ readBalance,
34
+ requireClientWith,
35
+ type SentTransaction,
36
+ sendPlannedTransactions,
37
+ type WalletSigner,
38
+ waitForTransferStatus,
39
+ } from './shared.js';
40
+
41
+ const ethereumChainId = 1;
42
+ const pulsechainChainId = 369;
43
+
44
+ /** What the example needs from the wallet. */
45
+ export type EtherToPulseOptions = {
46
+ readonly signer: WalletSigner;
47
+ readonly rpcUrlByChainId: RpcUrlByChainId;
48
+ /** The ether to send, in wei. */
49
+ readonly amountWei: bigint;
50
+ /** Cancels the search, every live quote and every status poll. */
51
+ readonly signal: AbortSignal;
52
+ /** The wallet's own LI.FI key, sent only as the `x-lifi-api-key` header. Leave it out to use LI.FI's keyless tier. */
53
+ readonly lifiApiKey?: string;
54
+ /** The wallet's own Relay key, sent only as a header. Leave it out to use Relay's anonymous tier. */
55
+ readonly relayApiKey?: string;
56
+ };
57
+
58
+ const omnibridgeCarriers: ReadonlySet<string> = new Set(Object.values(Providers));
59
+
60
+ const isOmnibridgeCarrier = (carrier: string): carrier is Provider =>
61
+ omnibridgeCarriers.has(carrier);
62
+
63
+ /** A plan's sections, one per signature, each as its hops. */
64
+ const sectionsOf = (plan: RoutePlan): readonly (readonly PlannedHop[])[] =>
65
+ plan.signatures.map((group) => group.flatMap((hopIndex) => plan.hops[hopIndex] ?? []));
66
+
67
+ /** Where `fetchTransferStatus` asks about an aggregator's leg. */
68
+ const aggregatorSourceOf = (
69
+ quote: ConsolidatedQuote,
70
+ relayApiKey: string | undefined,
71
+ ): AggregatorStatusSource => {
72
+ if (quote.provider === 'lifi') return { provider: 'lifi' };
73
+ if (quote.provider === 'relay') {
74
+ return {
75
+ provider: 'relay',
76
+ requestId: quote.quoteId,
77
+ ...(relayApiKey === undefined ? {} : { apiKey: relayApiKey }),
78
+ };
79
+ }
80
+ return { provider: 'near-intents', depositAddress: quote.quoteId };
81
+ };
82
+
83
+ /** One section's inputs, and the live quote it signs when an aggregator carries it. */
84
+ type SectionPlanInputs = {
85
+ readonly inputs: SectionExecutionInputs;
86
+ readonly liveQuote: ConsolidatedQuote | null;
87
+ };
88
+
89
+ /**
90
+ * Builds the inputs for ONE section, from what it spends NOW: the typed
91
+ * amount for the first section, what actually arrived for a later one.
92
+ */
93
+ const buildSectionInputs = async (options: {
94
+ readonly served: ServedBridgeRoute;
95
+ readonly sectionIndex: number;
96
+ readonly hops: readonly PlannedHop[];
97
+ readonly amountIn: bigint;
98
+ readonly resolveToken: ResolveNodeToken;
99
+ readonly account: Hex;
100
+ readonly requireClient: (chainId: number) => PublicClient;
101
+ readonly clientForChain: (chainId: number) => PublicClient | null;
102
+ readonly signal: AbortSignal;
103
+ readonly lifiApiKey: string | undefined;
104
+ readonly relayApiKey: string | undefined;
105
+ }): Promise<SectionPlanInputs> => {
106
+ const { served, sectionIndex, hops, amountIn, resolveToken, account, requireClient } = options;
107
+ const firstHop = hops[0];
108
+ if (firstHop === undefined) throw new Error(`section ${sectionIndex} has no hops`);
109
+ // Every section delivers to the signer; this example sends to the signer too.
110
+ const recipient = account;
111
+ const { carrier, from, to } = firstHop.edge;
112
+
113
+ if (isOmnibridgeCarrier(carrier)) {
114
+ // How the token maps across the bridge. Native ether reads the wrapped-ether mapping.
115
+ const assetLink = await tokenBridgeInfo({
116
+ bridgeKey: [carrier, toChain(from.chainId), toChain(to.chainId)],
117
+ assetIn: resolveToken(from),
118
+ isProd: true,
119
+ fromChainClient: requireClient(from.chainId),
120
+ toChainClient: requireClient(to.chainId),
121
+ });
122
+ if (assetLink === null)
123
+ throw new Error(`no omnibridge mapping for ${from.address} on chain ${from.chainId}`);
124
+ // The explicit delivery choice. Entering PulseChain, the bridge itself
125
+ // executes the crossing, so `shouldDeliver: false` is the only accepted
126
+ // choice here. Leaving PulseChain, see Example B.
127
+ const omnibridge = { isProd: true, assetLink, delivery: { shouldDeliver: false }, unwrap: false } as const;
128
+ return { inputs: { amountIn, recipient, omnibridge }, liveQuote: null };
129
+ }
130
+ if (carrier === 'PulseX') {
131
+ // A FRESH QUOTE, read now along the exact path the swap signs, for the
132
+ // amount that actually arrived. The floor is one percent below it, and the
133
+ // deadline starts now, not before the crossing.
134
+ const quotedAmountOut = await readPulseXSectionQuote({
135
+ hop: firstHop,
136
+ amountIn,
137
+ client: requireClient(from.chainId),
138
+ });
139
+ if (quotedAmountOut === null) throw new Error(`section ${sectionIndex}: PulseX gave no quote`);
140
+ return {
141
+ inputs: { amountIn, recipient, swap: { slippageBasisPoints: 100n, deadline: deadlineIn(20 * 60), quotedAmountOut } },
142
+ liveQuote: null,
143
+ };
144
+ }
145
+ if (carrier === 'wrap') return { inputs: { amountIn, recipient }, liveQuote: null };
146
+ // Any other carrier is an aggregator. It signs only a LIVE quote, asked now
147
+ // for this wallet, this recipient and this amount.
148
+ const live = await fetchLiveSectionQuote({
149
+ plan: served.plan,
150
+ sectionIndex,
151
+ resolveToken,
152
+ amountIn,
153
+ account,
154
+ recipient,
155
+ clientForChain: options.clientForChain,
156
+ fetch: (input, init) => globalThis.fetch(input, init),
157
+ signal: options.signal,
158
+ keys: {
159
+ ...(options.lifiApiKey === undefined ? {} : { lifiApiKey: options.lifiApiKey }),
160
+ ...(options.relayApiKey === undefined ? {} : { relayApiKey: options.relayApiKey }),
161
+ },
162
+ nearIntentsUserAgent: 'your-wallet/1.0',
163
+ });
164
+ if (live.kind !== 'quoted') throw new Error(`section ${sectionIndex}: no live quote (${live.kind})`);
165
+ return { inputs: { amountIn, recipient, aggregator: { quote: live.quote, request: live.request } }, liveQuote: live.quote };
166
+ };
167
+
168
+ /** Turns a refused plan into an error a person can act on. */
169
+ const assertPlanned = (
170
+ plan: CandidateExecutionPlan,
171
+ ): Extract<CandidateExecutionPlan, { kind: 'planned' }> => {
172
+ if (plan.kind === 'planned') return plan;
173
+ throw new Error(
174
+ `section ${plan.sectionIndex} cannot be planned: ${JSON.stringify(plan.refusal, (_key, value) => (typeof value === 'bigint' ? value.toString() : value))}`,
175
+ );
176
+ };
177
+
178
+ /**
179
+ * Sends ether from Ethereum and delivers Pulse on PulseChain.
180
+ *
181
+ * @param options - see {@link EtherToPulseOptions}
182
+ * @returns the transactions sent, and the final status of the last crossing
183
+ */
184
+ export const sendEtherToPulse = async (options: EtherToPulseOptions) => {
185
+ const { signer, signal } = options;
186
+ const clientForChain = createClientLookup(options.rpcUrlByChainId);
187
+ const requireClient = requireClientWith(clientForChain);
188
+
189
+ // 1. Search. No attribution and no fee: the wallet names none here.
190
+ const search = await searchBridgeRoutes({
191
+ origin: {
192
+ chainId: ethereumChainId,
193
+ token: { address: zeroAddress, decimals: 18, symbol: 'ETH' },
194
+ amount: options.amountWei,
195
+ account: signer.address,
196
+ },
197
+ destination: {
198
+ chainId: pulsechainChainId,
199
+ token: { address: zeroAddress, decimals: 18, symbol: 'PLS' },
200
+ recipient: signer.address,
201
+ },
202
+ clientForChain,
203
+ fetch: (input, init) => globalThis.fetch(input, init),
204
+ signal,
205
+ timeouts: { requestMs: 15_000, searchMs: 60_000 },
206
+ ...(options.lifiApiKey === undefined ? {} : { lifiApiKey: options.lifiApiKey }),
207
+ ...(options.relayApiKey === undefined ? {} : { relayApiKey: options.relayApiKey }),
208
+ nearIntentsUserAgent: 'your-wallet/1.0',
209
+ });
210
+
211
+ // 2. Pick a route. `routes` is best first, one per provider.
212
+ const served = search.routes[0];
213
+ if (served === undefined) {
214
+ const why =
215
+ search.priceImpactRefusal === null ? `${search.refusals.length} refusals` : 'price impact';
216
+ throw new Error(`no route can be served (${why})`);
217
+ }
218
+ const hopsBySection = sectionsOf(served.plan);
219
+
220
+ const statusOptionsFor = (
221
+ crossing: SentTransaction,
222
+ liveQuote: ConsolidatedQuote | null,
223
+ ): FetchTransferStatusOptions => ({
224
+ signal,
225
+ originClient: requireClient(crossing.transaction.chainId),
226
+ ...(liveQuote === null ? {} : { aggregator: aggregatorSourceOf(liveQuote, options.relayApiKey) }),
227
+ });
228
+ const waitForDelivered = (crossing: SentTransaction, liveQuote: ConsolidatedQuote | null) =>
229
+ waitForTransferStatus({
230
+ source: { chainId: crossing.transaction.chainId, txHash: crossing.hash },
231
+ statusOptions: statusOptionsFor(crossing, liveQuote),
232
+ isDone: (status) => status.state === 'delivered',
233
+ intervalMs: 15_000,
234
+ });
235
+
236
+ // 3 and 4. One section at a time. The first spends the typed amount; each
237
+ // later one spends what actually arrived from the one before it.
238
+ const sent: SentTransaction[] = [];
239
+ let amountIn = options.amountWei;
240
+ for (const [sectionIndex, hops] of hopsBySection.entries()) {
241
+ const { inputs, liveQuote } = await buildSectionInputs({
242
+ served,
243
+ sectionIndex,
244
+ hops,
245
+ amountIn,
246
+ resolveToken: search.resolveToken,
247
+ account: signer.address,
248
+ requireClient,
249
+ clientForChain,
250
+ signal,
251
+ lifiApiKey: options.lifiApiKey,
252
+ relayApiKey: options.relayApiKey,
253
+ });
254
+ const plan = assertPlanned(
255
+ await planCandidateExecution({
256
+ candidate: served.candidate,
257
+ account: signer.address,
258
+ readAllowance: allowanceReaderWith(requireClient),
259
+ resolveToken: search.resolveToken,
260
+ firstSectionIndex: sectionIndex,
261
+ sections: [inputs],
262
+ }),
263
+ );
264
+ // Show `plan.deliveries` and every fee before the user signs. See "Fees and attribution".
265
+
266
+ // What the next section will spend arrives on its own chain. Read the
267
+ // balance there before this section runs, and again once it arrived.
268
+ const nextHop = hopsBySection[sectionIndex + 1]?.[0];
269
+ const nextToken = nextHop === undefined ? null : search.resolveToken(nextHop.edge.from);
270
+ const readNextBalance = () =>
271
+ nextToken === null
272
+ ? Promise.resolve(0n)
273
+ : readBalance({ client: requireClient(nextToken.chainId), token: nextToken.address, owner: signer.address });
274
+ const balanceBefore = await readNextBalance();
275
+
276
+ // Every transaction of one section is on one chain.
277
+ const sectionSent = await sendPlannedTransactions({
278
+ transactions: plan.transactions,
279
+ signer,
280
+ requireClient,
281
+ waitForArrival: async () => {},
282
+ });
283
+ sent.push(...sectionSent);
284
+ const last = sectionSent.at(-1);
285
+ if (last === undefined) throw new Error(`section ${sectionIndex} planned no transactions`);
286
+ const receipt = await requireClient(last.transaction.chainId).waitForTransactionReceipt({ hash: last.hash });
287
+ if (receipt.status !== 'success') throw new Error(`the transaction ${last.hash} reverted`);
288
+
289
+ const crossesChains = last.transaction.kind === 'bridge';
290
+ // 5. The route ends with this section. A crossing is done when delivered;
291
+ // a swap is done when its receipt succeeds.
292
+ if (nextToken === null) {
293
+ return { sent, finalStatus: crossesChains ? await waitForDelivered(last, liveQuote) : null };
294
+ }
295
+ if (crossesChains) await waitForDelivered(last, liveQuote);
296
+ amountIn = (await readNextBalance()) - balanceBefore;
297
+ if (amountIn <= 0n) throw new Error(`nothing arrived for section ${sectionIndex + 1}`);
298
+ }
299
+ throw new Error('the route had no sections');
300
+ };
@@ -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
+ };