@stablechain/sdk 0.1.0 → 0.1.1
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/dist/index.cjs +38 -313
- package/dist/index.d.cts +3 -6
- package/dist/index.d.ts +3 -6
- package/dist/index.js +65 -340
- package/package.json +8 -4
package/dist/index.cjs
CHANGED
|
@@ -79,9 +79,7 @@ function formatValue(v) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// src/services/
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// src/services/lifi.ts
|
|
85
83
|
|
|
86
84
|
|
|
87
85
|
|
|
@@ -103,6 +101,38 @@ function makeChain(id, rpc) {
|
|
|
103
101
|
});
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
// src/helpers/retry.ts
|
|
105
|
+
var HttpError = class extends Error {
|
|
106
|
+
constructor(message, status, retryable) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.status = status;
|
|
109
|
+
this.retryable = retryable;
|
|
110
|
+
this.name = "HttpError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function isRetryableError(err) {
|
|
114
|
+
if (err instanceof HttpError) return err.retryable;
|
|
115
|
+
return err instanceof TypeError;
|
|
116
|
+
}
|
|
117
|
+
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
118
|
+
const maxAttempts = _nullishCoalesce(opts.maxAttempts, () => ( 3));
|
|
119
|
+
const base = _nullishCoalesce(opts.baseDelayMs, () => ( 250));
|
|
120
|
+
const max = _nullishCoalesce(opts.maxDelayMs, () => ( 4e3));
|
|
121
|
+
let lastErr;
|
|
122
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
return await fn();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
lastErr = err;
|
|
127
|
+
if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
128
|
+
const delay = Math.min(max, base * 2 ** attempt);
|
|
129
|
+
const jittered = delay * (0.75 + Math.random() * 0.5);
|
|
130
|
+
await new Promise((r) => setTimeout(r, jittered));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw lastErr;
|
|
134
|
+
}
|
|
135
|
+
|
|
106
136
|
// src/types.ts
|
|
107
137
|
var Chain = /* @__PURE__ */ ((Chain2) => {
|
|
108
138
|
Chain2["Sepolia"] = "sepolia";
|
|
@@ -196,305 +226,6 @@ var CHAIN_CONFIGS = {
|
|
|
196
226
|
}
|
|
197
227
|
};
|
|
198
228
|
|
|
199
|
-
// src/services/layerzero.ts
|
|
200
|
-
var C = CHAIN_CONFIGS;
|
|
201
|
-
var BRIDGE_CHAIN_CONFIGS = {
|
|
202
|
-
// ─── Testnet ───────────────────────────────────────────────────────────────
|
|
203
|
-
["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126" },
|
|
204
|
-
["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" },
|
|
205
|
-
// ─── Mainnet ──────────────────────────────────────────────────────────────
|
|
206
|
-
["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027" },
|
|
207
|
-
["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee" },
|
|
208
|
-
["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92" },
|
|
209
|
-
["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65" },
|
|
210
|
-
["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB" },
|
|
211
|
-
["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5" },
|
|
212
|
-
["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98" }
|
|
213
|
-
};
|
|
214
|
-
var OFT_ADAPTER_ABI = [
|
|
215
|
-
{
|
|
216
|
-
name: "quoteSend",
|
|
217
|
-
type: "function",
|
|
218
|
-
inputs: [
|
|
219
|
-
{
|
|
220
|
-
name: "_sendParam",
|
|
221
|
-
type: "tuple",
|
|
222
|
-
components: [
|
|
223
|
-
{ name: "dstEid", type: "uint32" },
|
|
224
|
-
{ name: "to", type: "bytes32" },
|
|
225
|
-
{ name: "amountLD", type: "uint256" },
|
|
226
|
-
{ name: "minAmountLD", type: "uint256" },
|
|
227
|
-
{ name: "extraOptions", type: "bytes" },
|
|
228
|
-
{ name: "composeMsg", type: "bytes" },
|
|
229
|
-
{ name: "oftCmd", type: "bytes" }
|
|
230
|
-
]
|
|
231
|
-
},
|
|
232
|
-
{ name: "_payInLzToken", type: "bool" }
|
|
233
|
-
],
|
|
234
|
-
outputs: [
|
|
235
|
-
{
|
|
236
|
-
name: "msgFee",
|
|
237
|
-
type: "tuple",
|
|
238
|
-
components: [
|
|
239
|
-
{ name: "nativeFee", type: "uint256" },
|
|
240
|
-
{ name: "lzTokenFee", type: "uint256" }
|
|
241
|
-
]
|
|
242
|
-
}
|
|
243
|
-
],
|
|
244
|
-
stateMutability: "view"
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "send",
|
|
248
|
-
type: "function",
|
|
249
|
-
inputs: [
|
|
250
|
-
{
|
|
251
|
-
name: "_sendParam",
|
|
252
|
-
type: "tuple",
|
|
253
|
-
components: [
|
|
254
|
-
{ name: "dstEid", type: "uint32" },
|
|
255
|
-
{ name: "to", type: "bytes32" },
|
|
256
|
-
{ name: "amountLD", type: "uint256" },
|
|
257
|
-
{ name: "minAmountLD", type: "uint256" },
|
|
258
|
-
{ name: "extraOptions", type: "bytes" },
|
|
259
|
-
{ name: "composeMsg", type: "bytes" },
|
|
260
|
-
{ name: "oftCmd", type: "bytes" }
|
|
261
|
-
]
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: "_fee",
|
|
265
|
-
type: "tuple",
|
|
266
|
-
components: [
|
|
267
|
-
{ name: "nativeFee", type: "uint256" },
|
|
268
|
-
{ name: "lzTokenFee", type: "uint256" }
|
|
269
|
-
]
|
|
270
|
-
},
|
|
271
|
-
{ name: "_refundAddress", type: "address" }
|
|
272
|
-
],
|
|
273
|
-
outputs: [],
|
|
274
|
-
stateMutability: "payable"
|
|
275
|
-
}
|
|
276
|
-
];
|
|
277
|
-
function buildLzOptions(gasLimit = 0n) {
|
|
278
|
-
return _viem.concat.call(void 0, [
|
|
279
|
-
_viem.numberToHex.call(void 0, 3, { size: 2 }),
|
|
280
|
-
// TYPE_3 header
|
|
281
|
-
_viem.numberToHex.call(void 0, 1, { size: 1 }),
|
|
282
|
-
// Executor worker ID
|
|
283
|
-
_viem.numberToHex.call(void 0, 17, { size: 2 }),
|
|
284
|
-
// option payload length = 17 bytes
|
|
285
|
-
_viem.numberToHex.call(void 0, 1, { size: 1 }),
|
|
286
|
-
// LzReceive option type
|
|
287
|
-
_viem.numberToHex.call(void 0, gasLimit, { size: 16 })
|
|
288
|
-
// gas limit as uint128
|
|
289
|
-
]);
|
|
290
|
-
}
|
|
291
|
-
var LayerZeroService = class {
|
|
292
|
-
constructor(config) {
|
|
293
|
-
this.config = config;
|
|
294
|
-
}
|
|
295
|
-
async quoteBridge({ fromChain, toChain, amount, recipient }) {
|
|
296
|
-
const recipientAddr = _nullishCoalesce(recipient, () => ( "0x0000000000000000000000000000000000000000"));
|
|
297
|
-
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "quoteBridge");
|
|
298
|
-
const { src, dst } = this.resolveChains(fromChain, toChain);
|
|
299
|
-
const oftAdapter = src.oftAdapter;
|
|
300
|
-
const amountLD = _viem.parseUnits.call(void 0, String(amount), src.decimals);
|
|
301
|
-
try {
|
|
302
|
-
const { nativeFee, lzTokenFee } = await _viem.createPublicClient.call(void 0, {
|
|
303
|
-
chain: makeChain(src.id, src.rpc),
|
|
304
|
-
transport: _viem.http.call(void 0, src.rpc)
|
|
305
|
-
}).readContract({
|
|
306
|
-
address: oftAdapter,
|
|
307
|
-
abi: OFT_ADAPTER_ABI,
|
|
308
|
-
functionName: "quoteSend",
|
|
309
|
-
args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
|
|
310
|
-
});
|
|
311
|
-
return { nativeFee, lzTokenFee, toAmount: amount };
|
|
312
|
-
} catch (err) {
|
|
313
|
-
throw new StableQuoteError({
|
|
314
|
-
provider: "layerzero",
|
|
315
|
-
shortMessage: "quoteBridge: quoteSend call failed",
|
|
316
|
-
cause: err
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
|
|
321
|
-
const from = _nullishCoalesce(_optionalChain([this, 'access', _2 => _2.config, 'access', _3 => _3.walletClient, 'optionalAccess', _4 => _4.account, 'optionalAccess', _5 => _5.address]), () => ( _optionalChain([this, 'access', _6 => _6.config, 'access', _7 => _7.account, 'optionalAccess', _8 => _8.address])));
|
|
322
|
-
if (!from) {
|
|
323
|
-
throw new StableValidationError(
|
|
324
|
-
"signer",
|
|
325
|
-
void 0,
|
|
326
|
-
"bridge: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
const recipientAddr = _nullishCoalesce(recipient, () => ( from));
|
|
330
|
-
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
|
|
331
|
-
const { src, dst } = this.resolveChains(fromChain, toChain);
|
|
332
|
-
const oftAdapter = src.oftAdapter;
|
|
333
|
-
const srcChain = makeChain(src.id, src.rpc);
|
|
334
|
-
const wc = _nullishCoalesce(this.config.walletClient, () => ( _viem.createWalletClient.call(void 0, {
|
|
335
|
-
account: this.config.account,
|
|
336
|
-
chain: srcChain,
|
|
337
|
-
transport: _nullishCoalesce(this.config.transport, () => ( _viem.http.call(void 0, src.rpc)))
|
|
338
|
-
})));
|
|
339
|
-
const pc = _viem.createPublicClient.call(void 0, { chain: srcChain, transport: _viem.http.call(void 0, src.rpc) });
|
|
340
|
-
try {
|
|
341
|
-
await wc.switchChain({ id: src.id });
|
|
342
|
-
} catch (err) {
|
|
343
|
-
throw new StableTransactionError({
|
|
344
|
-
phase: "switch_chain",
|
|
345
|
-
shortMessage: `bridge: wallet rejected or failed to switch to chain ${src.id}`,
|
|
346
|
-
chainId: src.id,
|
|
347
|
-
cause: err
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
const amountLD = _viem.parseUnits.call(void 0, String(amount), src.decimals);
|
|
351
|
-
const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
|
|
352
|
-
const lzFee = _optionalChain([quote, 'optionalAccess', _9 => _9.nativeFee]) !== void 0 && _optionalChain([quote, 'optionalAccess', _10 => _10.lzTokenFee]) !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee } : await this.quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient: recipientAddr });
|
|
353
|
-
const { nativeFee = 0n, lzTokenFee = 0n } = lzFee;
|
|
354
|
-
const allowance = await pc.readContract({
|
|
355
|
-
address: src.usdt,
|
|
356
|
-
abi: _viem.erc20Abi,
|
|
357
|
-
functionName: "allowance",
|
|
358
|
-
args: [from, oftAdapter]
|
|
359
|
-
});
|
|
360
|
-
if (allowance < amountLD) {
|
|
361
|
-
let approveHash;
|
|
362
|
-
try {
|
|
363
|
-
approveHash = await wc.writeContract({
|
|
364
|
-
address: src.usdt,
|
|
365
|
-
abi: _viem.erc20Abi,
|
|
366
|
-
functionName: "approve",
|
|
367
|
-
args: [oftAdapter, amountLD],
|
|
368
|
-
account: wc.account,
|
|
369
|
-
chain: srcChain
|
|
370
|
-
});
|
|
371
|
-
} catch (err) {
|
|
372
|
-
throw new StableTransactionError({
|
|
373
|
-
phase: "approve",
|
|
374
|
-
shortMessage: "bridge: USDT0 approve failed",
|
|
375
|
-
chainId: src.id,
|
|
376
|
-
cause: err
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
|
|
380
|
-
if (approveReceipt.status === "reverted") {
|
|
381
|
-
throw new StableTransactionError({
|
|
382
|
-
phase: "approve",
|
|
383
|
-
shortMessage: "bridge: USDT0 approve reverted on-chain",
|
|
384
|
-
txHash: approveReceipt.transactionHash,
|
|
385
|
-
chainId: src.id
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
try {
|
|
390
|
-
const hash = await wc.writeContract({
|
|
391
|
-
address: oftAdapter,
|
|
392
|
-
abi: OFT_ADAPTER_ABI,
|
|
393
|
-
functionName: "send",
|
|
394
|
-
args: [sendParams, { nativeFee, lzTokenFee }, from],
|
|
395
|
-
value: nativeFee,
|
|
396
|
-
account: wc.account,
|
|
397
|
-
chain: srcChain
|
|
398
|
-
});
|
|
399
|
-
return { txHash: hash, toAmount: amount };
|
|
400
|
-
} catch (err) {
|
|
401
|
-
throw new StableTransactionError({
|
|
402
|
-
phase: "send",
|
|
403
|
-
shortMessage: "bridge: OFT send failed",
|
|
404
|
-
chainId: src.id,
|
|
405
|
-
cause: err
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
validateBridgeParams(params, method) {
|
|
410
|
-
if (!_viem.isAddress.call(void 0, params.recipient)) {
|
|
411
|
-
throw new StableValidationError(
|
|
412
|
-
"recipient",
|
|
413
|
-
params.recipient,
|
|
414
|
-
`${method}: invalid recipient address "${params.recipient}"`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
if (!Number.isFinite(params.amount) || params.amount <= 0) {
|
|
418
|
-
throw new StableValidationError(
|
|
419
|
-
"amount",
|
|
420
|
-
params.amount,
|
|
421
|
-
`${method}: amount must be a finite positive number`
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
if (params.fromChain === params.toChain) {
|
|
425
|
-
throw new StableValidationError(
|
|
426
|
-
"toChain",
|
|
427
|
-
params.toChain,
|
|
428
|
-
`${method}: fromChain and toChain must differ`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
resolveChains(fromChain, toChain) {
|
|
433
|
-
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
434
|
-
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
435
|
-
if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
|
|
436
|
-
if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
|
|
437
|
-
if (!src.oftAdapter) {
|
|
438
|
-
throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
|
|
439
|
-
}
|
|
440
|
-
return { src, dst };
|
|
441
|
-
}
|
|
442
|
-
buildSendParams(dst, amountLD, recipient) {
|
|
443
|
-
return {
|
|
444
|
-
dstEid: dst.lzEid,
|
|
445
|
-
to: _viem.padHex.call(void 0, recipient, { dir: "left", size: 32 }),
|
|
446
|
-
amountLD,
|
|
447
|
-
minAmountLD: amountLD,
|
|
448
|
-
extraOptions: buildLzOptions(),
|
|
449
|
-
composeMsg: "0x",
|
|
450
|
-
oftCmd: "0x"
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// src/services/lifi.ts
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// src/helpers/retry.ts
|
|
467
|
-
var HttpError = class extends Error {
|
|
468
|
-
constructor(message, status, retryable) {
|
|
469
|
-
super(message);
|
|
470
|
-
this.status = status;
|
|
471
|
-
this.retryable = retryable;
|
|
472
|
-
this.name = "HttpError";
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
function isRetryableError(err) {
|
|
476
|
-
if (err instanceof HttpError) return err.retryable;
|
|
477
|
-
return err instanceof TypeError;
|
|
478
|
-
}
|
|
479
|
-
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
480
|
-
const maxAttempts = _nullishCoalesce(opts.maxAttempts, () => ( 3));
|
|
481
|
-
const base = _nullishCoalesce(opts.baseDelayMs, () => ( 250));
|
|
482
|
-
const max = _nullishCoalesce(opts.maxDelayMs, () => ( 4e3));
|
|
483
|
-
let lastErr;
|
|
484
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
485
|
-
try {
|
|
486
|
-
return await fn();
|
|
487
|
-
} catch (err) {
|
|
488
|
-
lastErr = err;
|
|
489
|
-
if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
490
|
-
const delay = Math.min(max, base * 2 ** attempt);
|
|
491
|
-
const jittered = delay * (0.75 + Math.random() * 0.5);
|
|
492
|
-
await new Promise((r) => setTimeout(r, jittered));
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
throw lastErr;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
229
|
// src/services/lifi.ts
|
|
499
230
|
var LiFiService = class {
|
|
500
231
|
constructor(config) {
|
|
@@ -526,7 +257,7 @@ var LiFiService = class {
|
|
|
526
257
|
async quoteBridge(params) {
|
|
527
258
|
this._validateBridgeParams(params);
|
|
528
259
|
const { fromChain, toChain, fromToken, toToken, amount, fromDecimals = 6, recipient } = params;
|
|
529
|
-
const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access',
|
|
260
|
+
const from = _nullishCoalesce(_nullishCoalesce(_optionalChain([this, 'access', _2 => _2.config, 'access', _3 => _3.walletClient, 'optionalAccess', _4 => _4.account, 'optionalAccess', _5 => _5.address]), () => ( _optionalChain([this, 'access', _6 => _6.config, 'access', _7 => _7.account, 'optionalAccess', _8 => _8.address]))), () => ( "0x0000000000000000000000000000000000000000"));
|
|
530
261
|
const src = CHAIN_CONFIGS[fromChain];
|
|
531
262
|
const dst = CHAIN_CONFIGS[toChain];
|
|
532
263
|
if (!src) throw new StableValidationError("fromChain", fromChain, `LI.FI bridge: unsupported source chain "${fromChain}"`);
|
|
@@ -603,7 +334,7 @@ var LiFiService = class {
|
|
|
603
334
|
}
|
|
604
335
|
}
|
|
605
336
|
_requireFrom() {
|
|
606
|
-
const from = _nullishCoalesce(_optionalChain([this, 'access',
|
|
337
|
+
const from = _nullishCoalesce(_optionalChain([this, 'access', _9 => _9.config, 'access', _10 => _10.walletClient, 'optionalAccess', _11 => _11.account, 'optionalAccess', _12 => _12.address]), () => ( _optionalChain([this, 'access', _13 => _13.config, 'access', _14 => _14.account, 'optionalAccess', _15 => _15.address])));
|
|
607
338
|
if (!from) {
|
|
608
339
|
throw new StableValidationError(
|
|
609
340
|
"signer",
|
|
@@ -772,11 +503,6 @@ function isLiFiQuoteShape(v) {
|
|
|
772
503
|
}
|
|
773
504
|
|
|
774
505
|
// src/stable.ts
|
|
775
|
-
function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
|
|
776
|
-
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
777
|
-
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
778
|
-
return !!src && !!dst && src.lzOutbound !== false && src.usdt.toLowerCase() === fromToken.toLowerCase() && dst.usdt.toLowerCase() === toToken.toLowerCase();
|
|
779
|
-
}
|
|
780
506
|
function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
781
507
|
if (!_viem.isAddress.call(void 0, to)) {
|
|
782
508
|
throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
|
|
@@ -832,12 +558,11 @@ function createStable({
|
|
|
832
558
|
const chain = network === "mainnet" /* Mainnet */ ? _chains.stable : _chains.stableTestnet;
|
|
833
559
|
const rpc = (_nullishCoalesce(rpcOverride, () => ( chain.rpcUrls.default.http[0]))).replace(/\/$/, "");
|
|
834
560
|
const signerConfig = { account, transport, walletClient };
|
|
835
|
-
const lz = new LayerZeroService(signerConfig);
|
|
836
561
|
const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
|
|
837
562
|
async function transfer(params) {
|
|
838
563
|
validateTransferParams(params);
|
|
839
564
|
const { from, to, amount, token, tokenDecimals } = params;
|
|
840
|
-
const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess',
|
|
565
|
+
const acc = _nullishCoalesce(_nullishCoalesce(_optionalChain([walletClient, 'optionalAccess', _16 => _16.account]), () => ( account)), () => ( from));
|
|
841
566
|
const wc = _nullishCoalesce(walletClient, () => ( _viem.createWalletClient.call(void 0, {
|
|
842
567
|
account: acc,
|
|
843
568
|
chain,
|
|
@@ -867,10 +592,10 @@ function createStable({
|
|
|
867
592
|
return { txHash: receipt.transactionHash };
|
|
868
593
|
}
|
|
869
594
|
function quoteBridge(params) {
|
|
870
|
-
return
|
|
595
|
+
return lifi.quoteBridge(params);
|
|
871
596
|
}
|
|
872
597
|
function bridge(params) {
|
|
873
|
-
return
|
|
598
|
+
return lifi.bridge(params);
|
|
874
599
|
}
|
|
875
600
|
return {
|
|
876
601
|
transfer,
|
package/dist/index.d.cts
CHANGED
|
@@ -82,9 +82,6 @@ declare enum Network {
|
|
|
82
82
|
interface BridgeQuote {
|
|
83
83
|
/** Estimated output amount on the destination chain */
|
|
84
84
|
toAmount?: number;
|
|
85
|
-
/** LayerZero native fee — only present for USDT0→USDT0 routes */
|
|
86
|
-
nativeFee?: bigint;
|
|
87
|
-
lzTokenFee?: bigint;
|
|
88
85
|
}
|
|
89
86
|
interface BridgeParams {
|
|
90
87
|
fromChain: Chain;
|
|
@@ -98,7 +95,7 @@ interface BridgeParams {
|
|
|
98
95
|
fromDecimals?: number;
|
|
99
96
|
/** Recipient address — defaults to the signer */
|
|
100
97
|
recipient?: string;
|
|
101
|
-
/** Pre-fetched quote from quoteBridge() — skips the
|
|
98
|
+
/** Pre-fetched quote from quoteBridge() — skips the quote fetch when passed to bridge() */
|
|
102
99
|
quote?: BridgeQuote;
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -130,12 +127,12 @@ declare class StableValidationError extends StableError {
|
|
|
130
127
|
});
|
|
131
128
|
}
|
|
132
129
|
declare class StableQuoteError extends StableError {
|
|
133
|
-
provider:
|
|
130
|
+
provider: string;
|
|
134
131
|
httpStatus?: number;
|
|
135
132
|
providerCode?: string;
|
|
136
133
|
body?: unknown;
|
|
137
134
|
constructor(args: {
|
|
138
|
-
provider:
|
|
135
|
+
provider: string;
|
|
139
136
|
shortMessage: string;
|
|
140
137
|
httpStatus?: number;
|
|
141
138
|
providerCode?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -82,9 +82,6 @@ declare enum Network {
|
|
|
82
82
|
interface BridgeQuote {
|
|
83
83
|
/** Estimated output amount on the destination chain */
|
|
84
84
|
toAmount?: number;
|
|
85
|
-
/** LayerZero native fee — only present for USDT0→USDT0 routes */
|
|
86
|
-
nativeFee?: bigint;
|
|
87
|
-
lzTokenFee?: bigint;
|
|
88
85
|
}
|
|
89
86
|
interface BridgeParams {
|
|
90
87
|
fromChain: Chain;
|
|
@@ -98,7 +95,7 @@ interface BridgeParams {
|
|
|
98
95
|
fromDecimals?: number;
|
|
99
96
|
/** Recipient address — defaults to the signer */
|
|
100
97
|
recipient?: string;
|
|
101
|
-
/** Pre-fetched quote from quoteBridge() — skips the
|
|
98
|
+
/** Pre-fetched quote from quoteBridge() — skips the quote fetch when passed to bridge() */
|
|
102
99
|
quote?: BridgeQuote;
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -130,12 +127,12 @@ declare class StableValidationError extends StableError {
|
|
|
130
127
|
});
|
|
131
128
|
}
|
|
132
129
|
declare class StableQuoteError extends StableError {
|
|
133
|
-
provider:
|
|
130
|
+
provider: string;
|
|
134
131
|
httpStatus?: number;
|
|
135
132
|
providerCode?: string;
|
|
136
133
|
body?: unknown;
|
|
137
134
|
constructor(args: {
|
|
138
|
-
provider:
|
|
135
|
+
provider: string;
|
|
139
136
|
shortMessage: string;
|
|
140
137
|
httpStatus?: number;
|
|
141
138
|
providerCode?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// src/stable.ts
|
|
2
2
|
import {
|
|
3
|
-
createPublicClient as
|
|
4
|
-
createWalletClient as
|
|
5
|
-
erc20Abi as
|
|
6
|
-
http as
|
|
7
|
-
isAddress as
|
|
8
|
-
parseUnits as
|
|
3
|
+
createPublicClient as createPublicClient2,
|
|
4
|
+
createWalletClient as createWalletClient2,
|
|
5
|
+
erc20Abi as erc20Abi2,
|
|
6
|
+
http as http2,
|
|
7
|
+
isAddress as isAddress2,
|
|
8
|
+
parseUnits as parseUnits2
|
|
9
9
|
} from "viem";
|
|
10
10
|
import { stable as stableMainnet, stableTestnet } from "viem/chains";
|
|
11
11
|
|
|
@@ -79,16 +79,14 @@ function formatValue(v) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// src/services/
|
|
82
|
+
// src/services/lifi.ts
|
|
83
83
|
import {
|
|
84
|
-
concat,
|
|
85
84
|
createPublicClient,
|
|
86
85
|
createWalletClient,
|
|
87
86
|
erc20Abi,
|
|
87
|
+
formatUnits,
|
|
88
88
|
http,
|
|
89
89
|
isAddress,
|
|
90
|
-
numberToHex,
|
|
91
|
-
padHex,
|
|
92
90
|
parseUnits
|
|
93
91
|
} from "viem";
|
|
94
92
|
|
|
@@ -103,6 +101,38 @@ function makeChain(id, rpc) {
|
|
|
103
101
|
});
|
|
104
102
|
}
|
|
105
103
|
|
|
104
|
+
// src/helpers/retry.ts
|
|
105
|
+
var HttpError = class extends Error {
|
|
106
|
+
constructor(message, status, retryable) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.status = status;
|
|
109
|
+
this.retryable = retryable;
|
|
110
|
+
this.name = "HttpError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function isRetryableError(err) {
|
|
114
|
+
if (err instanceof HttpError) return err.retryable;
|
|
115
|
+
return err instanceof TypeError;
|
|
116
|
+
}
|
|
117
|
+
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
118
|
+
const maxAttempts = opts.maxAttempts ?? 3;
|
|
119
|
+
const base = opts.baseDelayMs ?? 250;
|
|
120
|
+
const max = opts.maxDelayMs ?? 4e3;
|
|
121
|
+
let lastErr;
|
|
122
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
return await fn();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
lastErr = err;
|
|
127
|
+
if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
128
|
+
const delay = Math.min(max, base * 2 ** attempt);
|
|
129
|
+
const jittered = delay * (0.75 + Math.random() * 0.5);
|
|
130
|
+
await new Promise((r) => setTimeout(r, jittered));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw lastErr;
|
|
134
|
+
}
|
|
135
|
+
|
|
106
136
|
// src/types.ts
|
|
107
137
|
var Chain = /* @__PURE__ */ ((Chain2) => {
|
|
108
138
|
Chain2["Sepolia"] = "sepolia";
|
|
@@ -196,305 +226,6 @@ var CHAIN_CONFIGS = {
|
|
|
196
226
|
}
|
|
197
227
|
};
|
|
198
228
|
|
|
199
|
-
// src/services/layerzero.ts
|
|
200
|
-
var C = CHAIN_CONFIGS;
|
|
201
|
-
var BRIDGE_CHAIN_CONFIGS = {
|
|
202
|
-
// ─── Testnet ───────────────────────────────────────────────────────────────
|
|
203
|
-
["sepolia" /* Sepolia */]: { ...C["sepolia" /* Sepolia */], lzEid: 40161, oftAdapter: "0xc099cD946d5efCC35A99D64E808c1430cEf08126" },
|
|
204
|
-
["stable-testnet" /* StableTestnet */]: { ...C["stable-testnet" /* StableTestnet */], lzEid: 40374, oftAdapter: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" },
|
|
205
|
-
// ─── Mainnet ──────────────────────────────────────────────────────────────
|
|
206
|
-
["stable" /* Stable */]: { ...C["stable" /* Stable */], lzEid: 30396, oftAdapter: "0xedaba024be4d87974d5aB11C6Dd586963CcCB027" },
|
|
207
|
-
["ethereum" /* Ethereum */]: { ...C["ethereum" /* Ethereum */], lzEid: 30101, oftAdapter: "0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee" },
|
|
208
|
-
["arbitrum" /* Arbitrum */]: { ...C["arbitrum" /* Arbitrum */], lzEid: 30110, oftAdapter: "0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92" },
|
|
209
|
-
["ink" /* Ink */]: { ...C["ink" /* Ink */], lzEid: 30339, oftAdapter: "0x1cB6De532588fCA4a21B7209DE7C456AF8434A65" },
|
|
210
|
-
["bera" /* Bera */]: { ...C["bera" /* Bera */], lzEid: 30362, oftAdapter: "0x3Dc96399109df5ceb2C226664A086140bD0379cB" },
|
|
211
|
-
["megaeth" /* MegaETH */]: { ...C["megaeth" /* MegaETH */], lzEid: 30398, oftAdapter: "0x9151434b16b9763660705744891fa906f660ecc5" },
|
|
212
|
-
["hyperevm" /* HyperEVM */]: { ...C["hyperevm" /* HyperEVM */], lzEid: 30367, oftAdapter: "0x904861a24F30EC96ea7CFC3bE9EA4B476d237e98" }
|
|
213
|
-
};
|
|
214
|
-
var OFT_ADAPTER_ABI = [
|
|
215
|
-
{
|
|
216
|
-
name: "quoteSend",
|
|
217
|
-
type: "function",
|
|
218
|
-
inputs: [
|
|
219
|
-
{
|
|
220
|
-
name: "_sendParam",
|
|
221
|
-
type: "tuple",
|
|
222
|
-
components: [
|
|
223
|
-
{ name: "dstEid", type: "uint32" },
|
|
224
|
-
{ name: "to", type: "bytes32" },
|
|
225
|
-
{ name: "amountLD", type: "uint256" },
|
|
226
|
-
{ name: "minAmountLD", type: "uint256" },
|
|
227
|
-
{ name: "extraOptions", type: "bytes" },
|
|
228
|
-
{ name: "composeMsg", type: "bytes" },
|
|
229
|
-
{ name: "oftCmd", type: "bytes" }
|
|
230
|
-
]
|
|
231
|
-
},
|
|
232
|
-
{ name: "_payInLzToken", type: "bool" }
|
|
233
|
-
],
|
|
234
|
-
outputs: [
|
|
235
|
-
{
|
|
236
|
-
name: "msgFee",
|
|
237
|
-
type: "tuple",
|
|
238
|
-
components: [
|
|
239
|
-
{ name: "nativeFee", type: "uint256" },
|
|
240
|
-
{ name: "lzTokenFee", type: "uint256" }
|
|
241
|
-
]
|
|
242
|
-
}
|
|
243
|
-
],
|
|
244
|
-
stateMutability: "view"
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "send",
|
|
248
|
-
type: "function",
|
|
249
|
-
inputs: [
|
|
250
|
-
{
|
|
251
|
-
name: "_sendParam",
|
|
252
|
-
type: "tuple",
|
|
253
|
-
components: [
|
|
254
|
-
{ name: "dstEid", type: "uint32" },
|
|
255
|
-
{ name: "to", type: "bytes32" },
|
|
256
|
-
{ name: "amountLD", type: "uint256" },
|
|
257
|
-
{ name: "minAmountLD", type: "uint256" },
|
|
258
|
-
{ name: "extraOptions", type: "bytes" },
|
|
259
|
-
{ name: "composeMsg", type: "bytes" },
|
|
260
|
-
{ name: "oftCmd", type: "bytes" }
|
|
261
|
-
]
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: "_fee",
|
|
265
|
-
type: "tuple",
|
|
266
|
-
components: [
|
|
267
|
-
{ name: "nativeFee", type: "uint256" },
|
|
268
|
-
{ name: "lzTokenFee", type: "uint256" }
|
|
269
|
-
]
|
|
270
|
-
},
|
|
271
|
-
{ name: "_refundAddress", type: "address" }
|
|
272
|
-
],
|
|
273
|
-
outputs: [],
|
|
274
|
-
stateMutability: "payable"
|
|
275
|
-
}
|
|
276
|
-
];
|
|
277
|
-
function buildLzOptions(gasLimit = 0n) {
|
|
278
|
-
return concat([
|
|
279
|
-
numberToHex(3, { size: 2 }),
|
|
280
|
-
// TYPE_3 header
|
|
281
|
-
numberToHex(1, { size: 1 }),
|
|
282
|
-
// Executor worker ID
|
|
283
|
-
numberToHex(17, { size: 2 }),
|
|
284
|
-
// option payload length = 17 bytes
|
|
285
|
-
numberToHex(1, { size: 1 }),
|
|
286
|
-
// LzReceive option type
|
|
287
|
-
numberToHex(gasLimit, { size: 16 })
|
|
288
|
-
// gas limit as uint128
|
|
289
|
-
]);
|
|
290
|
-
}
|
|
291
|
-
var LayerZeroService = class {
|
|
292
|
-
constructor(config) {
|
|
293
|
-
this.config = config;
|
|
294
|
-
}
|
|
295
|
-
async quoteBridge({ fromChain, toChain, amount, recipient }) {
|
|
296
|
-
const recipientAddr = recipient ?? "0x0000000000000000000000000000000000000000";
|
|
297
|
-
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "quoteBridge");
|
|
298
|
-
const { src, dst } = this.resolveChains(fromChain, toChain);
|
|
299
|
-
const oftAdapter = src.oftAdapter;
|
|
300
|
-
const amountLD = parseUnits(String(amount), src.decimals);
|
|
301
|
-
try {
|
|
302
|
-
const { nativeFee, lzTokenFee } = await createPublicClient({
|
|
303
|
-
chain: makeChain(src.id, src.rpc),
|
|
304
|
-
transport: http(src.rpc)
|
|
305
|
-
}).readContract({
|
|
306
|
-
address: oftAdapter,
|
|
307
|
-
abi: OFT_ADAPTER_ABI,
|
|
308
|
-
functionName: "quoteSend",
|
|
309
|
-
args: [this.buildSendParams(dst, amountLD, recipientAddr), false]
|
|
310
|
-
});
|
|
311
|
-
return { nativeFee, lzTokenFee, toAmount: amount };
|
|
312
|
-
} catch (err) {
|
|
313
|
-
throw new StableQuoteError({
|
|
314
|
-
provider: "layerzero",
|
|
315
|
-
shortMessage: "quoteBridge: quoteSend call failed",
|
|
316
|
-
cause: err
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async bridge({ fromChain, toChain, fromToken, toToken, amount, recipient, quote }) {
|
|
321
|
-
const from = this.config.walletClient?.account?.address ?? this.config.account?.address;
|
|
322
|
-
if (!from) {
|
|
323
|
-
throw new StableValidationError(
|
|
324
|
-
"signer",
|
|
325
|
-
void 0,
|
|
326
|
-
"bridge: requires a signer \u2014 set walletClient or account in StableConfig"
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
const recipientAddr = recipient ?? from;
|
|
330
|
-
this.validateBridgeParams({ fromChain, toChain, amount, recipient: recipientAddr }, "bridge");
|
|
331
|
-
const { src, dst } = this.resolveChains(fromChain, toChain);
|
|
332
|
-
const oftAdapter = src.oftAdapter;
|
|
333
|
-
const srcChain = makeChain(src.id, src.rpc);
|
|
334
|
-
const wc = this.config.walletClient ?? createWalletClient({
|
|
335
|
-
account: this.config.account,
|
|
336
|
-
chain: srcChain,
|
|
337
|
-
transport: this.config.transport ?? http(src.rpc)
|
|
338
|
-
});
|
|
339
|
-
const pc = createPublicClient({ chain: srcChain, transport: http(src.rpc) });
|
|
340
|
-
try {
|
|
341
|
-
await wc.switchChain({ id: src.id });
|
|
342
|
-
} catch (err) {
|
|
343
|
-
throw new StableTransactionError({
|
|
344
|
-
phase: "switch_chain",
|
|
345
|
-
shortMessage: `bridge: wallet rejected or failed to switch to chain ${src.id}`,
|
|
346
|
-
chainId: src.id,
|
|
347
|
-
cause: err
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
const amountLD = parseUnits(String(amount), src.decimals);
|
|
351
|
-
const sendParams = this.buildSendParams(dst, amountLD, recipientAddr);
|
|
352
|
-
const lzFee = quote?.nativeFee !== void 0 && quote?.lzTokenFee !== void 0 ? { nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee } : await this.quoteBridge({ fromChain, toChain, fromToken, toToken, amount, recipient: recipientAddr });
|
|
353
|
-
const { nativeFee = 0n, lzTokenFee = 0n } = lzFee;
|
|
354
|
-
const allowance = await pc.readContract({
|
|
355
|
-
address: src.usdt,
|
|
356
|
-
abi: erc20Abi,
|
|
357
|
-
functionName: "allowance",
|
|
358
|
-
args: [from, oftAdapter]
|
|
359
|
-
});
|
|
360
|
-
if (allowance < amountLD) {
|
|
361
|
-
let approveHash;
|
|
362
|
-
try {
|
|
363
|
-
approveHash = await wc.writeContract({
|
|
364
|
-
address: src.usdt,
|
|
365
|
-
abi: erc20Abi,
|
|
366
|
-
functionName: "approve",
|
|
367
|
-
args: [oftAdapter, amountLD],
|
|
368
|
-
account: wc.account,
|
|
369
|
-
chain: srcChain
|
|
370
|
-
});
|
|
371
|
-
} catch (err) {
|
|
372
|
-
throw new StableTransactionError({
|
|
373
|
-
phase: "approve",
|
|
374
|
-
shortMessage: "bridge: USDT0 approve failed",
|
|
375
|
-
chainId: src.id,
|
|
376
|
-
cause: err
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
const approveReceipt = await pc.waitForTransactionReceipt({ hash: approveHash });
|
|
380
|
-
if (approveReceipt.status === "reverted") {
|
|
381
|
-
throw new StableTransactionError({
|
|
382
|
-
phase: "approve",
|
|
383
|
-
shortMessage: "bridge: USDT0 approve reverted on-chain",
|
|
384
|
-
txHash: approveReceipt.transactionHash,
|
|
385
|
-
chainId: src.id
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
try {
|
|
390
|
-
const hash = await wc.writeContract({
|
|
391
|
-
address: oftAdapter,
|
|
392
|
-
abi: OFT_ADAPTER_ABI,
|
|
393
|
-
functionName: "send",
|
|
394
|
-
args: [sendParams, { nativeFee, lzTokenFee }, from],
|
|
395
|
-
value: nativeFee,
|
|
396
|
-
account: wc.account,
|
|
397
|
-
chain: srcChain
|
|
398
|
-
});
|
|
399
|
-
return { txHash: hash, toAmount: amount };
|
|
400
|
-
} catch (err) {
|
|
401
|
-
throw new StableTransactionError({
|
|
402
|
-
phase: "send",
|
|
403
|
-
shortMessage: "bridge: OFT send failed",
|
|
404
|
-
chainId: src.id,
|
|
405
|
-
cause: err
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
validateBridgeParams(params, method) {
|
|
410
|
-
if (!isAddress(params.recipient)) {
|
|
411
|
-
throw new StableValidationError(
|
|
412
|
-
"recipient",
|
|
413
|
-
params.recipient,
|
|
414
|
-
`${method}: invalid recipient address "${params.recipient}"`
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
if (!Number.isFinite(params.amount) || params.amount <= 0) {
|
|
418
|
-
throw new StableValidationError(
|
|
419
|
-
"amount",
|
|
420
|
-
params.amount,
|
|
421
|
-
`${method}: amount must be a finite positive number`
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
if (params.fromChain === params.toChain) {
|
|
425
|
-
throw new StableValidationError(
|
|
426
|
-
"toChain",
|
|
427
|
-
params.toChain,
|
|
428
|
-
`${method}: fromChain and toChain must differ`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
resolveChains(fromChain, toChain) {
|
|
433
|
-
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
434
|
-
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
435
|
-
if (!src) throw new StableValidationError("fromChain", fromChain, `Unsupported source chain: "${fromChain}"`);
|
|
436
|
-
if (!dst) throw new StableValidationError("toChain", toChain, `Unsupported destination chain: "${toChain}"`);
|
|
437
|
-
if (!src.oftAdapter) {
|
|
438
|
-
throw new StableValidationError("fromChain", fromChain, `No OFT adapter configured for "${fromChain}"`);
|
|
439
|
-
}
|
|
440
|
-
return { src, dst };
|
|
441
|
-
}
|
|
442
|
-
buildSendParams(dst, amountLD, recipient) {
|
|
443
|
-
return {
|
|
444
|
-
dstEid: dst.lzEid,
|
|
445
|
-
to: padHex(recipient, { dir: "left", size: 32 }),
|
|
446
|
-
amountLD,
|
|
447
|
-
minAmountLD: amountLD,
|
|
448
|
-
extraOptions: buildLzOptions(),
|
|
449
|
-
composeMsg: "0x",
|
|
450
|
-
oftCmd: "0x"
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// src/services/lifi.ts
|
|
456
|
-
import {
|
|
457
|
-
createPublicClient as createPublicClient2,
|
|
458
|
-
createWalletClient as createWalletClient2,
|
|
459
|
-
erc20Abi as erc20Abi2,
|
|
460
|
-
formatUnits,
|
|
461
|
-
http as http2,
|
|
462
|
-
isAddress as isAddress2,
|
|
463
|
-
parseUnits as parseUnits2
|
|
464
|
-
} from "viem";
|
|
465
|
-
|
|
466
|
-
// src/helpers/retry.ts
|
|
467
|
-
var HttpError = class extends Error {
|
|
468
|
-
constructor(message, status, retryable) {
|
|
469
|
-
super(message);
|
|
470
|
-
this.status = status;
|
|
471
|
-
this.retryable = retryable;
|
|
472
|
-
this.name = "HttpError";
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
function isRetryableError(err) {
|
|
476
|
-
if (err instanceof HttpError) return err.retryable;
|
|
477
|
-
return err instanceof TypeError;
|
|
478
|
-
}
|
|
479
|
-
async function retryWithBackoff(fn, isRetryable = isRetryableError, opts = {}) {
|
|
480
|
-
const maxAttempts = opts.maxAttempts ?? 3;
|
|
481
|
-
const base = opts.baseDelayMs ?? 250;
|
|
482
|
-
const max = opts.maxDelayMs ?? 4e3;
|
|
483
|
-
let lastErr;
|
|
484
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
485
|
-
try {
|
|
486
|
-
return await fn();
|
|
487
|
-
} catch (err) {
|
|
488
|
-
lastErr = err;
|
|
489
|
-
if (attempt === maxAttempts - 1 || !isRetryable(err)) throw err;
|
|
490
|
-
const delay = Math.min(max, base * 2 ** attempt);
|
|
491
|
-
const jittered = delay * (0.75 + Math.random() * 0.5);
|
|
492
|
-
await new Promise((r) => setTimeout(r, jittered));
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
throw lastErr;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
229
|
// src/services/lifi.ts
|
|
499
230
|
var LiFiService = class {
|
|
500
231
|
constructor(config) {
|
|
@@ -505,7 +236,7 @@ var LiFiService = class {
|
|
|
505
236
|
const { fromToken, toToken, amount, fromDecimals = 6, toAddress } = params;
|
|
506
237
|
const from = this._requireFrom();
|
|
507
238
|
const chain = this.config.chainId.toString();
|
|
508
|
-
const fromAmountRaw =
|
|
239
|
+
const fromAmountRaw = parseUnits(String(amount), fromDecimals).toString();
|
|
509
240
|
const qs = new URLSearchParams({
|
|
510
241
|
fromChain: chain,
|
|
511
242
|
toChain: chain,
|
|
@@ -536,7 +267,7 @@ var LiFiService = class {
|
|
|
536
267
|
toChain: dst.id.toString(),
|
|
537
268
|
fromToken,
|
|
538
269
|
toToken,
|
|
539
|
-
fromAmount:
|
|
270
|
+
fromAmount: parseUnits(String(amount), fromDecimals).toString(),
|
|
540
271
|
fromAddress: from,
|
|
541
272
|
...recipient && { toAddress: recipient }
|
|
542
273
|
});
|
|
@@ -555,7 +286,7 @@ var LiFiService = class {
|
|
|
555
286
|
if (!dst) {
|
|
556
287
|
throw new StableValidationError("toChain", toChain, `LI.FI bridge: unsupported destination chain "${toChain}"`);
|
|
557
288
|
}
|
|
558
|
-
const fromAmountRaw =
|
|
289
|
+
const fromAmountRaw = parseUnits(String(amount), fromDecimals).toString();
|
|
559
290
|
const qs = new URLSearchParams({
|
|
560
291
|
fromChain: src.id.toString(),
|
|
561
292
|
toChain: dst.id.toString(),
|
|
@@ -569,10 +300,10 @@ var LiFiService = class {
|
|
|
569
300
|
return this._executeSwapQuote(quote, src.rpc, src.id);
|
|
570
301
|
}
|
|
571
302
|
_validateSwapParams({ fromToken, toToken, amount, toAddress }) {
|
|
572
|
-
if (!
|
|
303
|
+
if (!isAddress(fromToken)) {
|
|
573
304
|
throw new StableValidationError("fromToken", fromToken, `swap: invalid fromToken address "${fromToken}"`);
|
|
574
305
|
}
|
|
575
|
-
if (!
|
|
306
|
+
if (!isAddress(toToken)) {
|
|
576
307
|
throw new StableValidationError("toToken", toToken, `swap: invalid toToken address "${toToken}"`);
|
|
577
308
|
}
|
|
578
309
|
if (fromToken.toLowerCase() === toToken.toLowerCase()) {
|
|
@@ -581,21 +312,21 @@ var LiFiService = class {
|
|
|
581
312
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
582
313
|
throw new StableValidationError("amount", amount, "swap: amount must be a finite positive number");
|
|
583
314
|
}
|
|
584
|
-
if (toAddress !== void 0 && !
|
|
315
|
+
if (toAddress !== void 0 && !isAddress(toAddress)) {
|
|
585
316
|
throw new StableValidationError("toAddress", toAddress, `swap: invalid toAddress "${toAddress}"`);
|
|
586
317
|
}
|
|
587
318
|
}
|
|
588
319
|
_validateBridgeParams({ fromChain, toChain, fromToken, toToken, amount, recipient }) {
|
|
589
|
-
if (!
|
|
320
|
+
if (!isAddress(fromToken)) {
|
|
590
321
|
throw new StableValidationError("fromToken", fromToken, `LI.FI bridge: invalid fromToken address "${fromToken}"`);
|
|
591
322
|
}
|
|
592
|
-
if (!
|
|
323
|
+
if (!isAddress(toToken)) {
|
|
593
324
|
throw new StableValidationError("toToken", toToken, `LI.FI bridge: invalid toToken address "${toToken}"`);
|
|
594
325
|
}
|
|
595
326
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
596
327
|
throw new StableValidationError("amount", amount, "LI.FI bridge: amount must be a finite positive number");
|
|
597
328
|
}
|
|
598
|
-
if (recipient !== void 0 && !
|
|
329
|
+
if (recipient !== void 0 && !isAddress(recipient)) {
|
|
599
330
|
throw new StableValidationError("recipient", recipient, `LI.FI bridge: invalid recipient address "${recipient}"`);
|
|
600
331
|
}
|
|
601
332
|
if (fromChain === toChain) {
|
|
@@ -683,10 +414,10 @@ var LiFiService = class {
|
|
|
683
414
|
});
|
|
684
415
|
}
|
|
685
416
|
const srcChain = makeChain(srcChainId, srcRpc);
|
|
686
|
-
const wc = this.config.walletClient ??
|
|
417
|
+
const wc = this.config.walletClient ?? createWalletClient({
|
|
687
418
|
account: this.config.account,
|
|
688
419
|
chain: srcChain,
|
|
689
|
-
transport: this.config.transport ??
|
|
420
|
+
transport: this.config.transport ?? http(srcRpc)
|
|
690
421
|
});
|
|
691
422
|
try {
|
|
692
423
|
await wc.switchChain({ id: srcChainId });
|
|
@@ -723,10 +454,10 @@ var LiFiService = class {
|
|
|
723
454
|
const tokenAddr = quote.fromToken;
|
|
724
455
|
const isNative = tokenAddr.toLowerCase() === "0x0000000000000000000000000000000000000000" || tokenAddr.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
|
725
456
|
if (isNative || !quote.approvalAddress) return;
|
|
726
|
-
const pc =
|
|
457
|
+
const pc = createPublicClient({ chain: srcChain, transport: http(srcRpc) });
|
|
727
458
|
const allowance = await pc.readContract({
|
|
728
459
|
address: tokenAddr,
|
|
729
|
-
abi:
|
|
460
|
+
abi: erc20Abi,
|
|
730
461
|
functionName: "allowance",
|
|
731
462
|
args: [from, quote.approvalAddress]
|
|
732
463
|
});
|
|
@@ -735,7 +466,7 @@ var LiFiService = class {
|
|
|
735
466
|
try {
|
|
736
467
|
approveHash = await wc.writeContract({
|
|
737
468
|
address: tokenAddr,
|
|
738
|
-
abi:
|
|
469
|
+
abi: erc20Abi,
|
|
739
470
|
functionName: "approve",
|
|
740
471
|
args: [quote.approvalAddress, quote.fromAmount],
|
|
741
472
|
account: wc.account,
|
|
@@ -772,19 +503,14 @@ function isLiFiQuoteShape(v) {
|
|
|
772
503
|
}
|
|
773
504
|
|
|
774
505
|
// src/stable.ts
|
|
775
|
-
function isUsdt0Route(fromChain, toChain, fromToken, toToken) {
|
|
776
|
-
const src = BRIDGE_CHAIN_CONFIGS[fromChain];
|
|
777
|
-
const dst = BRIDGE_CHAIN_CONFIGS[toChain];
|
|
778
|
-
return !!src && !!dst && src.lzOutbound !== false && src.usdt.toLowerCase() === fromToken.toLowerCase() && dst.usdt.toLowerCase() === toToken.toLowerCase();
|
|
779
|
-
}
|
|
780
506
|
function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
781
|
-
if (!
|
|
507
|
+
if (!isAddress2(to)) {
|
|
782
508
|
throw new StableValidationError("to", to, `transfer: invalid recipient address "${to}"`);
|
|
783
509
|
}
|
|
784
510
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
785
511
|
throw new StableValidationError("amount", amount, "transfer: amount must be a finite positive number");
|
|
786
512
|
}
|
|
787
|
-
if (token !== void 0 && !
|
|
513
|
+
if (token !== void 0 && !isAddress2(token)) {
|
|
788
514
|
throw new StableValidationError("token", token, `transfer: invalid token address "${token}"`);
|
|
789
515
|
}
|
|
790
516
|
if (tokenDecimals !== void 0 && (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > 77)) {
|
|
@@ -794,7 +520,7 @@ function validateTransferParams({ to, amount, token, tokenDecimals }) {
|
|
|
794
520
|
async function resolveTokenDecimals(publicClient, token, tokenDecimals) {
|
|
795
521
|
if (tokenDecimals !== void 0) return tokenDecimals;
|
|
796
522
|
try {
|
|
797
|
-
return await publicClient.readContract({ address: token, abi:
|
|
523
|
+
return await publicClient.readContract({ address: token, abi: erc20Abi2, functionName: "decimals" });
|
|
798
524
|
} catch {
|
|
799
525
|
throw new StableValidationError(
|
|
800
526
|
"tokenDecimals",
|
|
@@ -815,9 +541,9 @@ async function executeTokenTransfer(wc, publicClient, token, to, amount, tokenDe
|
|
|
815
541
|
const decimals = await resolveTokenDecimals(publicClient, token, tokenDecimals);
|
|
816
542
|
return wc.writeContract({
|
|
817
543
|
address: token,
|
|
818
|
-
abi:
|
|
544
|
+
abi: erc20Abi2,
|
|
819
545
|
functionName: "transfer",
|
|
820
|
-
args: [to,
|
|
546
|
+
args: [to, parseUnits2(String(amount), decimals)],
|
|
821
547
|
account: acc,
|
|
822
548
|
chain
|
|
823
549
|
});
|
|
@@ -832,16 +558,15 @@ function createStable({
|
|
|
832
558
|
const chain = network === "mainnet" /* Mainnet */ ? stableMainnet : stableTestnet;
|
|
833
559
|
const rpc = (rpcOverride ?? chain.rpcUrls.default.http[0]).replace(/\/$/, "");
|
|
834
560
|
const signerConfig = { account, transport, walletClient };
|
|
835
|
-
const lz = new LayerZeroService(signerConfig);
|
|
836
561
|
const lifi = new LiFiService({ ...signerConfig, rpc, chainId: chain.id });
|
|
837
562
|
async function transfer(params) {
|
|
838
563
|
validateTransferParams(params);
|
|
839
564
|
const { from, to, amount, token, tokenDecimals } = params;
|
|
840
565
|
const acc = walletClient?.account ?? account ?? from;
|
|
841
|
-
const wc = walletClient ??
|
|
566
|
+
const wc = walletClient ?? createWalletClient2({
|
|
842
567
|
account: acc,
|
|
843
568
|
chain,
|
|
844
|
-
transport: transport ??
|
|
569
|
+
transport: transport ?? http2(rpc)
|
|
845
570
|
});
|
|
846
571
|
try {
|
|
847
572
|
await wc.switchChain({ id: chain.id });
|
|
@@ -853,8 +578,8 @@ function createStable({
|
|
|
853
578
|
cause: err
|
|
854
579
|
});
|
|
855
580
|
}
|
|
856
|
-
const publicClient =
|
|
857
|
-
const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value:
|
|
581
|
+
const publicClient = createPublicClient2({ chain, transport: http2(rpc) });
|
|
582
|
+
const hash = token !== void 0 ? await executeTokenTransfer(wc, publicClient, token, to, amount, tokenDecimals, acc, chain) : await wc.sendTransaction({ account: acc, chain, to, value: parseUnits2(String(amount), 18) });
|
|
858
583
|
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
859
584
|
if (receipt.status === "reverted") {
|
|
860
585
|
throw new StableTransactionError({
|
|
@@ -867,10 +592,10 @@ function createStable({
|
|
|
867
592
|
return { txHash: receipt.transactionHash };
|
|
868
593
|
}
|
|
869
594
|
function quoteBridge(params) {
|
|
870
|
-
return
|
|
595
|
+
return lifi.quoteBridge(params);
|
|
871
596
|
}
|
|
872
597
|
function bridge(params) {
|
|
873
|
-
return
|
|
598
|
+
return lifi.bridge(params);
|
|
874
599
|
}
|
|
875
600
|
return {
|
|
876
601
|
transfer,
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stablechain/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Stable SDK",
|
|
5
5
|
"keywords": ["stable", "usdt", "gasless", "web3"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
9
|
+
"access": "public",
|
|
10
|
+
"provenance": true
|
|
10
11
|
},
|
|
11
12
|
"type": "module",
|
|
12
13
|
"main": "dist/index.cjs",
|
|
@@ -29,7 +30,9 @@
|
|
|
29
30
|
"format": "biome check --apply .",
|
|
30
31
|
"clean": "rimraf dist",
|
|
31
32
|
"build": "tsup --splitting",
|
|
32
|
-
"prepublishOnly": "bun run build"
|
|
33
|
+
"prepublishOnly": "bun run build",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest"
|
|
33
36
|
},
|
|
34
37
|
"peerDependencies": {
|
|
35
38
|
"viem": ">=2.0.0"
|
|
@@ -37,6 +40,7 @@
|
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"tsup": "^8.0.2",
|
|
39
42
|
"typescript": "^5",
|
|
40
|
-
"viem": "^2.0.0"
|
|
43
|
+
"viem": "^2.0.0",
|
|
44
|
+
"vitest": "^2.0.0"
|
|
41
45
|
}
|
|
42
46
|
}
|