@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
package/dist/index.js
CHANGED
|
@@ -1250,17 +1250,17 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1250
1250
|
let unsignedTx;
|
|
1251
1251
|
if (memo === " ")
|
|
1252
1252
|
memo = "";
|
|
1253
|
+
const isThorchainSwap = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1253
1254
|
switch (assetType) {
|
|
1254
1255
|
case "gas": {
|
|
1255
|
-
const isThorchainOperation = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1256
1256
|
let gasLimit;
|
|
1257
|
-
if (
|
|
1258
|
-
gasLimit = BigInt(
|
|
1257
|
+
if (isThorchainSwap) {
|
|
1258
|
+
gasLimit = BigInt(1e5);
|
|
1259
1259
|
console.log(tag, "Using higher gas limit for THORChain swap:", gasLimit.toString());
|
|
1260
1260
|
} else {
|
|
1261
1261
|
gasLimit = chainId === 1 ? BigInt(21000) : BigInt(25000);
|
|
1262
1262
|
}
|
|
1263
|
-
if (memo && memo !== "" && !
|
|
1263
|
+
if (memo && memo !== "" && !isThorchainSwap) {
|
|
1264
1264
|
const memoBytes = Buffer.from(memo, "utf8").length;
|
|
1265
1265
|
gasLimit += BigInt(memoBytes) * 68n;
|
|
1266
1266
|
}
|
|
@@ -1275,11 +1275,30 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1275
1275
|
console.log(tag, "isMax calculation - balance:", balance.toString(), "gasFee:", gasFee.toString(), "buffer:", buffer.toString(), "amountWei:", amountWei.toString());
|
|
1276
1276
|
} else {
|
|
1277
1277
|
amountWei = BigInt(Math.round(amount * 1000000000000000000));
|
|
1278
|
-
|
|
1279
|
-
|
|
1278
|
+
const totalNeeded = amountWei + gasFee;
|
|
1279
|
+
if (totalNeeded > balance) {
|
|
1280
|
+
const availableForSwap = balance > gasFee ? balance - gasFee : 0n;
|
|
1281
|
+
const balanceEth2 = (Number(balance) / 1000000000000000000).toFixed(6);
|
|
1282
|
+
const amountEth = (Number(amountWei) / 1000000000000000000).toFixed(6);
|
|
1283
|
+
const gasFeeEth = (Number(gasFee) / 1000000000000000000).toFixed(6);
|
|
1284
|
+
const availableEth = (Number(availableForSwap) / 1000000000000000000).toFixed(6);
|
|
1285
|
+
const swapType = isThorchainSwap ? "THORChain swap" : "transfer";
|
|
1286
|
+
throw new Error(`Insufficient funds for the transaction amount and gas fees.
|
|
1287
|
+
` + `Balance: ${balanceEth2} ETH
|
|
1288
|
+
` + `Attempting to ${swapType}: ${amountEth} ETH
|
|
1289
|
+
` + `Estimated gas fee: ${gasFeeEth} ETH (${gasLimit.toString()} gas limit)
|
|
1290
|
+
` + `Total needed: ${(Number(totalNeeded) / 1000000000000000000).toFixed(6)} ETH
|
|
1291
|
+
` + `Maximum swappable: ${availableEth} ETH (after gas fees)`);
|
|
1280
1292
|
}
|
|
1281
1293
|
}
|
|
1282
|
-
|
|
1294
|
+
console.log(tag, "Transaction calculation:", {
|
|
1295
|
+
balance: balance.toString() + " wei (" + (Number(balance) / 1000000000000000000).toFixed(6) + " ETH)",
|
|
1296
|
+
amountWei: amountWei.toString() + " wei (" + (Number(amountWei) / 1000000000000000000).toFixed(6) + " ETH)",
|
|
1297
|
+
gasFee: gasFee.toString() + " wei (" + (Number(gasFee) / 1000000000000000000).toFixed(6) + " ETH)",
|
|
1298
|
+
gasLimit: gasLimit.toString(),
|
|
1299
|
+
gasPrice: gasPrice.toString() + " wei (" + (Number(gasPrice) / 1e9).toFixed(2) + " gwei)",
|
|
1300
|
+
isThorchainSwap
|
|
1301
|
+
});
|
|
1283
1302
|
let txData = "0x";
|
|
1284
1303
|
if (isThorchainSwap) {
|
|
1285
1304
|
console.log(tag, "Detected THORChain swap, encoding deposit data for memo:", memo);
|
|
@@ -1301,14 +1320,25 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1301
1320
|
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1302
1321
|
if (inboundResponse.ok) {
|
|
1303
1322
|
const inboundData = await inboundResponse.json();
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1323
|
+
const chainIdToThorchain = {
|
|
1324
|
+
1: "ETH",
|
|
1325
|
+
43114: "AVAX",
|
|
1326
|
+
8453: "BASE",
|
|
1327
|
+
56: "BSC"
|
|
1328
|
+
};
|
|
1329
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1330
|
+
if (!thorchainName) {
|
|
1331
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1332
|
+
}
|
|
1333
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1334
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1335
|
+
if (chainInbound) {
|
|
1336
|
+
vaultAddress = chainInbound.address;
|
|
1337
|
+
routerAddress = chainInbound.router || to;
|
|
1308
1338
|
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1309
1339
|
to = routerAddress;
|
|
1310
1340
|
} else {
|
|
1311
|
-
throw new Error(
|
|
1341
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1312
1342
|
}
|
|
1313
1343
|
}
|
|
1314
1344
|
} catch (fetchError) {
|
|
@@ -1318,6 +1348,15 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1318
1348
|
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1319
1349
|
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1320
1350
|
}
|
|
1351
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1352
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1353
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1354
|
+
to = routerAddress;
|
|
1355
|
+
}
|
|
1356
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1357
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1358
|
+
}
|
|
1359
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1321
1360
|
const functionSelector = "44bc937b";
|
|
1322
1361
|
const assetAddress = "0x0000000000000000000000000000000000000000";
|
|
1323
1362
|
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
@@ -1406,17 +1445,98 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1406
1445
|
if (estimatedGasFee > balance) {
|
|
1407
1446
|
throw new Error("Insufficient ETH balance to cover gas fees");
|
|
1408
1447
|
}
|
|
1409
|
-
|
|
1448
|
+
let data;
|
|
1449
|
+
let finalTo;
|
|
1450
|
+
if (isThorchainSwap) {
|
|
1451
|
+
console.log(tag, "\uD83D\uDD04 ERC20 THORChain swap detected - encoding depositWithExpiry");
|
|
1452
|
+
let vaultAddress = "0x0000000000000000000000000000000000000000";
|
|
1453
|
+
let routerAddress = to;
|
|
1454
|
+
try {
|
|
1455
|
+
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1456
|
+
if (inboundResponse.ok) {
|
|
1457
|
+
const inboundData = await inboundResponse.json();
|
|
1458
|
+
const chainIdToThorchain = {
|
|
1459
|
+
1: "ETH",
|
|
1460
|
+
43114: "AVAX",
|
|
1461
|
+
8453: "BASE",
|
|
1462
|
+
56: "BSC"
|
|
1463
|
+
};
|
|
1464
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1465
|
+
if (!thorchainName) {
|
|
1466
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1467
|
+
}
|
|
1468
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1469
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1470
|
+
if (chainInbound) {
|
|
1471
|
+
vaultAddress = chainInbound.address;
|
|
1472
|
+
routerAddress = chainInbound.router || to;
|
|
1473
|
+
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1474
|
+
to = routerAddress;
|
|
1475
|
+
} else {
|
|
1476
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
} catch (fetchError) {
|
|
1480
|
+
console.error(tag, "Failed to fetch inbound addresses:", fetchError);
|
|
1481
|
+
throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
|
|
1482
|
+
}
|
|
1483
|
+
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1484
|
+
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1485
|
+
}
|
|
1486
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1487
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1488
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1489
|
+
to = routerAddress;
|
|
1490
|
+
}
|
|
1491
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1492
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1493
|
+
}
|
|
1494
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1495
|
+
console.log(tag, "✅ ERC20 THORChain swap addresses validated");
|
|
1496
|
+
console.log(tag, " Router:", to);
|
|
1497
|
+
console.log(tag, " Vault:", vaultAddress);
|
|
1498
|
+
console.log(tag, " Token:", contractAddress);
|
|
1499
|
+
const functionSelector = "44bc937b";
|
|
1500
|
+
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
1501
|
+
const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1502
|
+
const assetPadded = contractAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1503
|
+
const amountPadded = amountWei.toString(16).padStart(64, "0");
|
|
1504
|
+
const stringOffset = (5 * 32).toString(16).padStart(64, "0");
|
|
1505
|
+
const expiryPadded = expiryTime.toString(16).padStart(64, "0");
|
|
1506
|
+
const fixedMemo = memo || "";
|
|
1507
|
+
const memoBytes = Buffer.from(fixedMemo, "utf8");
|
|
1508
|
+
const memoHex = memoBytes.toString("hex");
|
|
1509
|
+
const stringLength = memoBytes.length.toString(16).padStart(64, "0");
|
|
1510
|
+
const paddingLength = (32 - memoBytes.length % 32) % 32;
|
|
1511
|
+
const memoPadded = memoHex + "0".repeat(paddingLength * 2);
|
|
1512
|
+
data = "0x" + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
|
|
1513
|
+
finalTo = to;
|
|
1514
|
+
console.log(tag, "✅ Encoded ERC20 depositWithExpiry:", {
|
|
1515
|
+
functionSelector: "0x" + functionSelector,
|
|
1516
|
+
vault: vaultAddress,
|
|
1517
|
+
asset: contractAddress,
|
|
1518
|
+
amount: amountWei.toString(),
|
|
1519
|
+
memo: fixedMemo,
|
|
1520
|
+
expiry: expiryTime,
|
|
1521
|
+
dataLength: data.length
|
|
1522
|
+
});
|
|
1523
|
+
gasLimit = BigInt(200000);
|
|
1524
|
+
} else {
|
|
1525
|
+
data = encodeTransferData(to, amountWei);
|
|
1526
|
+
finalTo = contractAddress;
|
|
1527
|
+
}
|
|
1410
1528
|
const ethPriceInUsd = await fetchEthPriceInUsd(pioneer, networkId);
|
|
1411
|
-
const
|
|
1529
|
+
const finalGasFee = gasPrice * gasLimit;
|
|
1530
|
+
const gasFeeUsd = Number(finalGasFee) / 1000000000000000000 * ethPriceInUsd;
|
|
1412
1531
|
const tokenPriceInUsd = await fetchTokenPriceInUsd(pioneer, caip);
|
|
1413
1532
|
const amountUsd = Number(amountWei) / Number(tokenMultiplier) * tokenPriceInUsd;
|
|
1414
1533
|
unsignedTx = {
|
|
1415
1534
|
chainId,
|
|
1535
|
+
from: address,
|
|
1416
1536
|
nonce: toHex(nonce),
|
|
1417
1537
|
gas: toHex(gasLimit),
|
|
1418
1538
|
gasPrice: toHex(gasPrice),
|
|
1419
|
-
to:
|
|
1539
|
+
to: finalTo,
|
|
1420
1540
|
value: "0x0",
|
|
1421
1541
|
data,
|
|
1422
1542
|
gasFeeUsd,
|
|
@@ -4276,8 +4396,58 @@ class SDK {
|
|
|
4276
4396
|
if (tx.type === "deposit") {
|
|
4277
4397
|
unsignedTx = await createUnsignedTendermintTx(caip, tx.type, tx.txParams.amount, tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false, undefined);
|
|
4278
4398
|
} else if (tx.type === "EVM" || tx.type === "evm") {
|
|
4279
|
-
console.log(tag, "
|
|
4280
|
-
unsignedTx = tx.txParams;
|
|
4399
|
+
console.log(tag, "Building EVM swap transaction with createUnsignedEvmTx");
|
|
4400
|
+
unsignedTx = await createUnsignedEvmTx(caip, tx.txParams.recipientAddress, parseFloat(tx.txParams.amount), tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false);
|
|
4401
|
+
console.log(tag, "✅ Built complete EVM transaction:", {
|
|
4402
|
+
to: unsignedTx.to,
|
|
4403
|
+
from: this.pubkeyContext?.address,
|
|
4404
|
+
value: unsignedTx.value,
|
|
4405
|
+
chainId: unsignedTx.chainId,
|
|
4406
|
+
hasData: !!unsignedTx.data
|
|
4407
|
+
});
|
|
4408
|
+
console.log(tag, "\uD83D\uDEE1️ Running Tenderly simulation for EVM swap...");
|
|
4409
|
+
try {
|
|
4410
|
+
const insightResult = await this.pioneer.Insight({
|
|
4411
|
+
tx: unsignedTx,
|
|
4412
|
+
source: "swap",
|
|
4413
|
+
isThorchainSwap: tx.txParams.isThorchainSwap || false
|
|
4414
|
+
});
|
|
4415
|
+
const insight = insightResult.body;
|
|
4416
|
+
console.log(tag, "Simulation result:", insight?.simulation);
|
|
4417
|
+
if (!insight || !insight.simulation) {
|
|
4418
|
+
console.warn(tag, "⚠️ WARNING: Tenderly simulation unavailable - proceeding without validation");
|
|
4419
|
+
} else if (!insight.simulation.success) {
|
|
4420
|
+
console.warn(tag, `⚠️ WARNING: Swap simulation FAILED - ${insight.simulation.error || "Transaction may revert"}`);
|
|
4421
|
+
console.warn(tag, "⚠️ Proceeding anyway - USE CAUTION");
|
|
4422
|
+
} else {
|
|
4423
|
+
if (tx.txParams.isThorchainSwap) {
|
|
4424
|
+
console.log(tag, "\uD83D\uDD0D Verifying THORChain swap parameters...");
|
|
4425
|
+
const method = insight.simulation.method;
|
|
4426
|
+
if (!method || !method.toLowerCase().includes("deposit")) {
|
|
4427
|
+
throw new Error(`❌ CRITICAL: Invalid THORChain swap method: ${method} - expected depositWithExpiry`);
|
|
4428
|
+
}
|
|
4429
|
+
const routerAddress = unsignedTx.to;
|
|
4430
|
+
const vaultAddress = tx.txParams.vaultAddress;
|
|
4431
|
+
if (routerAddress && vaultAddress) {
|
|
4432
|
+
if (routerAddress.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
4433
|
+
throw new Error(`❌ CRITICAL: Sending directly to vault ${vaultAddress} instead of router!`);
|
|
4434
|
+
}
|
|
4435
|
+
console.log(tag, `✅ Router: ${routerAddress}`);
|
|
4436
|
+
console.log(tag, `✅ Vault: ${vaultAddress}`);
|
|
4437
|
+
}
|
|
4438
|
+
if (insight.simulation.addresses && insight.simulation.addresses.length > 0) {
|
|
4439
|
+
console.log(tag, `✅ Addresses involved: ${insight.simulation.addresses.length}`);
|
|
4440
|
+
} else {
|
|
4441
|
+
console.log(tag, "⚠️ WARNING: No addresses detected in simulation");
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
console.log(tag, `✅ Simulation PASSED - Gas used: ${insight.simulation.gasUsed}`);
|
|
4445
|
+
console.log(tag, `✅ Method: ${insight.simulation.method}`);
|
|
4446
|
+
}
|
|
4447
|
+
} catch (e) {
|
|
4448
|
+
console.error(tag, "❌ Simulation validation failed:", e.message);
|
|
4449
|
+
throw new Error(`Swap blocked by simulation failure: ${e.message}`);
|
|
4450
|
+
}
|
|
4281
4451
|
} else {
|
|
4282
4452
|
if (!tx.txParams.memo)
|
|
4283
4453
|
throw Error("memo required on swaps!");
|
|
@@ -5147,6 +5317,30 @@ class SDK {
|
|
|
5147
5317
|
}
|
|
5148
5318
|
};
|
|
5149
5319
|
}
|
|
5320
|
+
CheckERC20Allowance = async (params) => {
|
|
5321
|
+
const tag = TAG9 + " | CheckERC20Allowance | ";
|
|
5322
|
+
try {
|
|
5323
|
+
console.log(tag, "Checking ERC20 allowance:", params);
|
|
5324
|
+
const result = await this.pioneer.GetTokenAllowance(params);
|
|
5325
|
+
console.log(tag, "Allowance result:", result);
|
|
5326
|
+
return result.data;
|
|
5327
|
+
} catch (e) {
|
|
5328
|
+
console.error(tag, "Error checking ERC20 allowance:", e);
|
|
5329
|
+
throw e;
|
|
5330
|
+
}
|
|
5331
|
+
};
|
|
5332
|
+
BuildERC20ApprovalTx = async (params) => {
|
|
5333
|
+
const tag = TAG9 + " | BuildERC20ApprovalTx | ";
|
|
5334
|
+
try {
|
|
5335
|
+
console.log(tag, "Building ERC20 approval transaction:", params);
|
|
5336
|
+
const result = await this.pioneer.BuildApprovalTransaction(params);
|
|
5337
|
+
console.log(tag, "Approval tx built:", result);
|
|
5338
|
+
return result.data;
|
|
5339
|
+
} catch (e) {
|
|
5340
|
+
console.error(tag, "Error building approval transaction:", e);
|
|
5341
|
+
throw e;
|
|
5342
|
+
}
|
|
5343
|
+
};
|
|
5150
5344
|
}
|
|
5151
5345
|
var src_default = SDK;
|
|
5152
5346
|
export {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { optimizedGetPubkeys } from './kkapi-batch-client.js';
|
|
|
13
13
|
import { OfflineClient } from './offline-client.js';
|
|
14
14
|
import { TransactionManager } from './TransactionManager.js';
|
|
15
15
|
import { createUnsignedTendermintTx } from './txbuilder/createUnsignedTendermintTx.js';
|
|
16
|
+
import { createUnsignedEvmTx } from './txbuilder/createUnsignedEvmTx.js';
|
|
16
17
|
import { createUnsignedStakingTx, type StakingTxParams } from './txbuilder/createUnsignedStakingTx.js';
|
|
17
18
|
import { getFees, estimateTransactionFee, type NormalizedFeeRates, type FeeEstimate } from './fees/index.js';
|
|
18
19
|
// Utils
|
|
@@ -1303,9 +1304,101 @@ export class SDK {
|
|
|
1303
1304
|
undefined,
|
|
1304
1305
|
);
|
|
1305
1306
|
} else if (tx.type === 'EVM' || tx.type === 'evm') {
|
|
1306
|
-
//EVM transaction -
|
|
1307
|
-
console.log(tag, '
|
|
1308
|
-
|
|
1307
|
+
//EVM transaction - build complete transaction using createUnsignedEvmTx
|
|
1308
|
+
console.log(tag, 'Building EVM swap transaction with createUnsignedEvmTx');
|
|
1309
|
+
|
|
1310
|
+
// The THORChain integration provides:
|
|
1311
|
+
// - recipientAddress: router address (where to send the transaction)
|
|
1312
|
+
// - amount: amount to swap
|
|
1313
|
+
// - memo: THORChain swap memo
|
|
1314
|
+
// - vaultAddress: vault address (encoded in the transaction data by createUnsignedEvmTx)
|
|
1315
|
+
// - isThorchainSwap: flag indicating this is a THORChain swap
|
|
1316
|
+
|
|
1317
|
+
unsignedTx = await createUnsignedEvmTx(
|
|
1318
|
+
caip,
|
|
1319
|
+
tx.txParams.recipientAddress, // Router address from THORChain
|
|
1320
|
+
parseFloat(tx.txParams.amount),
|
|
1321
|
+
tx.txParams.memo,
|
|
1322
|
+
this.pubkeys,
|
|
1323
|
+
this.pioneer,
|
|
1324
|
+
this.pubkeyContext,
|
|
1325
|
+
false, // isMax
|
|
1326
|
+
);
|
|
1327
|
+
|
|
1328
|
+
console.log(tag, '✅ Built complete EVM transaction:', {
|
|
1329
|
+
to: unsignedTx.to,
|
|
1330
|
+
from: this.pubkeyContext?.address,
|
|
1331
|
+
value: unsignedTx.value,
|
|
1332
|
+
chainId: unsignedTx.chainId,
|
|
1333
|
+
hasData: !!unsignedTx.data,
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// CRITICAL: Simulate EVM swap before signing
|
|
1337
|
+
console.log(tag, '🛡️ Running Tenderly simulation for EVM swap...');
|
|
1338
|
+
try {
|
|
1339
|
+
// Call insight API for simulation using proper Swagger operation
|
|
1340
|
+
const insightResult = await this.pioneer.Insight({
|
|
1341
|
+
tx: unsignedTx,
|
|
1342
|
+
source: 'swap',
|
|
1343
|
+
isThorchainSwap: tx.txParams.isThorchainSwap || false,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
const insight = insightResult.body;
|
|
1347
|
+
console.log(tag, 'Simulation result:', insight?.simulation);
|
|
1348
|
+
|
|
1349
|
+
// WARN if Tenderly is offline or unavailable (but don't block)
|
|
1350
|
+
if (!insight || !insight.simulation) {
|
|
1351
|
+
console.warn(tag, '⚠️ WARNING: Tenderly simulation unavailable - proceeding without validation');
|
|
1352
|
+
} else if (!insight.simulation.success) {
|
|
1353
|
+
// WARN if simulation failed (but don't block)
|
|
1354
|
+
console.warn(
|
|
1355
|
+
tag,
|
|
1356
|
+
`⚠️ WARNING: Swap simulation FAILED - ${insight.simulation.error || 'Transaction may revert'}`,
|
|
1357
|
+
);
|
|
1358
|
+
console.warn(tag, '⚠️ Proceeding anyway - USE CAUTION');
|
|
1359
|
+
} else {
|
|
1360
|
+
|
|
1361
|
+
// Verify swap parameters for THORChain
|
|
1362
|
+
if (tx.txParams.isThorchainSwap) {
|
|
1363
|
+
console.log(tag, '🔍 Verifying THORChain swap parameters...');
|
|
1364
|
+
|
|
1365
|
+
// Verify method is depositWithExpiry or similar
|
|
1366
|
+
const method = insight.simulation.method;
|
|
1367
|
+
if (!method || !method.toLowerCase().includes('deposit')) {
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`❌ CRITICAL: Invalid THORChain swap method: ${method} - expected depositWithExpiry`,
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Verify router address is being called (not vault directly)
|
|
1374
|
+
const routerAddress = unsignedTx.to;
|
|
1375
|
+
const vaultAddress = tx.txParams.vaultAddress;
|
|
1376
|
+
|
|
1377
|
+
if (routerAddress && vaultAddress) {
|
|
1378
|
+
if (routerAddress.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`❌ CRITICAL: Sending directly to vault ${vaultAddress} instead of router!`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
console.log(tag, `✅ Router: ${routerAddress}`);
|
|
1384
|
+
console.log(tag, `✅ Vault: ${vaultAddress}`);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Verify addresses involved in simulation
|
|
1388
|
+
if (insight.simulation.addresses && insight.simulation.addresses.length > 0) {
|
|
1389
|
+
console.log(tag, `✅ Addresses involved: ${insight.simulation.addresses.length}`);
|
|
1390
|
+
} else {
|
|
1391
|
+
console.log(tag, '⚠️ WARNING: No addresses detected in simulation');
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
console.log(tag, `✅ Simulation PASSED - Gas used: ${insight.simulation.gasUsed}`);
|
|
1396
|
+
console.log(tag, `✅ Method: ${insight.simulation.method}`);
|
|
1397
|
+
}
|
|
1398
|
+
} catch (e: any) {
|
|
1399
|
+
console.error(tag, '❌ Simulation validation failed:', e.message);
|
|
1400
|
+
throw new Error(`Swap blocked by simulation failure: ${e.message}`);
|
|
1401
|
+
}
|
|
1309
1402
|
} else {
|
|
1310
1403
|
//transfer transaction (UTXO chains) - requires memo
|
|
1311
1404
|
if (!tx.txParams.memo) throw Error('memo required on swaps!');
|
|
@@ -2531,6 +2624,55 @@ export class SDK {
|
|
|
2531
2624
|
}
|
|
2532
2625
|
};
|
|
2533
2626
|
}
|
|
2627
|
+
|
|
2628
|
+
/**
|
|
2629
|
+
* Check ERC20 token allowance for a spender
|
|
2630
|
+
* Used to verify if approval is needed before THORChain swaps
|
|
2631
|
+
*/
|
|
2632
|
+
public CheckERC20Allowance = async (params: {
|
|
2633
|
+
networkId: string;
|
|
2634
|
+
contractAddress: string;
|
|
2635
|
+
ownerAddress: string;
|
|
2636
|
+
spenderAddress: string;
|
|
2637
|
+
}) => {
|
|
2638
|
+
const tag = TAG + ' | CheckERC20Allowance | ';
|
|
2639
|
+
try {
|
|
2640
|
+
console.log(tag, 'Checking ERC20 allowance:', params);
|
|
2641
|
+
|
|
2642
|
+
const result = await this.pioneer.GetTokenAllowance(params);
|
|
2643
|
+
|
|
2644
|
+
console.log(tag, 'Allowance result:', result);
|
|
2645
|
+
return result.data;
|
|
2646
|
+
} catch (e) {
|
|
2647
|
+
console.error(tag, 'Error checking ERC20 allowance:', e);
|
|
2648
|
+
throw e;
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2652
|
+
/**
|
|
2653
|
+
* Build an ERC20 approval transaction
|
|
2654
|
+
* Required before THORChain router can transfer tokens
|
|
2655
|
+
*/
|
|
2656
|
+
public BuildERC20ApprovalTx = async (params: {
|
|
2657
|
+
networkId: string;
|
|
2658
|
+
contractAddress: string;
|
|
2659
|
+
spenderAddress: string;
|
|
2660
|
+
ownerAddress: string;
|
|
2661
|
+
amount: string;
|
|
2662
|
+
}) => {
|
|
2663
|
+
const tag = TAG + ' | BuildERC20ApprovalTx | ';
|
|
2664
|
+
try {
|
|
2665
|
+
console.log(tag, 'Building ERC20 approval transaction:', params);
|
|
2666
|
+
|
|
2667
|
+
const result = await this.pioneer.BuildApprovalTransaction(params);
|
|
2668
|
+
|
|
2669
|
+
console.log(tag, 'Approval tx built:', result);
|
|
2670
|
+
return result.data;
|
|
2671
|
+
} catch (e) {
|
|
2672
|
+
console.error(tag, 'Error building approval transaction:', e);
|
|
2673
|
+
throw e;
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2534
2676
|
}
|
|
2535
2677
|
|
|
2536
2678
|
// Export fee-related types for consumers
|