@gibs/bridge-sdk 1.12.0 → 1.13.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.
- package/LICENSE +15 -0
- package/README.md +234 -432
- package/dist/abis.d.ts +1 -0
- package/dist/bridge-limits.d.ts +103 -0
- package/dist/bridge-limits.js +84 -0
- package/dist/chain-info.d.ts +169 -0
- package/dist/chain-info.js +410 -0
- package/dist/chain-names.d.ts +160 -0
- package/dist/chain-names.js +200 -0
- package/dist/chains.d.ts +65 -0
- package/dist/chains.js +284 -0
- package/dist/config.d.ts +216 -0
- package/dist/config.js +493 -0
- package/dist/consolidated-quote.d.ts +554 -0
- package/dist/consolidated-quote.js +773 -0
- package/dist/destination-calldata.d.ts +118 -0
- package/dist/destination-calldata.js +152 -0
- package/dist/ecosystems.d.ts +217 -0
- package/dist/ecosystems.js +288 -0
- package/dist/fee-type.d.ts +10 -0
- package/dist/fee-type.js +9 -0
- package/dist/hyperliquid-execution.d.ts +152 -0
- package/dist/hyperliquid-execution.js +168 -0
- package/dist/image-links.d.ts +117 -0
- package/dist/image-links.js +142 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/lifi-execution-guard.d.ts +199 -0
- package/dist/lifi-execution-guard.js +400 -0
- package/dist/near-intents-conformance.d.ts +85 -0
- package/dist/near-intents-conformance.js +329 -0
- package/dist/near-intents-route.d.ts +47 -0
- package/dist/near-intents-route.js +265 -0
- package/dist/near-intents.d.ts +524 -0
- package/dist/near-intents.js +1055 -0
- package/dist/origin-deposit.d.ts +326 -0
- package/dist/origin-deposit.js +68 -0
- package/dist/pre-quote.d.ts +198 -0
- package/dist/pre-quote.js +175 -0
- package/dist/prices.d.ts +107 -0
- package/dist/prices.js +162 -0
- package/dist/providers.d.ts +221 -0
- package/dist/providers.js +157 -0
- package/dist/quote-fees.d.ts +102 -0
- package/dist/quote-fees.js +195 -0
- package/dist/referral.d.ts +77 -0
- package/dist/referral.js +72 -0
- package/dist/relay-currencies.d.ts +47 -0
- package/dist/relay-currencies.js +89 -0
- package/dist/relay-quote.d.ts +301 -0
- package/dist/relay-quote.js +983 -0
- package/dist/route-steps.d.ts +138 -0
- package/dist/route-steps.js +305 -0
- package/dist/routing/amounts.d.ts +80 -0
- package/dist/routing/amounts.js +120 -0
- package/dist/routing/candidate-route.d.ts +43 -0
- package/dist/routing/candidate-route.js +45 -0
- package/dist/routing/candidate.d.ts +237 -0
- package/dist/routing/candidate.js +96 -0
- package/dist/routing/carriers/ask-answer.d.ts +58 -0
- package/dist/routing/carriers/ask-answer.js +1 -0
- package/dist/routing/carriers/omnibridge.d.ts +137 -0
- package/dist/routing/carriers/omnibridge.js +419 -0
- package/dist/routing/carriers/pulsex.d.ts +111 -0
- package/dist/routing/carriers/pulsex.js +404 -0
- package/dist/routing/carriers/wrap.d.ts +25 -0
- package/dist/routing/carriers/wrap.js +115 -0
- package/dist/routing/composed-route-quality.d.ts +103 -0
- package/dist/routing/composed-route-quality.js +108 -0
- package/dist/routing/entry-chain-anchor.d.ts +88 -0
- package/dist/routing/entry-chain-anchor.js +55 -0
- package/dist/routing/format.d.ts +31 -0
- package/dist/routing/format.js +58 -0
- package/dist/routing/index.d.ts +34 -0
- package/dist/routing/index.js +42 -0
- package/dist/routing/producers.d.ts +770 -0
- package/dist/routing/producers.js +1564 -0
- package/dist/routing/providers.d.ts +26 -0
- package/dist/routing/providers.js +38 -0
- package/dist/routing/route-data.d.ts +355 -0
- package/dist/routing/route-data.js +1 -0
- package/dist/routing/search/compose.d.ts +168 -0
- package/dist/routing/search/compose.js +98 -0
- package/dist/routing/search/graph.d.ts +639 -0
- package/dist/routing/search/graph.js +445 -0
- package/dist/routing/search/plan-route-data.d.ts +244 -0
- package/dist/routing/search/plan-route-data.js +767 -0
- package/dist/routing/search/score.d.ts +324 -0
- package/dist/routing/search/score.js +400 -0
- package/dist/routing/usd-value.d.ts +17 -0
- package/dist/routing/usd-value.js +25 -0
- package/dist/settings.d.ts +692 -0
- package/dist/settings.js +909 -0
- package/dist/ton-execution.d.ts +117 -0
- package/dist/ton-execution.js +247 -0
- package/dist/tron-execution.d.ts +146 -0
- package/dist/tron-execution.js +239 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +1 -0
- package/package.json +203 -23
- package/src/chain-info.test.ts +0 -968
- package/src/chain-info.ts +0 -498
- package/src/chains.test.ts +0 -123
- package/src/chains.ts +0 -88
- package/src/config.test.ts +0 -679
- package/src/config.ts +0 -500
- package/src/fee-type.test.ts +0 -45
- package/src/fee-type.ts +0 -14
- package/src/image-links.test.ts +0 -160
- package/src/image-links.ts +0 -65
- package/src/index.ts +0 -8
- package/src/settings.test.ts +0 -2754
- package/src/settings.ts +0 -875
- package/src/types.ts +0 -48
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -13
- /package/{src/abis.ts → dist/abis.js} +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { multicallRead } from '@gibs/common/multicall';
|
|
2
|
+
import { getAddress, zeroAddress } from 'viem';
|
|
3
|
+
import * as abis from '../../abis.js';
|
|
4
|
+
import { checkEntryAmount, entryLimitsAbi } from '../../bridge-limits.js';
|
|
5
|
+
import { chainsMetadata } from '../../chains.js';
|
|
6
|
+
import { FOREIGN_TO_HOME_FEE, HOME_TO_FOREIGN_FEE, nativeAssetOut, pathway, Providers, pathwayGroup, toChain, } from '../../config.js';
|
|
7
|
+
import { isNativeAsset } from '../../ecosystems.js';
|
|
8
|
+
import { findProviderById } from '../../providers.js';
|
|
9
|
+
import { netDeliveredAmount } from '../amounts.js';
|
|
10
|
+
import { entryChainAnchorsForPair } from '../entry-chain-anchor.js';
|
|
11
|
+
import { BRIDGE_SETTLEMENT_ETA_MINUTES, pulsechainChainId } from '../producers.js';
|
|
12
|
+
import { assetNodeKey } from '../search/graph.js';
|
|
13
|
+
/** Builds an {@link AssetNode} for an Ethereum-Virtual-Machine token. */
|
|
14
|
+
const evmNode = (token) => ({
|
|
15
|
+
chainId: token.chainId,
|
|
16
|
+
address: token.address,
|
|
17
|
+
decimals: token.decimals,
|
|
18
|
+
ecosystem: 'evm',
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* A directed pair of node keys, joined for use as a `Map` key. Never leaves
|
|
22
|
+
* this module — see `capabilities.ts`'s former `directedPairKey` doc for why
|
|
23
|
+
* a plain `|` join is enough here.
|
|
24
|
+
*/
|
|
25
|
+
const directedPairKey = (from, to) => `${assetNodeKey(from)}|${assetNodeKey(to)}`;
|
|
26
|
+
/**
|
|
27
|
+
* Finds which omnibridge provider, if any, has a live pathway carrying funds
|
|
28
|
+
* from `fromChainId` to `toChainId`. Checks BOTH omnibridge providers rather
|
|
29
|
+
* than assuming one: `Providers.PULSECHAIN` carries the Ethereum pathway,
|
|
30
|
+
* `Providers.TOKENSEX` the Binance Smart Chain one.
|
|
31
|
+
*/
|
|
32
|
+
const omnibridgeCarrierBetween = (fromChainId, toChainId, isProductionNetwork) => {
|
|
33
|
+
const fromChain = toChain(fromChainId);
|
|
34
|
+
const toChainKey = toChain(toChainId);
|
|
35
|
+
const omnibridgeProviders = [Providers.PULSECHAIN, Providers.TOKENSEX];
|
|
36
|
+
for (const provider of omnibridgeProviders) {
|
|
37
|
+
const group = pathwayGroup(provider, fromChain, isProductionNetwork);
|
|
38
|
+
if (group?.[toChainKey]) {
|
|
39
|
+
return provider;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Builds the omnibridge's directed edge map from the bridgeable pairs and the
|
|
46
|
+
* live pathway config, in both directions. INBOUND goes through
|
|
47
|
+
* {@link entryChainAnchorsForPair} so a native coin and its wrapped form both
|
|
48
|
+
* anchor the same PulseChain mint; OUTBOUND needs no aliasing.
|
|
49
|
+
*/
|
|
50
|
+
const buildOmnibridgeEdgeMap = (bridgeablePairs, isProductionNetwork) => {
|
|
51
|
+
const edges = new Map();
|
|
52
|
+
for (const pair of bridgeablePairs) {
|
|
53
|
+
const inboundCarrier = omnibridgeCarrierBetween(pair.entryChainId, pair.pulsechainToken.chainId, isProductionNetwork);
|
|
54
|
+
if (inboundCarrier !== null) {
|
|
55
|
+
const anchors = entryChainAnchorsForPair({
|
|
56
|
+
entryChainToken: pair.entryChainToken,
|
|
57
|
+
pulsechainToken: pair.pulsechainToken,
|
|
58
|
+
entryChainId: pair.entryChainId,
|
|
59
|
+
});
|
|
60
|
+
for (const anchor of anchors) {
|
|
61
|
+
edges.set(directedPairKey(evmNode(anchor.token), evmNode(anchor.landsAs)), inboundCarrier);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const outboundCarrier = omnibridgeCarrierBetween(pair.pulsechainToken.chainId, pair.entryChainId, isProductionNetwork);
|
|
65
|
+
if (outboundCarrier !== null) {
|
|
66
|
+
edges.set(directedPairKey(evmNode(pair.pulsechainToken), evmNode(pair.entryChainToken)), outboundCarrier);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return edges;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Resolves one edge's crossing details from the live pathway config, or null
|
|
73
|
+
* when no pathway matches — the same "no live pathway" case `edgesFrom`
|
|
74
|
+
* already prunes, kept here as a guard rather than assumed away, because
|
|
75
|
+
* `prepare` walks every node pair itself rather than trusting the caller to
|
|
76
|
+
* hand it only edges this carrier already offered.
|
|
77
|
+
*/
|
|
78
|
+
const resolveCrossing = (edge, isProductionNetwork) => {
|
|
79
|
+
const bridgeKey = [edge.carrier, toChain(edge.from.chainId), toChain(edge.to.chainId)];
|
|
80
|
+
const wrappedNativeAddress = nativeAssetOut[toChain(edge.from.chainId)];
|
|
81
|
+
const isNativeEntry = edge.from.address.toLowerCase() === zeroAddress;
|
|
82
|
+
const entryTokenAddress = isNativeEntry && wrappedNativeAddress !== undefined ? getAddress(wrappedNativeAddress) : getAddress(edge.from.address);
|
|
83
|
+
const path = pathway(bridgeKey, isProductionNetwork, entryTokenAddress);
|
|
84
|
+
if (path === undefined)
|
|
85
|
+
return null;
|
|
86
|
+
const feeManagerHostChainId = path.feeManager === 'from' ? edge.from.chainId : edge.to.chainId;
|
|
87
|
+
const feeManagerHostAddress = path.feeManager === 'from' ? path.from : path.to;
|
|
88
|
+
return {
|
|
89
|
+
edge,
|
|
90
|
+
entryTokenAddress,
|
|
91
|
+
entryMediator: path.from,
|
|
92
|
+
limitsChainId: edge.from.chainId,
|
|
93
|
+
feeManagerHostAddress,
|
|
94
|
+
feeManagerHostChainId,
|
|
95
|
+
feeTypeKey: path.toHome ? FOREIGN_TO_HOME_FEE : HOME_TO_FOREIGN_FEE,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
/** Groups values by a computed chain id, preserving the deduped-by-`keyOf` invariant callers rely on. */
|
|
99
|
+
const groupByChainId = (values, chainIdOf) => {
|
|
100
|
+
const groups = new Map();
|
|
101
|
+
for (const value of values) {
|
|
102
|
+
const chainId = chainIdOf(value);
|
|
103
|
+
const group = groups.get(chainId);
|
|
104
|
+
if (group === undefined) {
|
|
105
|
+
groups.set(chainId, [value]);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
group.push(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return groups;
|
|
112
|
+
};
|
|
113
|
+
/** Stage-one read: `minPerTx`, `maxAvailablePerTx` (an entry mediator's own bounds) and `feeManager()` (the fee contract's address) — every one a fixed-arity view call, so one multicall per chain covers all three kinds at once. */
|
|
114
|
+
const readStageOneBatch = async ({ resolvedCrossings, clientForChain, }) => {
|
|
115
|
+
const limitsByKey = new Map();
|
|
116
|
+
const feeManagerAddressByKey = new Map();
|
|
117
|
+
const limitsRequests = new Map();
|
|
118
|
+
for (const crossing of resolvedCrossings) {
|
|
119
|
+
limitsRequests.set(limitsKey(crossing), {
|
|
120
|
+
chainId: crossing.limitsChainId,
|
|
121
|
+
mediator: crossing.entryMediator,
|
|
122
|
+
token: crossing.entryTokenAddress,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const feeManagerAddressRequests = new Map();
|
|
126
|
+
for (const crossing of resolvedCrossings) {
|
|
127
|
+
feeManagerAddressRequests.set(feeManagerHostKey(crossing), {
|
|
128
|
+
chainId: crossing.feeManagerHostChainId,
|
|
129
|
+
mediatorHost: crossing.feeManagerHostAddress,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const requestsByChain = groupByChainId([
|
|
133
|
+
...Array.from(limitsRequests.entries()).map(([key, request]) => ({ kind: 'limits', key, ...request })),
|
|
134
|
+
...Array.from(feeManagerAddressRequests.entries()).map(([key, request]) => ({
|
|
135
|
+
kind: 'fee-manager-address',
|
|
136
|
+
key,
|
|
137
|
+
chainId: request.chainId,
|
|
138
|
+
mediatorHost: request.mediatorHost,
|
|
139
|
+
})),
|
|
140
|
+
], (request) => request.chainId);
|
|
141
|
+
await Promise.all(Array.from(requestsByChain.entries()).map(async ([chainId, requests]) => {
|
|
142
|
+
const client = clientForChain(chainId);
|
|
143
|
+
if (client === null)
|
|
144
|
+
return;
|
|
145
|
+
const results = await multicallRead({
|
|
146
|
+
chain: chainsMetadata[toChain(chainId)],
|
|
147
|
+
client,
|
|
148
|
+
abi: entryLimitsAbi,
|
|
149
|
+
calls: requests.flatMap((request) => request.kind === 'limits'
|
|
150
|
+
? [
|
|
151
|
+
{ functionName: 'minPerTx', args: [request.token], target: request.mediator, allowFailure: true },
|
|
152
|
+
{ functionName: 'maxAvailablePerTx', args: [request.token], target: request.mediator, allowFailure: true },
|
|
153
|
+
]
|
|
154
|
+
: [{ functionName: 'feeManager', abi: abis.inputBridge, args: [], target: request.mediatorHost, allowFailure: true }]),
|
|
155
|
+
});
|
|
156
|
+
let cursor = 0;
|
|
157
|
+
for (const request of requests) {
|
|
158
|
+
if (request.kind === 'limits') {
|
|
159
|
+
const minimumPerTransaction = results[cursor];
|
|
160
|
+
const availablePerTransaction = results[cursor + 1];
|
|
161
|
+
cursor += 2;
|
|
162
|
+
if (typeof minimumPerTransaction === 'bigint' && typeof availablePerTransaction === 'bigint') {
|
|
163
|
+
limitsByKey.set(request.key, { minimumPerTransaction, availablePerTransaction });
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const feeManagerAddress = results[cursor];
|
|
168
|
+
cursor += 1;
|
|
169
|
+
// A DECODED address is exactly 42 characters ("0x" + 40 hex digits).
|
|
170
|
+
// `multicallRead` returns the UNDECODED revert bytes on a failed
|
|
171
|
+
// `allowFailure` call — also a `string`, usually a different length
|
|
172
|
+
// (`'0x'` for a bare revert) — so `typeof` alone cannot tell success
|
|
173
|
+
// from failure here the way it does for the `bigint` reads above.
|
|
174
|
+
if (typeof feeManagerAddress === 'string' && feeManagerAddress.length === 42) {
|
|
175
|
+
feeManagerAddressByKey.set(request.key, getAddress(feeManagerAddress));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
return { limitsByKey, feeManagerAddressByKey };
|
|
180
|
+
};
|
|
181
|
+
/** Stage-two read: `getFee(feeTypeKey, address(0))` on every fee manager contract stage one resolved — needs the address stage one just learned, so it cannot join that multicall. */
|
|
182
|
+
const readStageTwoBatch = async ({ resolvedCrossings, feeManagerAddressByKey, clientForChain, }) => {
|
|
183
|
+
const feeRateByKey = new Map();
|
|
184
|
+
const feeRateRequests = new Map();
|
|
185
|
+
for (const crossing of resolvedCrossings) {
|
|
186
|
+
const feeManagerAddress = feeManagerAddressByKey.get(feeManagerHostKey(crossing));
|
|
187
|
+
if (feeManagerAddress === undefined)
|
|
188
|
+
continue;
|
|
189
|
+
feeRateRequests.set(feeRateKey(crossing, feeManagerAddress), {
|
|
190
|
+
chainId: crossing.feeManagerHostChainId,
|
|
191
|
+
feeManagerAddress,
|
|
192
|
+
feeTypeKey: crossing.feeTypeKey,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const requestsByChain = groupByChainId(Array.from(feeRateRequests.entries()), ([, request]) => request.chainId);
|
|
196
|
+
await Promise.all(Array.from(requestsByChain.entries()).map(async ([chainId, entries]) => {
|
|
197
|
+
const client = clientForChain(chainId);
|
|
198
|
+
if (client === null)
|
|
199
|
+
return;
|
|
200
|
+
const results = await multicallRead({
|
|
201
|
+
chain: chainsMetadata[toChain(chainId)],
|
|
202
|
+
client,
|
|
203
|
+
abi: abis.feeManager,
|
|
204
|
+
calls: entries.map(([, request]) => ({
|
|
205
|
+
functionName: 'getFee',
|
|
206
|
+
args: [request.feeTypeKey, zeroAddress],
|
|
207
|
+
target: request.feeManagerAddress,
|
|
208
|
+
allowFailure: true,
|
|
209
|
+
})),
|
|
210
|
+
});
|
|
211
|
+
entries.forEach(([key], index) => {
|
|
212
|
+
const rate = results[index];
|
|
213
|
+
if (typeof rate === 'bigint') {
|
|
214
|
+
feeRateByKey.set(key, rate);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}));
|
|
218
|
+
return feeRateByKey;
|
|
219
|
+
};
|
|
220
|
+
const limitsKey = (crossing) => `${crossing.limitsChainId}|${crossing.entryMediator}|${crossing.entryTokenAddress}`;
|
|
221
|
+
const feeManagerHostKey = (crossing) => `${crossing.feeManagerHostChainId}|${crossing.feeManagerHostAddress}`;
|
|
222
|
+
const feeRateKey = (crossing, feeManagerAddress) => `${crossing.feeManagerHostChainId}|${feeManagerAddress}|${crossing.feeTypeKey}`;
|
|
223
|
+
const preparedEdgeKey = (edge) => `${edge.carrier}|${assetNodeKey(edge.from)}|${assetNodeKey(edge.to)}`;
|
|
224
|
+
/**
|
|
225
|
+
* Batch-reads every entry-mediator bound and fee rate `nodes` implies, once,
|
|
226
|
+
* so {@link createOmnibridgeCarrier}'s `exact` can answer every crossing
|
|
227
|
+
* SYNCHRONOUSLY afterward — the owner's own model, "each individual step and
|
|
228
|
+
* its associated fees can be known ahead of time."
|
|
229
|
+
*
|
|
230
|
+
* TWO MULTICALLS PER CHAIN, NOT ONE: `getFee` needs the fee manager's
|
|
231
|
+
* address, which stage one is what LEARNS — the same two-stage shape
|
|
232
|
+
* `chain-info.ts`'s `loadBridgeFees` already uses for one pathway at a time,
|
|
233
|
+
* generalized here to batch across every crossing the search's node universe
|
|
234
|
+
* touches. `HOME_TO_FOREIGN_FEE`/`FOREIGN_TO_HOME_FEE` are not re-read from
|
|
235
|
+
* the chain — they are fixed `keccak256` constants (`config.ts`), so reading
|
|
236
|
+
* them per pathway (as `loadBridgeFees` still does, defensively) would only
|
|
237
|
+
* spend a third multicall confirming a value that cannot change.
|
|
238
|
+
*
|
|
239
|
+
* WATCH PAYLOAD SIZE (request-payload-size-traps.md). `nodes` is the search
|
|
240
|
+
* graph's own hub-token-restricted universe, not a full token list — this
|
|
241
|
+
* function reads one `minPerTx`/`maxAvailablePerTx` pair per DISTINCT
|
|
242
|
+
* (mediator, token) crossing in that universe, never per token a chain
|
|
243
|
+
* lists. `multicallRead` (`@gibs/common`) chunks by measured encoded size on
|
|
244
|
+
* top of that, so a universe large enough to matter still cannot repeat the
|
|
245
|
+
* 8.5 MB trap that memory records.
|
|
246
|
+
*
|
|
247
|
+
* @param nodes - every node the search might branch through
|
|
248
|
+
* @param edgesFrom - this carrier's own edge enumerator, reused to find every candidate crossing among `nodes`
|
|
249
|
+
* @param isProductionNetwork - see `CreateOmnibridgeCarrierOptions`'s own field
|
|
250
|
+
* @param clientForChain - see `CreateOmnibridgeCarrierOptions`'s own field
|
|
251
|
+
* @returns every resolved crossing's edge key, mapped to what `exact` needs — never thrown; a chain this host has no client for is simply left unanswered
|
|
252
|
+
*/
|
|
253
|
+
const prepareOmnibridgeReads = async ({ nodes, edgesFrom, isProductionNetwork, clientForChain, }) => {
|
|
254
|
+
const edges = [];
|
|
255
|
+
for (const from of nodes) {
|
|
256
|
+
for (const to of nodes) {
|
|
257
|
+
if (assetNodeKey(from) === assetNodeKey(to))
|
|
258
|
+
continue;
|
|
259
|
+
edges.push(...edgesFrom(from, to));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const resolvedCrossings = edges
|
|
263
|
+
.map((edge) => resolveCrossing(edge, isProductionNetwork))
|
|
264
|
+
.filter((resolved) => resolved !== null);
|
|
265
|
+
const preparedByEdgeKey = new Map();
|
|
266
|
+
if (resolvedCrossings.length === 0)
|
|
267
|
+
return preparedByEdgeKey;
|
|
268
|
+
const { limitsByKey, feeManagerAddressByKey } = await readStageOneBatch({ resolvedCrossings, clientForChain });
|
|
269
|
+
const feeRateByKey = await readStageTwoBatch({ resolvedCrossings, feeManagerAddressByKey, clientForChain });
|
|
270
|
+
for (const crossing of resolvedCrossings) {
|
|
271
|
+
const feeManagerAddress = feeManagerAddressByKey.get(feeManagerHostKey(crossing));
|
|
272
|
+
const feePerEther = feeManagerAddress === undefined ? null : (feeRateByKey.get(feeRateKey(crossing, feeManagerAddress)) ?? null);
|
|
273
|
+
preparedByEdgeKey.set(preparedEdgeKey(crossing.edge), {
|
|
274
|
+
limits: limitsByKey.get(limitsKey(crossing)) ?? null,
|
|
275
|
+
feePerEther,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return preparedByEdgeKey;
|
|
279
|
+
};
|
|
280
|
+
const entryAmountVerdictReason = (verdict) => verdict.reason === 'below-minimum'
|
|
281
|
+
? `below the entry mediator's minimum of ${verdict.minimumPerTransaction} base units`
|
|
282
|
+
: `above what the entry mediator has left today (${verdict.availablePerTransaction} base units)`;
|
|
283
|
+
/**
|
|
284
|
+
* Builds the omnibridge crossing as a plug-in carrier.
|
|
285
|
+
*
|
|
286
|
+
* @param options - see {@link CreateOmnibridgeCarrierOptions}
|
|
287
|
+
* @returns the carrier — `id: 'omnibridge'`; every EDGE it offers carries the
|
|
288
|
+
* real `'pulsechain'`/`'tokensex'` provider id, since one registration can
|
|
289
|
+
* speak for either entry chain depending on the pair
|
|
290
|
+
*/
|
|
291
|
+
export const createOmnibridgeCarrier = (options) => {
|
|
292
|
+
const { bridgeablePairs, isProductionNetwork, readEntryLimits, readFeePerEther, clientForChain } = options;
|
|
293
|
+
const edgeMap = buildOmnibridgeEdgeMap(bridgeablePairs, isProductionNetwork);
|
|
294
|
+
const edgesFrom = (from, to) => {
|
|
295
|
+
if (assetNodeKey(from) === assetNodeKey(to))
|
|
296
|
+
return [];
|
|
297
|
+
const carrier = edgeMap.get(directedPairKey(from, to));
|
|
298
|
+
if (carrier === undefined)
|
|
299
|
+
return [];
|
|
300
|
+
return [{ carrier, from, to, fusesNext: false, origin: 'wallet', floors: [] }];
|
|
301
|
+
};
|
|
302
|
+
// Populated by `prepare`, read by `exact` — see that method's own doc for
|
|
303
|
+
// why a mutable closure here is safe: this carrier is a client-side,
|
|
304
|
+
// one-search-at-a-time object (quote-service.md: the omnibridge crossing
|
|
305
|
+
// is "never built into" the shared, multi-tenant display-quote service),
|
|
306
|
+
// so nothing else can be mutating this map while a search reads it.
|
|
307
|
+
let preparedByEdgeKey = new Map();
|
|
308
|
+
const prepare = async (nodes) => {
|
|
309
|
+
if (clientForChain === undefined)
|
|
310
|
+
return;
|
|
311
|
+
preparedByEdgeKey = await prepareOmnibridgeReads({ nodes, edgesFrom, isProductionNetwork, clientForChain });
|
|
312
|
+
};
|
|
313
|
+
const exact = (edge, amountIn) => {
|
|
314
|
+
const prepared = preparedByEdgeKey.get(preparedEdgeKey(edge));
|
|
315
|
+
if (prepared === undefined)
|
|
316
|
+
return null;
|
|
317
|
+
const verdict = checkEntryAmount({ amount: amountIn, limits: prepared.limits });
|
|
318
|
+
if (!verdict.fits) {
|
|
319
|
+
const minimumAmount = verdict.reason === 'below-minimum' ? verdict.minimumPerTransaction : null;
|
|
320
|
+
return {
|
|
321
|
+
outcome: 'refused',
|
|
322
|
+
refusal: {
|
|
323
|
+
kind: verdict.reason === 'below-minimum' ? 'below-minimum' : 'above-available',
|
|
324
|
+
reason: entryAmountVerdictReason(verdict),
|
|
325
|
+
minimumAmount,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return { outcome: 'delivered', deliveredAmount: netDeliveredAmount(amountIn, prepared.feePerEther) };
|
|
330
|
+
};
|
|
331
|
+
const ask = async ({ edge, inputAmount }) => {
|
|
332
|
+
const limits = await readEntryLimits(edge);
|
|
333
|
+
const verdict = checkEntryAmount({ amount: inputAmount, limits });
|
|
334
|
+
if (!verdict.fits) {
|
|
335
|
+
const minimumAmount = verdict.reason === 'below-minimum' ? verdict.minimumPerTransaction : null;
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
refusal: {
|
|
339
|
+
kind: verdict.reason === 'below-minimum' ? 'below-minimum' : 'above-available',
|
|
340
|
+
reason: entryAmountVerdictReason(verdict),
|
|
341
|
+
minimumAmount,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const feePerEther = await readFeePerEther(edge);
|
|
346
|
+
const deliveredAmount = netDeliveredAmount(inputAmount, feePerEther);
|
|
347
|
+
return {
|
|
348
|
+
ok: true,
|
|
349
|
+
deliveredAmount,
|
|
350
|
+
// An exact local computation: the guaranteed figure IS the expected one.
|
|
351
|
+
minimumDeliveredAmount: deliveredAmount,
|
|
352
|
+
claimedDurationSeconds: BRIDGE_SETTLEMENT_ETA_MINUTES * 60,
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
id: 'omnibridge',
|
|
357
|
+
edgesFrom,
|
|
358
|
+
ask,
|
|
359
|
+
prepare,
|
|
360
|
+
exact,
|
|
361
|
+
isRateLimited: false,
|
|
362
|
+
// Crossing requires the reader's own wallet signature on the entry
|
|
363
|
+
// chain — never a by-hand deposit.
|
|
364
|
+
depositMode: 'wallet-signed',
|
|
365
|
+
sender: 'omnibridge',
|
|
366
|
+
settlementSeconds: BRIDGE_SETTLEMENT_ETA_MINUTES * 60,
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Wraps ANY carrier-shaped object (built-in or a host's own) with the
|
|
371
|
+
* omnibridge's OWN fuse rule: whether THIS carrier's execution, landing on
|
|
372
|
+
* arrival, also performs the omnibridge's entry call — so the two hops
|
|
373
|
+
* together cost the reader one signature instead of two.
|
|
374
|
+
*
|
|
375
|
+
* EVERY CONDITION IS A FACT THE PROVIDER'S OWN DESCRIPTOR STATES
|
|
376
|
+
* (`../../providers.ts`), never a name compared here:
|
|
377
|
+
*
|
|
378
|
+
* - it executes destination calls at all (`executesDestinationCalls` — true
|
|
379
|
+
* for LI.FI and Relay, false for NEAR Intents, which delivers to the
|
|
380
|
+
* reader's own wallet and stops);
|
|
381
|
+
* - it can do so FROM this hop's origin chain (`destinationCallOriginChainIds`
|
|
382
|
+
* — LI.FI's contract-calls endpoint is Ethereum only, so from Base or
|
|
383
|
+
* Arbitrum a LI.FI hop is an unfused delivery followed by a crossing the
|
|
384
|
+
* reader signs);
|
|
385
|
+
* - when the entry is the omnibridge's NATIVE one (the provider lands a
|
|
386
|
+
* native coin, and `wrapAndRelayTokens` is payable), it can attach value to
|
|
387
|
+
* the call (`destinationCallCarriesNativeValue`);
|
|
388
|
+
* - and the next hop really IS the omnibridge entry, landing on PulseChain
|
|
389
|
+
* (`relayTokens`/`wrapAndRelayTokens` both MINT there; any other
|
|
390
|
+
* destination is the omnibridge's OUTBOUND leg, which no arrival can make).
|
|
391
|
+
*
|
|
392
|
+
* THIS IS THE ONE PLACE `'pulsechain'`/`'tokensex'`/PulseChain'S CHAIN ID
|
|
393
|
+
* APPEAR IN A `fusesWithNext` CALLBACK — gibs's own registration
|
|
394
|
+
* (`packages/ui/src/lib/state/useRouteSearch.ts`) wraps `createLifiCarrier`/
|
|
395
|
+
* `createRelayCarrier` with this before registering them, so the generic
|
|
396
|
+
* search engine (`@gibs/quotes`'s `capabilities.ts`) never has to know what
|
|
397
|
+
* "the bridge" means.
|
|
398
|
+
*
|
|
399
|
+
* @param carrier - the carrier to wrap; its own shape is preserved, only `fusesWithNext` is added/replaced
|
|
400
|
+
* @returns the same carrier, with a `fusesWithNext` that recognizes the omnibridge entry call
|
|
401
|
+
*/
|
|
402
|
+
export const withOmnibridgeEntryFusing = (carrier) => {
|
|
403
|
+
const descriptor = findProviderById(carrier.id);
|
|
404
|
+
return {
|
|
405
|
+
...carrier,
|
|
406
|
+
fusesWithNext: (nextHop) => {
|
|
407
|
+
if (descriptor === null || !descriptor.executesDestinationCalls)
|
|
408
|
+
return false;
|
|
409
|
+
const nextHopIsOmnibridgeCrossing = nextHop.nextHopCarrier === Providers.PULSECHAIN || nextHop.nextHopCarrier === Providers.TOKENSEX;
|
|
410
|
+
if (!nextHopIsOmnibridgeCrossing || nextHop.nextHopDestinationChainId !== pulsechainChainId)
|
|
411
|
+
return false;
|
|
412
|
+
const origins = descriptor.destinationCallOriginChainIds;
|
|
413
|
+
if (origins !== 'any' && !origins.includes(nextHop.hopFrom.chainId))
|
|
414
|
+
return false;
|
|
415
|
+
const entersNative = isNativeAsset(nextHop.nextHopFrom.address, nextHop.nextHopFrom.ecosystem);
|
|
416
|
+
return !entersNative || descriptor.destinationCallCarriesNativeValue;
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
2
|
+
import type { PublicClient } from 'viem';
|
|
3
|
+
import { type AssetNode, type HopEdge } from '../search/graph.js';
|
|
4
|
+
import type { CarrierAskAnswer, CarrierExactResult } from './ask-answer.js';
|
|
5
|
+
/**
|
|
6
|
+
* One PulseX deployment a swap can execute on: the router the reader signs
|
|
7
|
+
* and the factory whose pools that router trades against. A Uniswap-V2-style
|
|
8
|
+
* router resolves every pool through its OWN factory, so one router call can
|
|
9
|
+
* only ever touch that one deployment's pools — a path can never mix them.
|
|
10
|
+
*/
|
|
11
|
+
export type PulseXVenue = {
|
|
12
|
+
readonly router: Hex;
|
|
13
|
+
readonly factory: Hex;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Every PulseX deployment on PulseChain, V1 first: `uniV2Routers` paired with
|
|
17
|
+
* `uniV2Factories` by index. Each factory is the router's own
|
|
18
|
+
* `router.factory()` read on chain — see `uniV2Factories`'s own doc in
|
|
19
|
+
* `config.ts`. A router with no recorded factory is left out, because its
|
|
20
|
+
* pools cannot be read.
|
|
21
|
+
*/
|
|
22
|
+
export declare const pulseXVenues: readonly PulseXVenue[];
|
|
23
|
+
/**
|
|
24
|
+
* The router a PulseX edge must be signed on — the one its figure was quoted
|
|
25
|
+
* against. Reads the edge's own {@link HopEdge.venue}; an edge with none is a
|
|
26
|
+
* V1 edge (see {@link defaultPulseXVenue}).
|
|
27
|
+
*
|
|
28
|
+
* @param edge - the edge to sign
|
|
29
|
+
* @returns the router, or null when the edge is not a PulseX edge or names a venue that is not a known PulseX router
|
|
30
|
+
*/
|
|
31
|
+
export declare const pulseXRouterForEdge: (edge: Pick<HopEdge, "carrier" | "venue">) => Hex | null;
|
|
32
|
+
/**
|
|
33
|
+
* The router swap path a PulseX hop from `from` to `to` prices and signs
|
|
34
|
+
* along, or null when there is no swap for PulseX to do — including when
|
|
35
|
+
* both ends resolve to the SAME pool token, which is true of PLS<->WPLS
|
|
36
|
+
* (native PLS resolves to {@link wplsAddress} via {@link poolTokenAddress},
|
|
37
|
+
* same as Wrapped PLS itself): that pair is the wrap carrier's job, never a
|
|
38
|
+
* PulseX hop, so this returns null rather than a zero-length or self-paired
|
|
39
|
+
* path.
|
|
40
|
+
*
|
|
41
|
+
* EVERY PULSEX QUOTE AND SIGNATURE IN THIS FILE MUST GO THROUGH THIS ONE
|
|
42
|
+
* FUNCTION — `pulseXCarries`, `prepare`'s pool discovery, and `exact`'s hop
|
|
43
|
+
* pricing all call it, so native PLS resolves the same way in every one of
|
|
44
|
+
* them. Before this existed, `pulseXCarries` and `prepare`/`exact` called
|
|
45
|
+
* `pulseXConversionSwapPath` directly on the raw node address: a native PLS
|
|
46
|
+
* node produced a path that LITERALLY STARTED WITH THE ZERO ADDRESS
|
|
47
|
+
* (`[zeroAddress, wplsAddress, ...]`), which `pulseXCarries` accepted (its
|
|
48
|
+
* length is still >= 2) but for which `prepare` could never find a pool —
|
|
49
|
+
* the factory has no pair for the zero address — so `exact` always answered
|
|
50
|
+
* `null` for a direct native-PLS PulseX hop, and `enumerate.ts` pruned it,
|
|
51
|
+
* leaving only the two-signature wrap-then-swap path.
|
|
52
|
+
*
|
|
53
|
+
* @param from - the hop's origin node
|
|
54
|
+
* @param to - the hop's destination node
|
|
55
|
+
* @returns the swap path, or null when there is nothing for PulseX to swap
|
|
56
|
+
*/
|
|
57
|
+
export declare const pulseXPoolPath: (from: AssetNode, to: AssetNode) => readonly Hex[] | null;
|
|
58
|
+
/** What {@link createPulseXCarrier} needs. */
|
|
59
|
+
export type CreatePulseXCarrierOptions = {
|
|
60
|
+
/**
|
|
61
|
+
* Reads PulseX's `getAmountsOut` for one PulseX edge and an input amount,
|
|
62
|
+
* in the edge's `to` token's base units. Returns null when the path has no
|
|
63
|
+
* liquidity.
|
|
64
|
+
*
|
|
65
|
+
* MUST READ THE EDGE'S OWN ROUTER ({@link pulseXRouterForEdge}). A V2 edge
|
|
66
|
+
* answered from V1 would carry V1's figure into a V2 signature.
|
|
67
|
+
*/
|
|
68
|
+
readonly readAmountOut: (edge: HopEdge, inputAmount: bigint) => Promise<bigint | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Returns a PulseChain client, for {@link Carrier.prepare}'s own batched
|
|
71
|
+
* reserve reads — never called by `ask`, which keeps using
|
|
72
|
+
* {@link readAmountOut} unchanged.
|
|
73
|
+
*
|
|
74
|
+
* OPTIONAL, ON PURPOSE — see `omnibridge.ts`'s identical `clientForChain`
|
|
75
|
+
* field for why: a host that has not wired this yet gets the carrier's
|
|
76
|
+
* original behavior, `prepare` a no-op and `exact` always `null`.
|
|
77
|
+
*
|
|
78
|
+
* @param chainId - always {@link pulsechainChainId} — PulseX runs nowhere else
|
|
79
|
+
* @returns a client, or null when this host has none
|
|
80
|
+
*/
|
|
81
|
+
readonly clientForChain?: (chainId: number) => PublicClient | null;
|
|
82
|
+
/**
|
|
83
|
+
* The PulseX routers this host can SIGN on. The carrier offers an edge on
|
|
84
|
+
* each, and only on these: an edge is always signed on the router it was
|
|
85
|
+
* quoted against ({@link pulseXRouterForEdge}), so a router the host cannot
|
|
86
|
+
* sign on must never be quoted either.
|
|
87
|
+
*
|
|
88
|
+
* Defaults to every known venue ({@link pulseXVenues}: V1 and V2).
|
|
89
|
+
*/
|
|
90
|
+
readonly routers?: readonly Hex[];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Builds the PulseX conversion swap as a plug-in carrier.
|
|
94
|
+
*
|
|
95
|
+
* @param options - see {@link CreatePulseXCarrierOptions}
|
|
96
|
+
* @returns the carrier — `id: 'PulseX'`
|
|
97
|
+
*/
|
|
98
|
+
export declare const createPulseXCarrier: (options: CreatePulseXCarrierOptions) => {
|
|
99
|
+
id: string;
|
|
100
|
+
edgesFrom: (from: AssetNode, to: AssetNode) => readonly HopEdge[];
|
|
101
|
+
ask: ({ edge, inputAmount }: {
|
|
102
|
+
edge: HopEdge;
|
|
103
|
+
inputAmount: bigint;
|
|
104
|
+
}) => Promise<CarrierAskAnswer>;
|
|
105
|
+
prepare: (nodes: readonly AssetNode[]) => Promise<void>;
|
|
106
|
+
exact: (edge: HopEdge, amountIn: bigint) => CarrierExactResult | null;
|
|
107
|
+
isRateLimited: boolean;
|
|
108
|
+
depositMode: "wallet-signed";
|
|
109
|
+
sender: "swap";
|
|
110
|
+
settlementSeconds: number;
|
|
111
|
+
};
|