@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,428 @@
1
+ import { zeroAddress } from 'viem';
2
+ import * as abis from './abis.js';
3
+ import { Providers, toChain } from './config.js';
4
+ import { aggregatorOriginCalls, } from './consolidated-quote.js';
5
+ import { pulseXRouterForEdge, pulseXSwapPathForHop } from './routing/carriers/pulsex.js';
6
+ import { buildPulseXSwapTransaction, buildWrapTransaction, swapMinimumOut } from './routing/swap-transactions.js';
7
+ import { approvalAmounts, approveCall, bridgePathway, destinationDataParam, feeDirectorStructEncoded, feeTypeSettings, omnibridgeApproval, transactionInputs, } from './settings.js';
8
+ /**
9
+ * An {@link AllowanceReader} that reports no allowance anywhere, so the plan
10
+ * includes every approval. For a caller that reads allowances itself at send
11
+ * time.
12
+ */
13
+ export const assumeNoAllowance = async () => 0n;
14
+ const refusal = (value) => ({ kind: 'refused', refusal: value });
15
+ /** The fee director a delivery choice encodes, or null when the reader buys no delivery. */
16
+ const encodeDelivery = ({ leg, pathway, }) => {
17
+ const { delivery } = leg;
18
+ if (!delivery.shouldDeliver)
19
+ return { kind: 'encoded', feeDirector: null };
20
+ if (!pathway.requiresDelivery)
21
+ return refusal({ reason: 'delivery-not-offered' });
22
+ // A zero fee pays no relayer, so the transfer would wait for a delivery that never comes.
23
+ if (delivery.fee.maximumFee <= 0n)
24
+ return refusal({ reason: 'delivery-fee-missing' });
25
+ const feeDirector = feeDirectorStructEncoded({
26
+ recipient: leg.recipient,
27
+ bridgePathway: pathway,
28
+ feeType: delivery.fee.feeType,
29
+ priceCorrective: delivery.fee.priceCorrective,
30
+ gasTipFee: delivery.fee.gasTipFee,
31
+ percentFee: delivery.fee.percentFee,
32
+ feeTypeSettings: feeTypeSettings({ feeType: delivery.fee.feeType, unwrap: leg.unwrap }),
33
+ limit: delivery.fee.maximumFee,
34
+ assetOut: delivery.fee.assetOut,
35
+ });
36
+ if (feeDirector === null)
37
+ return refusal({ reason: 'delivery-fee-unencodable' });
38
+ return { kind: 'encoded', feeDirector };
39
+ };
40
+ const deliveryTermsOf = (delivery) => {
41
+ if (!delivery.shouldDeliver)
42
+ return { status: 'not-requested' };
43
+ return {
44
+ status: 'requested',
45
+ feeType: delivery.fee.feeType,
46
+ maximumFee: delivery.fee.maximumFee,
47
+ feeToken: delivery.fee.assetOut,
48
+ };
49
+ };
50
+ const omnibridgeCalls = (leg, account) => {
51
+ if (leg.amountIn <= 0n)
52
+ return refusal({ reason: 'amount-not-positive' });
53
+ const pathway = bridgePathway({ bridgeKey: leg.bridgeKey, isProd: leg.isProd, assetIn: leg.assetIn });
54
+ if (pathway === null)
55
+ return refusal({ reason: 'no-pathway' });
56
+ const encoded = encodeDelivery({ leg, pathway });
57
+ if (encoded.kind !== 'encoded')
58
+ return encoded;
59
+ const { feeDirector } = encoded;
60
+ const dataParam = destinationDataParam({
61
+ recipient: leg.recipient,
62
+ bridgePathway: pathway,
63
+ assetIn: leg.assetIn,
64
+ destinationRouter: pathway.destinationRouter,
65
+ feeDirectorStructEncoded: feeDirector,
66
+ assetLink: leg.assetLink,
67
+ shouldDeliver: leg.delivery.shouldDeliver,
68
+ unwrap: leg.unwrap,
69
+ });
70
+ const call = transactionInputs({
71
+ bridgeKey: leg.bridgeKey,
72
+ signedCrossing: { originChainId: Number(leg.bridgeKey[1]), destinationChainId: Number(leg.bridgeKey[2]) },
73
+ bridgePathway: pathway,
74
+ assetIn: leg.assetIn,
75
+ recipient: leg.recipient,
76
+ account,
77
+ assetLink: leg.assetLink,
78
+ destinationDataParam: dataParam,
79
+ amountToBridge: leg.amountIn,
80
+ feeDirectorStructEncoded: feeDirector,
81
+ shouldDeliver: leg.delivery.shouldDeliver,
82
+ });
83
+ if (call === null)
84
+ return refusal({ reason: 'bridge-call-refused' });
85
+ return {
86
+ kind: 'calls',
87
+ approval: omnibridgeApproval({
88
+ bridgePathway: pathway,
89
+ assetIn: leg.assetIn,
90
+ assetLink: leg.assetLink,
91
+ amountToBridge: leg.amountIn,
92
+ }),
93
+ action: { chainId: Number(leg.bridgeKey[1]), kind: 'bridge', ...call, gasLimit: null },
94
+ delivery: deliveryTermsOf(leg.delivery),
95
+ };
96
+ };
97
+ const pulseXSwapCalls = (leg) => {
98
+ const { edge } = leg.hop;
99
+ const chainId = edge.from.chainId;
100
+ const routerAddress = pulseXRouterForEdge(edge);
101
+ // The builder refuses a hop that records no priced path, a router other
102
+ // than the edge's own, and a leg on screen that disagrees.
103
+ const call = buildPulseXSwapTransaction({
104
+ hop: leg.hop,
105
+ amountIn: leg.amountIn,
106
+ minAmountOut: leg.minAmountOut,
107
+ recipient: leg.recipient,
108
+ deadline: leg.deadline,
109
+ signedLeg: { kind: 'swap', chainId },
110
+ chainId,
111
+ routerAddress,
112
+ });
113
+ if (call === null || routerAddress === null)
114
+ return refusal({ reason: 'swap-refused' });
115
+ const spendsNative = edge.from.address.toLowerCase() === zeroAddress;
116
+ return {
117
+ kind: 'calls',
118
+ approval: spendsNative
119
+ ? null
120
+ : { chainId, token: edge.from.address, spender: routerAddress, minimum: leg.amountIn },
121
+ action: { chainId, kind: 'swap', ...call, gasLimit: null },
122
+ delivery: null,
123
+ };
124
+ };
125
+ const wrapCalls = (leg) => {
126
+ const call = buildWrapTransaction({ edge: leg.edge, amountIn: leg.amountIn });
127
+ if (call === null)
128
+ return refusal({ reason: 'wrap-refused' });
129
+ const wraps = leg.edge.from.address.toLowerCase() === zeroAddress;
130
+ return {
131
+ kind: 'calls',
132
+ approval: null,
133
+ action: { chainId: leg.edge.from.chainId, kind: wraps ? 'wrap' : 'unwrap', ...call, gasLimit: null },
134
+ delivery: null,
135
+ };
136
+ };
137
+ const aggregatorCalls = (leg) => {
138
+ const origin = aggregatorOriginCalls({ quote: leg.quote, request: leg.request });
139
+ if (origin.kind === 'refused')
140
+ return refusal({ reason: 'aggregator-origin-refused', origin: origin.refusal });
141
+ const { approval, deposit } = origin;
142
+ const staysOnChain = leg.request.toChainId === leg.request.fromChainId;
143
+ return {
144
+ kind: 'calls',
145
+ approval,
146
+ action: {
147
+ chainId: leg.request.fromChainId,
148
+ kind: staysOnChain ? 'swap' : 'bridge',
149
+ to: deposit.to,
150
+ data: deposit.data,
151
+ value: deposit.value,
152
+ gasLimit: deposit.gasLimit,
153
+ },
154
+ delivery: null,
155
+ };
156
+ };
157
+ const legCalls = (leg, account) => {
158
+ if (leg.kind === 'omnibridge')
159
+ return omnibridgeCalls(leg, account);
160
+ if (leg.kind === 'pulsex-swap')
161
+ return pulseXSwapCalls(leg);
162
+ if (leg.kind === 'wrap')
163
+ return wrapCalls(leg);
164
+ return aggregatorCalls(leg);
165
+ };
166
+ /** The approve transactions an approval needs now: none when the allowance on chain covers it. */
167
+ const approvalTransactions = async ({ approval, account, readAllowance, }) => {
168
+ const allowance = await readAllowance(approval.chainId, approval.token, account, approval.spender);
169
+ return approvalAmounts({ allowance, minimum: approval.minimum }).map((amount) => ({
170
+ chainId: approval.chainId,
171
+ kind: 'approve',
172
+ ...approveCall({ token: approval.token, spender: approval.spender, amount }),
173
+ gasLimit: null,
174
+ }));
175
+ };
176
+ /**
177
+ * Plans the transactions for a list of legs, in order.
178
+ *
179
+ * Each leg contributes the exact approval it needs when the allowance on
180
+ * chain does not cover it (a reset to zero first when a smaller allowance
181
+ * remains), then its own call. Nothing is sent.
182
+ *
183
+ * @param options.account - the account that signs every transaction
184
+ * @param options.legs - the legs, in the order the wallet signs them
185
+ * @param options.readAllowance - reads an allowance on chain; see {@link AllowanceReader}
186
+ * @returns the ordered transactions, or the first leg that cannot be planned
187
+ */
188
+ export const planExecution = async ({ account, legs, readAllowance, }) => {
189
+ const calls = legs.map((leg) => legCalls(leg, account));
190
+ const refusedIndex = calls.findIndex((entry) => entry.kind === 'refused');
191
+ const refused = calls[refusedIndex];
192
+ if (refused !== undefined && refused.kind === 'refused') {
193
+ return { kind: 'refused', legIndex: refusedIndex, refusal: refused.refusal };
194
+ }
195
+ const planned = calls.flatMap((entry) => (entry.kind === 'calls' ? [entry] : []));
196
+ const perLeg = await Promise.all(planned.map(async (entry) => {
197
+ const approvals = entry.approval === null
198
+ ? []
199
+ : await approvalTransactions({ approval: entry.approval, account, readAllowance });
200
+ return [...approvals, entry.action];
201
+ }));
202
+ const ordered = perLeg.flat();
203
+ const transactions = ordered.map((action, index) => ({ ...action, waitForReceipt: index < ordered.length - 1 }));
204
+ const deliveries = planned.flatMap((entry, legIndex) => entry.delivery === null ? [] : [{ ...entry.delivery, legIndex }]);
205
+ return { kind: 'planned', transactions, deliveries };
206
+ };
207
+ const OMNIBRIDGE_CARRIERS = new Set(Object.values(Providers));
208
+ const isOmnibridgeCarrier = (carrier) => OMNIBRIDGE_CARRIERS.has(carrier);
209
+ const missing = (needs) => ({
210
+ kind: 'refused',
211
+ refusal: { reason: 'section-inputs-missing', needs },
212
+ });
213
+ const shape = (detail) => ({ kind: 'refused', refusal: { reason: 'section-shape', detail } });
214
+ const omnibridgeSectionLeg = ({ carrier, from, to, inputs, assetIn, }) => {
215
+ if (inputs.omnibridge === undefined)
216
+ return missing('omnibridge');
217
+ return {
218
+ kind: 'leg',
219
+ leg: {
220
+ kind: 'omnibridge',
221
+ bridgeKey: [carrier, toChain(from.chainId), toChain(to.chainId)],
222
+ isProd: inputs.omnibridge.isProd,
223
+ assetIn,
224
+ assetLink: inputs.omnibridge.assetLink,
225
+ amountIn: inputs.amountIn,
226
+ recipient: inputs.recipient,
227
+ delivery: inputs.omnibridge.delivery,
228
+ unwrap: inputs.omnibridge.unwrap,
229
+ },
230
+ };
231
+ };
232
+ const liveQuoteRequired = { kind: 'refused', refusal: { reason: 'live-quote-required' } };
233
+ const aggregatorSectionLeg = (carrier, inputs) => {
234
+ if (inputs.aggregator === undefined)
235
+ return liveQuoteRequired;
236
+ const { quote, request } = inputs.aggregator;
237
+ if (quote.provider !== carrier) {
238
+ return {
239
+ kind: 'refused',
240
+ refusal: { reason: 'quote-provider-mismatch', quoteProvider: quote.provider, sectionCarrier: carrier },
241
+ };
242
+ }
243
+ return { kind: 'leg', leg: { kind: 'aggregator', quote, request } };
244
+ };
245
+ const pulseXSectionLeg = (hop, inputs) => {
246
+ if (inputs.swap === undefined)
247
+ return missing('swap');
248
+ const { quotedAmountOut } = inputs.swap;
249
+ if (quotedAmountOut !== undefined && quotedAmountOut <= 0n) {
250
+ return { kind: 'refused', refusal: { reason: 'swap-quote-not-positive' } };
251
+ }
252
+ // Without a fresh quote the floor comes from the hop's own figure, so it must be a real quote.
253
+ if (quotedAmountOut === undefined && hop.outcome.kind !== 'quote') {
254
+ return { kind: 'refused', refusal: { reason: 'swap-estimate-only' } };
255
+ }
256
+ return {
257
+ kind: 'leg',
258
+ leg: {
259
+ kind: 'pulsex-swap',
260
+ hop,
261
+ amountIn: inputs.amountIn,
262
+ minAmountOut: swapMinimumOut({
263
+ quotedAmount: quotedAmountOut ?? hop.outcome.deliveredAmount,
264
+ slippageBasisPoints: inputs.swap.slippageBasisPoints,
265
+ }),
266
+ recipient: inputs.recipient,
267
+ deadline: inputs.swap.deadline,
268
+ },
269
+ };
270
+ };
271
+ /** Turns one section of an engine plan into its leg, dispatching on the first hop's carrier. */
272
+ const planSectionLeg = ({ hops, inputs, resolveToken, }) => {
273
+ const firstHop = hops[0];
274
+ if (firstHop === undefined)
275
+ return shape('the section has no hops');
276
+ const { carrier, origin, from, to } = firstHop.edge;
277
+ if (origin === 'by-hand')
278
+ return { kind: 'refused', refusal: { reason: 'paid-by-hand' } };
279
+ // An aggregator section may run several hops inside one signature (a fused
280
+ // omnibridge entry); every other carrier signs exactly one hop.
281
+ const singleHop = hops.length === 1;
282
+ if (isOmnibridgeCarrier(carrier)) {
283
+ if (!singleHop)
284
+ return shape('an omnibridge section is exactly one hop');
285
+ return omnibridgeSectionLeg({ carrier, from, to, inputs, assetIn: resolveToken(from) });
286
+ }
287
+ if (carrier === 'PulseX') {
288
+ if (!singleHop)
289
+ return shape('a swap section is exactly one hop');
290
+ return pulseXSectionLeg(firstHop, inputs);
291
+ }
292
+ if (carrier === 'wrap') {
293
+ if (!singleHop)
294
+ return shape('a wrap section is exactly one hop');
295
+ return { kind: 'leg', leg: { kind: 'wrap', edge: firstHop.edge, amountIn: inputs.amountIn } };
296
+ }
297
+ return aggregatorSectionLeg(carrier, inputs);
298
+ };
299
+ /** The sections of a plan, each as its hops. */
300
+ const planSections = (plan) => plan.signatures.map((group) => group.flatMap((hopIndex) => plan.hops[hopIndex] ?? []));
301
+ /**
302
+ * A candidate built without an engine plan is one signature: either a
303
+ * same-chain omnibridge crossing (`buildDirectCrossingCandidate`, whose quote
304
+ * names the bridge) or an aggregator quote.
305
+ *
306
+ * THE CANDIDATE'S OWN QUOTE IS NEVER SIGNED. It priced the route on screen
307
+ * and may be stale or a dry probe; only the live quote in
308
+ * `inputs.aggregator` is. Without one the section is refused with
309
+ * `live-quote-required`.
310
+ */
311
+ const planlessSectionLeg = (candidate, inputs) => {
312
+ const provider = candidate.quote.provider;
313
+ if (isOmnibridgeCarrier(provider)) {
314
+ return omnibridgeSectionLeg({
315
+ carrier: provider,
316
+ from: candidate.originToken,
317
+ to: candidate.destinationToken,
318
+ inputs,
319
+ assetIn: candidate.originToken,
320
+ });
321
+ }
322
+ return aggregatorSectionLeg(provider, inputs);
323
+ };
324
+ /**
325
+ * What is wrong with the requested window of sections, or null when it fits.
326
+ * A window starts at `firstSectionIndex` and spans `plannedCount` sections.
327
+ */
328
+ const sectionWindowProblem = ({ firstSectionIndex, plannedCount, sectionCount, }) => {
329
+ if (!Number.isSafeInteger(firstSectionIndex) || firstSectionIndex < 0) {
330
+ return `firstSectionIndex must be a non-negative safe integer, received ${firstSectionIndex}`;
331
+ }
332
+ if (plannedCount < 1)
333
+ return `pass at least 1 section input, received ${plannedCount}`;
334
+ if (firstSectionIndex + plannedCount > sectionCount) {
335
+ return `sections ${firstSectionIndex} to ${firstSectionIndex + plannedCount - 1} do not fit a route of ${sectionCount} section${sectionCount === 1 ? '' : 's'}`;
336
+ }
337
+ return null;
338
+ };
339
+ /**
340
+ * Plans the transactions for a chosen route candidate: the candidate
341
+ * `buildRouteCandidateFromPlan` returns for an engine plan (the interface's
342
+ * `buildCandidateFromEnginePlan`), or a candidate built without a plan.
343
+ *
344
+ * ONE SECTION IS ONE SIGNATURE. A plan's `signatures` groups its hops into
345
+ * sections; the section's first hop names its carrier. An omnibridge section
346
+ * becomes a crossing, a PulseX section a swap signed on its recorded path, a
347
+ * wrap section a wrap or unwrap, and any other wallet-signed carrier an
348
+ * aggregator deposit from the live quote the caller supplies. A section paid
349
+ * by hand has nothing to sign and is refused.
350
+ *
351
+ * A LATER SECTION CAN BE PLANNED ALONE. Pass `firstSectionIndex` to skip the
352
+ * sections that already landed. The wallet then plans the swap after the
353
+ * crossing arrived, from the amount that arrived, with a fresh
354
+ * `swap.quotedAmountOut` and a fresh `swap.deadline`. The slippage floor and the
355
+ * deadline are not fixed minutes before the swap is signed. Every safety check
356
+ * still applies to the later section: verified spenders, live quotes for
357
+ * aggregator sections, exact approvals.
358
+ *
359
+ * Every `sectionIndex` in a refusal and every `legIndex` in `deliveries` is
360
+ * the absolute section index.
361
+ *
362
+ * @param inputs - see {@link CandidateExecutionInputs}
363
+ * @returns the ordered transactions with the deliveries they buy, or the first section that cannot be planned
364
+ */
365
+ export const planCandidateExecution = async (inputs) => {
366
+ const { candidate, account, readAllowance, resolveToken, sections } = inputs;
367
+ const firstSectionIndex = inputs.firstSectionIndex ?? 0;
368
+ const plan = candidate.plan;
369
+ const allSections = plan === undefined ? null : planSections(plan);
370
+ const sectionCount = allSections === null ? 1 : allSections.length;
371
+ const problem = sectionWindowProblem({ firstSectionIndex, plannedCount: sections.length, sectionCount });
372
+ if (problem !== null) {
373
+ const validIndex = Number.isSafeInteger(firstSectionIndex) && firstSectionIndex >= 0;
374
+ return {
375
+ kind: 'refused',
376
+ sectionIndex: validIndex ? firstSectionIndex : 0,
377
+ refusal: { reason: 'section-shape', detail: problem },
378
+ };
379
+ }
380
+ const sectionLegs = sections.map((sectionInputs, index) => allSections === null
381
+ ? planlessSectionLeg(candidate, sectionInputs)
382
+ : planSectionLeg({ hops: allSections[firstSectionIndex + index] ?? [], inputs: sectionInputs, resolveToken }));
383
+ const refusedIndex = sectionLegs.findIndex((entry) => entry.kind === 'refused');
384
+ const refused = sectionLegs[refusedIndex];
385
+ if (refused !== undefined && refused.kind === 'refused') {
386
+ return { kind: 'refused', sectionIndex: firstSectionIndex + refusedIndex, refusal: refused.refusal };
387
+ }
388
+ const legs = sectionLegs.flatMap((entry) => (entry.kind === 'leg' ? [entry.leg] : []));
389
+ const planned = await planExecution({ account, legs, readAllowance });
390
+ if (planned.kind === 'refused') {
391
+ return { kind: 'refused', sectionIndex: firstSectionIndex + planned.legIndex, refusal: planned.refusal };
392
+ }
393
+ return {
394
+ kind: 'planned',
395
+ transactions: planned.transactions,
396
+ deliveries: planned.deliveries.map((delivery) => ({ ...delivery, legIndex: firstSectionIndex + delivery.legIndex })),
397
+ remainingSectionCount: sectionCount - firstSectionIndex - sections.length,
398
+ };
399
+ };
400
+ /**
401
+ * Reads a fresh quote for a PulseX section: what the router's `getAmountsOut`
402
+ * says the swap delivers for `amountIn`, along the exact path the swap signs
403
+ * ({@link pulseXSwapPathForHop}) on the edge's own router. A wallet passes the
404
+ * result as `swap.quotedAmountOut` just before it signs.
405
+ *
406
+ * @param options.hop - the swap hop; its recorded path and edge name what to read
407
+ * @param options.amountIn - the amount the swap spends, in base units
408
+ * @param options.client - a client that can call `readContract`
409
+ * @returns the amount the last token of the path delivers, or null when the hop records no path, names no known router, the amount is not positive, or the read fails or returns nothing
410
+ */
411
+ export const readPulseXSectionQuote = async ({ hop, amountIn, client, }) => {
412
+ const path = pulseXSwapPathForHop(hop);
413
+ const router = pulseXRouterForEdge(hop.edge);
414
+ if (path === null || router === null || amountIn <= 0n)
415
+ return null;
416
+ try {
417
+ const amounts = await client.readContract({
418
+ address: router,
419
+ abi: abis.univ2Router,
420
+ functionName: 'getAmountsOut',
421
+ args: [amountIn, path],
422
+ });
423
+ return amounts.at(-1) ?? null;
424
+ }
425
+ catch {
426
+ return null;
427
+ }
428
+ };
@@ -1,3 +1,4 @@
1
+ import { buildUrl } from '@gibs/common/url';
1
2
  import { parseSignature } from 'viem';
2
3
  import { relayApiBaseUrl } from './relay-quote.js';
3
4
  /**
@@ -158,10 +159,8 @@ export const buildHyperliquidAuthorizationRequest = ({ authorization, signature,
158
159
  if (!endpoint.startsWith('/')) {
159
160
  return null;
160
161
  }
161
- const url = new URL(`${baseUrl}${endpoint}`);
162
- url.searchParams.set('signature', signature);
163
162
  return {
164
- url: url.href,
163
+ url: buildUrl(`${baseUrl}${endpoint}`, { signature }),
165
164
  method,
166
165
  body: { ...body, signatureChainId },
167
166
  };
package/dist/index.d.ts CHANGED
@@ -18,3 +18,6 @@ export * from './route-steps.js';
18
18
  export * from './routing/index.js';
19
19
  export * from './settings.js';
20
20
  export * from './types.js';
21
+ export * from './claims.js';
22
+ export * from './execution.js';
23
+ export * from './relay-depository.js';
package/dist/index.js CHANGED
@@ -18,3 +18,6 @@ export * from './route-steps.js';
18
18
  export * from './routing/index.js';
19
19
  export * from './settings.js';
20
20
  export * from './types.js';
21
+ export * from './claims.js';
22
+ export * from './execution.js';
23
+ export * from './relay-depository.js';
@@ -77,12 +77,13 @@ import { type Hex } from 'viem';
77
77
  * `receiver-mismatch`, `destination-chain-mismatch`, `sending-asset-mismatch`,
78
78
  * `destination-call-flag-mismatch` and `minimum-amount-below-requested`
79
79
  * distrust `data`'s DECODED fields against what this application asked for;
80
- * `native-value-mismatch` distrusts `value`. `missing-request-context` is
80
+ * `origin-amount-mismatch` distrusts the decoded amount pulled from the
81
+ * depositor; `native-value-mismatch` distrusts `value`. `missing-request-context` is
81
82
  * different in kind: it means the CALLER never supplied what this
82
83
  * application expected, so none of the above could even be checked — see
83
84
  * `parseLifiContractCallsQuote` and `parseLifiQuote` in `consolidated-quote.ts`.
84
85
  */
85
- export type LifiExecutionRefusalReason = 'unknown-chain' | 'router-mismatch' | 'approval-address-mismatch' | 'unknown-selector' | 'calldata-undecodable' | 'receiver-mismatch' | 'destination-chain-mismatch' | 'sending-asset-mismatch' | 'destination-call-flag-mismatch' | 'minimum-amount-below-requested' | 'native-value-mismatch' | 'missing-request-context';
86
+ export type LifiExecutionRefusalReason = 'unknown-chain' | 'router-mismatch' | 'approval-address-mismatch' | 'unknown-selector' | 'calldata-undecodable' | 'receiver-mismatch' | 'destination-chain-mismatch' | 'sending-asset-mismatch' | 'destination-call-flag-mismatch' | 'minimum-amount-below-requested' | 'origin-amount-mismatch' | 'native-value-mismatch' | 'missing-request-context';
86
87
  /** Why a LI.FI execution transaction was refused before signing. */
87
88
  export type LifiExecutionRefusal = {
88
89
  readonly reason: LifiExecutionRefusalReason;
@@ -174,6 +175,16 @@ export type LifiExecutionExpectation = {
174
175
  * of constrained by the request. See `ConsolidatedQuote.toAmount`.
175
176
  */
176
177
  readonly minimumAmount: bigint | null;
178
+ /**
179
+ * The exact amount of the origin token this application sends, in base units,
180
+ * or `null` when the request names none.
181
+ *
182
+ * Compared with the decoded `swapData[0].fromAmount`, the amount pulled from
183
+ * the depositor. Without it a response could keep every other field honest and
184
+ * still pull more than the reader chose. The request, never the response,
185
+ * supplies this figure.
186
+ */
187
+ readonly originAmount: bigint | null;
177
188
  };
178
189
  /** The transaction fields {@link verifyLifiEvmExecution} reads off the raw response. */
179
190
  export type LifiExecutionTransactionRequest = {
@@ -389,6 +389,10 @@ export const verifyLifiEvmExecution = (transactionRequest, approvalAddress, expe
389
389
  return;
390
390
  }
391
391
  }
392
+ if (expectation.originAmount !== null && decoded.originAmount !== expectation.originAmount) {
393
+ refuse('origin-amount-mismatch', `decoded origin amount (${decoded.originAmount}) does not equal the amount to send (${expectation.originAmount})`);
394
+ return;
395
+ }
392
396
  const nativeOriginComponent = sameAddress(decoded.originSendingAssetId, zeroAddress)
393
397
  ? decoded.originAmount
394
398
  : 0n;
@@ -1,3 +1,4 @@
1
+ import { buildUrl } from '@gibs/common/url';
1
2
  import { arbitrum, avalanche, base, bsc, gnosis, mainnet, optimism, polygon, scroll, } from 'viem/chains';
2
3
  import { encodeFunctionData, erc20Abi, isAddress } from 'viem';
3
4
  import { depositAddressChainIds, isDepositAddressChain, relayChainIds, } from './chain-names.js';
@@ -102,9 +103,7 @@ export const nearIntentsAuthenticatedStablecoinFeeBasisPoints = 1;
102
103
  * @returns the fully-formed status address
103
104
  */
104
105
  export const nearIntentsStatusUrl = ({ quoteId }) => {
105
- const url = new URL(`${nearIntentsApiBaseUrl}/v0/status`);
106
- url.searchParams.set('depositAddress', quoteId);
107
- return url.toString();
106
+ return buildUrl(`${nearIntentsApiBaseUrl}/v0/status`, { depositAddress: quoteId });
108
107
  };
109
108
  /**
110
109
  * What this repository calls itself when it asks the one-click service.
@@ -82,12 +82,12 @@ export type PreQuoteRequest = {
82
82
  */
83
83
  relayReferrer?: string | null;
84
84
  /**
85
- * Overrides the `integrator` sent to LI.FI: `undefined` defaults to
86
- * {@link lifiIntegrator} (a free attribution label, `gibs.finance`),
87
- * `null` OMITS the `integrator` query parameter entirely (it is optional
88
- * per LI.FI's own `/v1/quote` schema — verified 2026-09-23 — an empty
89
- * string is not "absent", it is a literal empty integrator), and a string
90
- * is sent as-is.
85
+ * The `integrator` sent to LI.FI. Left out or `null`, the query parameter is
86
+ * OMITTED entirely (it is optional per LI.FI's own `/v1/quote` schema —
87
+ * verified 2026-09-23 — and an empty string would be a literal empty
88
+ * integrator, not an absent one). A string is sent as-is. Gibs Finance's own
89
+ * label is `lifiIntegrator` in `consolidated-quote.ts`; it is never sent
90
+ * unless you pass it.
91
91
  */
92
92
  lifiIntegrator?: string | null;
93
93
  };
package/dist/pre-quote.js CHANGED
@@ -1,4 +1,5 @@
1
- import { isNonEthereumVirtualMachineOrigin, lifiIntegrator, } from './consolidated-quote.js';
1
+ import { buildQuery } from '@gibs/common/url';
2
+ import { isNonEthereumVirtualMachineOrigin, } from './consolidated-quote.js';
2
3
  import { resolveAttribution } from './referral.js';
3
4
  import { relayReferrerField, slippageToBasisPoints, } from './relay-quote.js';
4
5
  /**
@@ -84,26 +85,21 @@ export const buildLifiPreQuoteRequest = (request) => {
84
85
  if (isNonEthereumVirtualMachineOrigin(request)) {
85
86
  return null;
86
87
  }
87
- const params = new URLSearchParams({
88
+ // Left out or `null` OMITS the query parameter (LI.FI's own schema marks it
89
+ // optional); a string is sent as-is. No package default: attribution is the
90
+ // caller's to choose. See `PreQuoteRequest.lifiIntegrator`'s doc.
91
+ const integrator = resolveAttribution(request.lifiIntegrator, undefined);
92
+ return buildQuery({
88
93
  fromChain: request.fromChainId.toString(),
89
94
  toChain: request.toChainId.toString(),
90
95
  fromToken: request.fromTokenAddress,
91
96
  toToken: request.toTokenAddress,
92
97
  fromAmount: request.amount.toString(),
93
98
  fromAddress: request.fromAddress,
99
+ // Each optional parameter is left out when it is undefined.
100
+ integrator,
101
+ slippage: request.slippage?.toString(),
94
102
  });
95
- // `null` means "no integrator of this caller's own" and OMITS the query
96
- // parameter (LI.FI's own schema marks it optional); `undefined` means
97
- // "this repository's own default"; a string is sent as-is. See
98
- // `PreQuoteRequest.lifiIntegrator`'s doc.
99
- const integrator = resolveAttribution(request.lifiIntegrator, lifiIntegrator);
100
- if (integrator !== undefined) {
101
- params.set('integrator', integrator);
102
- }
103
- if (request.slippage !== undefined) {
104
- params.set('slippage', request.slippage.toString());
105
- }
106
- return params.toString();
107
103
  };
108
104
  const asRecord = (value) => {
109
105
  if (typeof value !== 'object' || value === null) {
@@ -132,6 +132,23 @@ export type ProviderDescriptor = {
132
132
  * `providers.test.ts`).
133
133
  */
134
134
  destinationCallCarriesNativeValue: boolean;
135
+ /**
136
+ * How this provider takes an Ethereum token from the reader's wallet.
137
+ *
138
+ * - `'allowance'` — the provider's contract pulls the token, so the wallet
139
+ * must first approve that contract as a spender. The execution quote names
140
+ * the spender (`ConsolidatedQuote.approvalAddress`), and the executor
141
+ * raises the allowance before it sends.
142
+ * - `'transfer'` — the reader sends the token to an address the provider
143
+ * gives. A plain transfer needs no approval, and the parser leaves
144
+ * `approvalAddress` null.
145
+ *
146
+ * A DISPLAY FACT, NEVER A SIGNING ONE. The press reads the live quote's own
147
+ * `approvalAddress` and the live allowance. This field lets a route say, BEFORE
148
+ * any live quote exists, that its wallet may ask for an approval first.
149
+ * The native coin never needs one, whatever this says.
150
+ */
151
+ originTokenSpend: 'allowance' | 'transfer';
135
152
  /**
136
153
  * Phase one: the plain forward-solving origin-to-anchor swap pre-quote.
137
154
  *
package/dist/providers.js CHANGED
@@ -11,6 +11,8 @@ const lifiDescriptor = {
11
11
  executesDestinationCalls: true,
12
12
  destinationCallOriginChainIds: [Number(Chains.ETH)],
13
13
  destinationCallCarriesNativeValue: false,
14
+ // Its quote names the Diamond as `estimate.approvalAddress`.
15
+ originTokenSpend: 'allowance',
14
16
  preQuote: {
15
17
  method: 'GET',
16
18
  build: buildLifiPreQuoteRequest,
@@ -33,6 +35,8 @@ const relayDescriptor = {
33
35
  executesDestinationCalls: true,
34
36
  destinationCallOriginChainIds: 'any',
35
37
  destinationCallCarriesNativeValue: true,
38
+ // Its quote carries an approve step; the parser lifts its spender.
39
+ originTokenSpend: 'allowance',
36
40
  preQuote: {
37
41
  method: 'POST',
38
42
  build: buildRelayPreQuoteRequest,
@@ -61,6 +65,8 @@ const nearIntentsDescriptor = {
61
65
  executesDestinationCalls: false,
62
66
  destinationCallOriginChainIds: [],
63
67
  destinationCallCarriesNativeValue: false,
68
+ // The reader sends the token to a deposit address.
69
+ originTokenSpend: 'transfer',
64
70
  preQuote: {
65
71
  method: 'POST',
66
72
  build: buildNearIntentsPreQuoteRequest,
@@ -34,7 +34,11 @@ export declare const referralIntegrator = "gibs.finance";
34
34
  */
35
35
  export declare const resolveAttribution: (override: string | null | undefined, packageDefault: string | undefined) => string | undefined;
36
36
  /**
37
- * Payout address for Relay application fees.
37
+ * Gibs Finance's payout address for Relay application fees.
38
+ *
39
+ * NO BUILDER APPLIES IT. A Relay request carries an `appFees` entry only when
40
+ * the caller passes `relayAppFee`. The Gibs interface passes this address
41
+ * explicitly; any other caller passes its own, or nothing.
38
42
  *
39
43
  * Set to the owner's receiving wallet. Setting it back to `null` is a working
40
44
  * state rather than a broken one, and is the way to stop collecting: Relay
@@ -53,9 +57,13 @@ export declare const resolveAttribution: (override: string | null | undefined, p
53
57
  */
54
58
  export declare const relayReferralRecipient: Hex | null;
55
59
  /**
56
- * Default application fee in basis points. Ten basis points is one tenth of one
57
- * percent. The interface can override it per provider in stored settings; this
58
- * is the value a reader who has never touched the setting gets.
60
+ * The application fee the Gibs interface charges, in basis points. Ten basis
61
+ * points is one tenth of one percent. The interface can override it per
62
+ * provider in stored settings; this is the value a reader who has never touched
63
+ * the setting gets.
64
+ *
65
+ * NO BUILDER APPLIES IT. It reaches a request only when a caller passes it as
66
+ * `relayAppFee.basisPoints` (Relay) or converts it to `lifiFee` (LI.FI).
59
67
  */
60
68
  export declare const defaultReferralFeeBasisPoints = 10n;
61
69
  /**