@pioneer-platform/pioneer-sdk 8.12.0 → 8.12.4
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 +211 -17
- package/dist/index.es.js +211 -17
- package/dist/index.js +211 -17
- package/package.json +1 -1
- package/src/index.ts +145 -3
- package/src/txbuilder/createUnsignedEvmTx.ts +223 -28
|
@@ -104,6 +104,14 @@ async function fetchTokenPriceInUsd(pioneer: any, caip: string) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Create an unsigned EVM transaction
|
|
107
|
+
/**
|
|
108
|
+
* Create an unsigned EVM transaction
|
|
109
|
+
*
|
|
110
|
+
* @param feeLevel - Fee speed preference:
|
|
111
|
+
* - 1-2: Slow (80% of base gas price)
|
|
112
|
+
* - 3-7: Average (100% of base gas price) [DEFAULT: 5]
|
|
113
|
+
* - 8-10: Fast (150% of base gas price)
|
|
114
|
+
*/
|
|
107
115
|
export async function createUnsignedEvmTx(
|
|
108
116
|
caip,
|
|
109
117
|
to,
|
|
@@ -113,7 +121,7 @@ export async function createUnsignedEvmTx(
|
|
|
113
121
|
pioneer,
|
|
114
122
|
pubkeyContext,
|
|
115
123
|
isMax,
|
|
116
|
-
feeLevel = 5, //
|
|
124
|
+
feeLevel = 5, // Default: average fee level (100% of base)
|
|
117
125
|
) {
|
|
118
126
|
const tag = TAG + ' | createUnsignedEvmTx | ';
|
|
119
127
|
|
|
@@ -281,18 +289,20 @@ export async function createUnsignedEvmTx(
|
|
|
281
289
|
|
|
282
290
|
if (memo === ' ') memo = '';
|
|
283
291
|
|
|
292
|
+
// Check if this is a THORChain swap (memo starts with '=' or 'SWAP' or contains ':')
|
|
293
|
+
// Define this before the switch so it's available in all cases
|
|
294
|
+
const isThorchainSwap =
|
|
295
|
+
memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
|
|
296
|
+
|
|
284
297
|
// Build transaction object based on asset type
|
|
285
298
|
switch (assetType) {
|
|
286
299
|
case 'gas': {
|
|
287
|
-
//
|
|
288
|
-
const isThorchainOperation =
|
|
289
|
-
memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
|
|
290
|
-
|
|
300
|
+
// Use the top-level isThorchainSwap check
|
|
291
301
|
let gasLimit;
|
|
292
|
-
if (
|
|
293
|
-
// THORChain depositWithExpiry requires more gas (90-
|
|
294
|
-
// Use
|
|
295
|
-
gasLimit = BigInt(
|
|
302
|
+
if (isThorchainSwap) {
|
|
303
|
+
// THORChain depositWithExpiry requires more gas (90-100k typical)
|
|
304
|
+
// Use 100000 with 10% buffer for safety (reduced from 120000 to minimize gas fees)
|
|
305
|
+
gasLimit = BigInt(100000);
|
|
296
306
|
console.log(tag, 'Using higher gas limit for THORChain swap:', gasLimit.toString());
|
|
297
307
|
} else {
|
|
298
308
|
// Standard gas limit for ETH transfer
|
|
@@ -300,7 +310,7 @@ export async function createUnsignedEvmTx(
|
|
|
300
310
|
gasLimit = chainId === 1 ? BigInt(21000) : BigInt(25000);
|
|
301
311
|
}
|
|
302
312
|
|
|
303
|
-
if (memo && memo !== '' && !
|
|
313
|
+
if (memo && memo !== '' && !isThorchainSwap) {
|
|
304
314
|
const memoBytes = Buffer.from(memo, 'utf8').length;
|
|
305
315
|
gasLimit += BigInt(memoBytes) * 68n; // Approximate additional gas
|
|
306
316
|
//console.log(tag, 'Adjusted gasLimit for memo:', gasLimit.toString());
|
|
@@ -321,17 +331,37 @@ export async function createUnsignedEvmTx(
|
|
|
321
331
|
console.log(tag, 'isMax calculation - balance:', balance.toString(), 'gasFee:', gasFee.toString(), 'buffer:', buffer.toString(), 'amountWei:', amountWei.toString());
|
|
322
332
|
} else {
|
|
323
333
|
amountWei = BigInt(Math.round(amount * 1e18));
|
|
324
|
-
|
|
325
|
-
|
|
334
|
+
const totalNeeded = amountWei + gasFee;
|
|
335
|
+
|
|
336
|
+
if (totalNeeded > balance) {
|
|
337
|
+
const availableForSwap = balance > gasFee ? balance - gasFee : 0n;
|
|
338
|
+
const balanceEth = (Number(balance) / 1e18).toFixed(6);
|
|
339
|
+
const amountEth = (Number(amountWei) / 1e18).toFixed(6);
|
|
340
|
+
const gasFeeEth = (Number(gasFee) / 1e18).toFixed(6);
|
|
341
|
+
const availableEth = (Number(availableForSwap) / 1e18).toFixed(6);
|
|
342
|
+
const swapType = isThorchainSwap ? 'THORChain swap' : 'transfer';
|
|
343
|
+
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Insufficient funds for the transaction amount and gas fees.\n` +
|
|
346
|
+
`Balance: ${balanceEth} ETH\n` +
|
|
347
|
+
`Attempting to ${swapType}: ${amountEth} ETH\n` +
|
|
348
|
+
`Estimated gas fee: ${gasFeeEth} ETH (${gasLimit.toString()} gas limit)\n` +
|
|
349
|
+
`Total needed: ${(Number(totalNeeded) / 1e18).toFixed(6)} ETH\n` +
|
|
350
|
+
`Maximum swappable: ${availableEth} ETH (after gas fees)`
|
|
351
|
+
);
|
|
326
352
|
}
|
|
327
353
|
}
|
|
328
354
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
355
|
+
console.log(tag, 'Transaction calculation:', {
|
|
356
|
+
balance: balance.toString() + ' wei (' + (Number(balance) / 1e18).toFixed(6) + ' ETH)',
|
|
357
|
+
amountWei: amountWei.toString() + ' wei (' + (Number(amountWei) / 1e18).toFixed(6) + ' ETH)',
|
|
358
|
+
gasFee: gasFee.toString() + ' wei (' + (Number(gasFee) / 1e18).toFixed(6) + ' ETH)',
|
|
359
|
+
gasLimit: gasLimit.toString(),
|
|
360
|
+
gasPrice: gasPrice.toString() + ' wei (' + (Number(gasPrice) / 1e9).toFixed(2) + ' gwei)',
|
|
361
|
+
isThorchainSwap: isThorchainSwap,
|
|
362
|
+
});
|
|
334
363
|
|
|
364
|
+
// Use the top-level isThorchainSwap check (defined before switch statement)
|
|
335
365
|
let txData = '0x';
|
|
336
366
|
|
|
337
367
|
if (isThorchainSwap) {
|
|
@@ -366,19 +396,35 @@ export async function createUnsignedEvmTx(
|
|
|
366
396
|
const inboundResponse = await fetch('https://thornode.ninerealms.com/thorchain/inbound_addresses');
|
|
367
397
|
if (inboundResponse.ok) {
|
|
368
398
|
const inboundData = await inboundResponse.json();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
399
|
+
|
|
400
|
+
// Map chainId to THORChain chain name
|
|
401
|
+
const chainIdToThorchain: Record<number, string> = {
|
|
402
|
+
1: 'ETH', // Ethereum mainnet
|
|
403
|
+
43114: 'AVAX', // Avalanche
|
|
404
|
+
8453: 'BASE', // Base
|
|
405
|
+
56: 'BSC', // Binance Smart Chain
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
409
|
+
if (!thorchainName) {
|
|
410
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(tag, 'Looking for THORChain inbound for chain:', thorchainName, 'chainId:', chainId);
|
|
414
|
+
|
|
415
|
+
// Find inbound data for the specific chain
|
|
416
|
+
const chainInbound = inboundData.find(inbound =>
|
|
417
|
+
inbound.chain === thorchainName && !inbound.halted
|
|
372
418
|
);
|
|
373
|
-
if (
|
|
374
|
-
vaultAddress =
|
|
375
|
-
routerAddress =
|
|
419
|
+
if (chainInbound) {
|
|
420
|
+
vaultAddress = chainInbound.address; // This is the Asgard vault
|
|
421
|
+
routerAddress = chainInbound.router || to; // Use fetched router or fallback to 'to'
|
|
376
422
|
console.log(tag, 'Using THORChain inbound addresses - vault:', vaultAddress, 'router:', routerAddress);
|
|
377
|
-
|
|
423
|
+
|
|
378
424
|
// Update the 'to' address to be the router (in case it wasn't)
|
|
379
425
|
to = routerAddress;
|
|
380
426
|
} else {
|
|
381
|
-
throw new Error(
|
|
427
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
382
428
|
}
|
|
383
429
|
}
|
|
384
430
|
} catch (fetchError) {
|
|
@@ -392,6 +438,21 @@ export async function createUnsignedEvmTx(
|
|
|
392
438
|
throw new Error('Cannot proceed with THORChain swap - vault address is invalid (0x0)');
|
|
393
439
|
}
|
|
394
440
|
|
|
441
|
+
// CRITICAL SAFETY CHECK: Ensure we're sending to the router, not the vault
|
|
442
|
+
// This prevents the original bug where USDC was sent to a user wallet
|
|
443
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
444
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
445
|
+
console.warn(tag, '⚠️ Using fetched router address instead:', routerAddress);
|
|
446
|
+
to = routerAddress; // Override with correct router
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Validate router address format
|
|
450
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
451
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log(tag, '✅ Final validation passed - router:', to, 'vault:', vaultAddress);
|
|
455
|
+
|
|
395
456
|
// Use depositWithExpiry for better safety
|
|
396
457
|
// Function signature: depositWithExpiry(address,address,uint256,string,uint256)
|
|
397
458
|
// Function selector: 0x44bc937b
|
|
@@ -539,10 +600,143 @@ export async function createUnsignedEvmTx(
|
|
|
539
600
|
// For simplicity, we assume user has enough tokens
|
|
540
601
|
// In practice, need to check token balance
|
|
541
602
|
|
|
542
|
-
|
|
603
|
+
let data: string;
|
|
604
|
+
let finalTo: string;
|
|
605
|
+
|
|
606
|
+
// Check if this is a THORChain swap
|
|
607
|
+
if (isThorchainSwap) {
|
|
608
|
+
console.log(tag, '🔄 ERC20 THORChain swap detected - encoding depositWithExpiry');
|
|
609
|
+
|
|
610
|
+
// For ERC20 THORChain swaps, we need to:
|
|
611
|
+
// 1. Approve the router to spend tokens (done separately by user)
|
|
612
|
+
// 2. Call router.depositWithExpiry(vault, asset, amount, memo, expiry)
|
|
613
|
+
|
|
614
|
+
// Fetch router and vault addresses from THORChain (same as native ETH flow)
|
|
615
|
+
let vaultAddress = '0x0000000000000000000000000000000000000000';
|
|
616
|
+
let routerAddress = to; // The 'to' field should already be the router
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
// Fetch inbound addresses from THORChain
|
|
620
|
+
const inboundResponse = await fetch('https://thornode.ninerealms.com/thorchain/inbound_addresses');
|
|
621
|
+
if (inboundResponse.ok) {
|
|
622
|
+
const inboundData = await inboundResponse.json();
|
|
623
|
+
|
|
624
|
+
// Map chainId to THORChain chain name
|
|
625
|
+
const chainIdToThorchain: Record<number, string> = {
|
|
626
|
+
1: 'ETH', // Ethereum mainnet
|
|
627
|
+
43114: 'AVAX', // Avalanche
|
|
628
|
+
8453: 'BASE', // Base
|
|
629
|
+
56: 'BSC', // Binance Smart Chain
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
633
|
+
if (!thorchainName) {
|
|
634
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
console.log(tag, 'Looking for THORChain inbound for chain:', thorchainName, 'chainId:', chainId);
|
|
638
|
+
|
|
639
|
+
// Find inbound data for the specific chain
|
|
640
|
+
const chainInbound = inboundData.find(inbound =>
|
|
641
|
+
inbound.chain === thorchainName && !inbound.halted
|
|
642
|
+
);
|
|
643
|
+
if (chainInbound) {
|
|
644
|
+
vaultAddress = chainInbound.address; // This is the Asgard vault
|
|
645
|
+
routerAddress = chainInbound.router || to; // Use fetched router or fallback to 'to'
|
|
646
|
+
console.log(tag, 'Using THORChain inbound addresses - vault:', vaultAddress, 'router:', routerAddress);
|
|
647
|
+
|
|
648
|
+
// Update the 'to' address to be the router (in case it wasn't)
|
|
649
|
+
to = routerAddress;
|
|
650
|
+
} else {
|
|
651
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch (fetchError) {
|
|
655
|
+
console.error(tag, 'Failed to fetch inbound addresses:', fetchError);
|
|
656
|
+
// ABORT - cannot proceed without proper vault address
|
|
657
|
+
throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Final validation - never use 0x0 as vault
|
|
661
|
+
if (vaultAddress === '0x0000000000000000000000000000000000000000') {
|
|
662
|
+
throw new Error('Cannot proceed with THORChain swap - vault address is invalid (0x0)');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// CRITICAL SAFETY CHECK: Ensure we're sending to the router, not the vault
|
|
666
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
667
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
668
|
+
console.warn(tag, '⚠️ Using fetched router address instead:', routerAddress);
|
|
669
|
+
to = routerAddress; // Override with correct router
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Validate router address format
|
|
673
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
674
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
console.log(tag, '✅ Final validation passed - router:', to, 'vault:', vaultAddress);
|
|
678
|
+
console.log(tag, '✅ ERC20 THORChain swap addresses validated');
|
|
679
|
+
console.log(tag, ' Router:', to);
|
|
680
|
+
console.log(tag, ' Vault:', vaultAddress);
|
|
681
|
+
console.log(tag, ' Token:', contractAddress);
|
|
682
|
+
|
|
683
|
+
// Encode depositWithExpiry for ERC20 tokens
|
|
684
|
+
// Function signature: depositWithExpiry(address,address,uint256,string,uint256)
|
|
685
|
+
const functionSelector = '44bc937b';
|
|
686
|
+
|
|
687
|
+
// Calculate expiry time (current time + 1 hour)
|
|
688
|
+
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
689
|
+
|
|
690
|
+
// Encode parameters
|
|
691
|
+
const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0');
|
|
692
|
+
const assetPadded = contractAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0'); // ERC20 token address
|
|
693
|
+
const amountPadded = amountWei.toString(16).padStart(64, '0');
|
|
694
|
+
|
|
695
|
+
// String offset for depositWithExpiry with 5 parameters
|
|
696
|
+
// Offset must point after all 5 head words: 5 * 32 = 160 = 0xa0
|
|
697
|
+
const stringOffset = (5 * 32).toString(16).padStart(64, '0');
|
|
698
|
+
|
|
699
|
+
const expiryPadded = expiryTime.toString(16).padStart(64, '0');
|
|
700
|
+
|
|
701
|
+
// Encode memo
|
|
702
|
+
const fixedMemo = memo || '';
|
|
703
|
+
const memoBytes = Buffer.from(fixedMemo, 'utf8');
|
|
704
|
+
const memoHex = memoBytes.toString('hex');
|
|
705
|
+
const stringLength = memoBytes.length.toString(16).padStart(64, '0');
|
|
706
|
+
|
|
707
|
+
// Pad memo to 32-byte boundary
|
|
708
|
+
const paddingLength = (32 - (memoBytes.length % 32)) % 32;
|
|
709
|
+
const memoPadded = memoHex + '0'.repeat(paddingLength * 2);
|
|
710
|
+
|
|
711
|
+
// Construct transaction data
|
|
712
|
+
data = '0x' + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
|
|
713
|
+
|
|
714
|
+
// Set recipient to router (NOT token contract)
|
|
715
|
+
finalTo = to; // 'to' has been validated and updated to be the router
|
|
716
|
+
|
|
717
|
+
console.log(tag, '✅ Encoded ERC20 depositWithExpiry:', {
|
|
718
|
+
functionSelector: '0x' + functionSelector,
|
|
719
|
+
vault: vaultAddress,
|
|
720
|
+
asset: contractAddress,
|
|
721
|
+
amount: amountWei.toString(),
|
|
722
|
+
memo: fixedMemo,
|
|
723
|
+
expiry: expiryTime,
|
|
724
|
+
dataLength: data.length,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Increase gas limit for router call (more complex than simple transfer)
|
|
728
|
+
// ERC20 depositWithExpiry typically uses 150-200k gas
|
|
729
|
+
gasLimit = BigInt(200000); // Reduced from 300k - router calls with 33% buffer
|
|
730
|
+
} else {
|
|
731
|
+
// Regular ERC20 transfer (non-THORChain)
|
|
732
|
+
data = encodeTransferData(to, amountWei);
|
|
733
|
+
finalTo = contractAddress;
|
|
734
|
+
}
|
|
543
735
|
|
|
544
736
|
const ethPriceInUsd = await fetchEthPriceInUsd(pioneer, networkId);
|
|
545
|
-
|
|
737
|
+
// Recalculate gasFee in case gasLimit was updated for THORChain swap
|
|
738
|
+
const finalGasFee = gasPrice * gasLimit;
|
|
739
|
+
const gasFeeUsd = (Number(finalGasFee) / 1e18) * ethPriceInUsd;
|
|
546
740
|
|
|
547
741
|
// For token price, fetch from Pioneer API using the full CAIP
|
|
548
742
|
const tokenPriceInUsd = await fetchTokenPriceInUsd(pioneer, caip);
|
|
@@ -551,10 +745,11 @@ export async function createUnsignedEvmTx(
|
|
|
551
745
|
|
|
552
746
|
unsignedTx = {
|
|
553
747
|
chainId,
|
|
748
|
+
from: address, // Required for simulation
|
|
554
749
|
nonce: toHex(nonce),
|
|
555
750
|
gas: toHex(gasLimit),
|
|
556
751
|
gasPrice: toHex(gasPrice),
|
|
557
|
-
to:
|
|
752
|
+
to: finalTo,
|
|
558
753
|
value: '0x0',
|
|
559
754
|
data,
|
|
560
755
|
// USD estimations
|