@pioneer-platform/pioneer-sdk 8.11.28 → 8.12.2
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 +164 -14
- package/dist/index.es.js +164 -14
- package/dist/index.js +164 -14
- package/package.json +3 -3
- package/src/index.ts +96 -3
- package/src/txbuilder/createUnsignedEvmTx.ts +184 -21
package/dist/index.cjs
CHANGED
|
@@ -1074,17 +1074,17 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1074
1074
|
let unsignedTx;
|
|
1075
1075
|
if (memo === " ")
|
|
1076
1076
|
memo = "";
|
|
1077
|
+
const isThorchainSwap = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1077
1078
|
switch (assetType) {
|
|
1078
1079
|
case "gas": {
|
|
1079
|
-
const isThorchainOperation = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1080
1080
|
let gasLimit;
|
|
1081
|
-
if (
|
|
1081
|
+
if (isThorchainSwap) {
|
|
1082
1082
|
gasLimit = BigInt(120000);
|
|
1083
1083
|
console.log(tag, "Using higher gas limit for THORChain swap:", gasLimit.toString());
|
|
1084
1084
|
} else {
|
|
1085
1085
|
gasLimit = chainId === 1 ? BigInt(21000) : BigInt(25000);
|
|
1086
1086
|
}
|
|
1087
|
-
if (memo && memo !== "" && !
|
|
1087
|
+
if (memo && memo !== "" && !isThorchainSwap) {
|
|
1088
1088
|
const memoBytes = Buffer.from(memo, "utf8").length;
|
|
1089
1089
|
gasLimit += BigInt(memoBytes) * 68n;
|
|
1090
1090
|
}
|
|
@@ -1103,7 +1103,6 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1103
1103
|
throw new Error("Insufficient funds for the transaction amount and gas fees");
|
|
1104
1104
|
}
|
|
1105
1105
|
}
|
|
1106
|
-
const isThorchainSwap = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1107
1106
|
let txData = "0x";
|
|
1108
1107
|
if (isThorchainSwap) {
|
|
1109
1108
|
console.log(tag, "Detected THORChain swap, encoding deposit data for memo:", memo);
|
|
@@ -1125,14 +1124,25 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1125
1124
|
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1126
1125
|
if (inboundResponse.ok) {
|
|
1127
1126
|
const inboundData = await inboundResponse.json();
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1127
|
+
const chainIdToThorchain = {
|
|
1128
|
+
1: "ETH",
|
|
1129
|
+
43114: "AVAX",
|
|
1130
|
+
8453: "BASE",
|
|
1131
|
+
56: "BSC"
|
|
1132
|
+
};
|
|
1133
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1134
|
+
if (!thorchainName) {
|
|
1135
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1136
|
+
}
|
|
1137
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1138
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1139
|
+
if (chainInbound) {
|
|
1140
|
+
vaultAddress = chainInbound.address;
|
|
1141
|
+
routerAddress = chainInbound.router || to;
|
|
1132
1142
|
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1133
1143
|
to = routerAddress;
|
|
1134
1144
|
} else {
|
|
1135
|
-
throw new Error(
|
|
1145
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1136
1146
|
}
|
|
1137
1147
|
}
|
|
1138
1148
|
} catch (fetchError) {
|
|
@@ -1142,6 +1152,15 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1142
1152
|
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1143
1153
|
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1144
1154
|
}
|
|
1155
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1156
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1157
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1158
|
+
to = routerAddress;
|
|
1159
|
+
}
|
|
1160
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1161
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1162
|
+
}
|
|
1163
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1145
1164
|
const functionSelector = "44bc937b";
|
|
1146
1165
|
const assetAddress = "0x0000000000000000000000000000000000000000";
|
|
1147
1166
|
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
@@ -1230,17 +1249,98 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1230
1249
|
if (estimatedGasFee > balance) {
|
|
1231
1250
|
throw new Error("Insufficient ETH balance to cover gas fees");
|
|
1232
1251
|
}
|
|
1233
|
-
|
|
1252
|
+
let data;
|
|
1253
|
+
let finalTo;
|
|
1254
|
+
if (isThorchainSwap) {
|
|
1255
|
+
console.log(tag, "\uD83D\uDD04 ERC20 THORChain swap detected - encoding depositWithExpiry");
|
|
1256
|
+
let vaultAddress = "0x0000000000000000000000000000000000000000";
|
|
1257
|
+
let routerAddress = to;
|
|
1258
|
+
try {
|
|
1259
|
+
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1260
|
+
if (inboundResponse.ok) {
|
|
1261
|
+
const inboundData = await inboundResponse.json();
|
|
1262
|
+
const chainIdToThorchain = {
|
|
1263
|
+
1: "ETH",
|
|
1264
|
+
43114: "AVAX",
|
|
1265
|
+
8453: "BASE",
|
|
1266
|
+
56: "BSC"
|
|
1267
|
+
};
|
|
1268
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1269
|
+
if (!thorchainName) {
|
|
1270
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1271
|
+
}
|
|
1272
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1273
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1274
|
+
if (chainInbound) {
|
|
1275
|
+
vaultAddress = chainInbound.address;
|
|
1276
|
+
routerAddress = chainInbound.router || to;
|
|
1277
|
+
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1278
|
+
to = routerAddress;
|
|
1279
|
+
} else {
|
|
1280
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
} catch (fetchError) {
|
|
1284
|
+
console.error(tag, "Failed to fetch inbound addresses:", fetchError);
|
|
1285
|
+
throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
|
|
1286
|
+
}
|
|
1287
|
+
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1288
|
+
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1289
|
+
}
|
|
1290
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1291
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1292
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1293
|
+
to = routerAddress;
|
|
1294
|
+
}
|
|
1295
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1296
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1297
|
+
}
|
|
1298
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1299
|
+
console.log(tag, "✅ ERC20 THORChain swap addresses validated");
|
|
1300
|
+
console.log(tag, " Router:", to);
|
|
1301
|
+
console.log(tag, " Vault:", vaultAddress);
|
|
1302
|
+
console.log(tag, " Token:", contractAddress);
|
|
1303
|
+
const functionSelector = "44bc937b";
|
|
1304
|
+
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
1305
|
+
const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1306
|
+
const assetPadded = contractAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1307
|
+
const amountPadded = amountWei.toString(16).padStart(64, "0");
|
|
1308
|
+
const stringOffset = (5 * 32).toString(16).padStart(64, "0");
|
|
1309
|
+
const expiryPadded = expiryTime.toString(16).padStart(64, "0");
|
|
1310
|
+
const fixedMemo = memo || "";
|
|
1311
|
+
const memoBytes = Buffer.from(fixedMemo, "utf8");
|
|
1312
|
+
const memoHex = memoBytes.toString("hex");
|
|
1313
|
+
const stringLength = memoBytes.length.toString(16).padStart(64, "0");
|
|
1314
|
+
const paddingLength = (32 - memoBytes.length % 32) % 32;
|
|
1315
|
+
const memoPadded = memoHex + "0".repeat(paddingLength * 2);
|
|
1316
|
+
data = "0x" + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
|
|
1317
|
+
finalTo = to;
|
|
1318
|
+
console.log(tag, "✅ Encoded ERC20 depositWithExpiry:", {
|
|
1319
|
+
functionSelector: "0x" + functionSelector,
|
|
1320
|
+
vault: vaultAddress,
|
|
1321
|
+
asset: contractAddress,
|
|
1322
|
+
amount: amountWei.toString(),
|
|
1323
|
+
memo: fixedMemo,
|
|
1324
|
+
expiry: expiryTime,
|
|
1325
|
+
dataLength: data.length
|
|
1326
|
+
});
|
|
1327
|
+
gasLimit = BigInt(300000);
|
|
1328
|
+
} else {
|
|
1329
|
+
data = encodeTransferData(to, amountWei);
|
|
1330
|
+
finalTo = contractAddress;
|
|
1331
|
+
}
|
|
1234
1332
|
const ethPriceInUsd = await fetchEthPriceInUsd(pioneer, networkId);
|
|
1235
|
-
const
|
|
1333
|
+
const finalGasFee = gasPrice * gasLimit;
|
|
1334
|
+
const gasFeeUsd = Number(finalGasFee) / 1000000000000000000 * ethPriceInUsd;
|
|
1236
1335
|
const tokenPriceInUsd = await fetchTokenPriceInUsd(pioneer, caip);
|
|
1237
1336
|
const amountUsd = Number(amountWei) / Number(tokenMultiplier) * tokenPriceInUsd;
|
|
1238
1337
|
unsignedTx = {
|
|
1239
1338
|
chainId,
|
|
1339
|
+
from: address,
|
|
1240
1340
|
nonce: toHex(nonce),
|
|
1241
1341
|
gas: toHex(gasLimit),
|
|
1242
1342
|
gasPrice: toHex(gasPrice),
|
|
1243
|
-
to:
|
|
1343
|
+
to: finalTo,
|
|
1244
1344
|
value: "0x0",
|
|
1245
1345
|
data,
|
|
1246
1346
|
gasFeeUsd,
|
|
@@ -4100,8 +4200,58 @@ class SDK {
|
|
|
4100
4200
|
if (tx.type === "deposit") {
|
|
4101
4201
|
unsignedTx = await createUnsignedTendermintTx(caip, tx.type, tx.txParams.amount, tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false, undefined);
|
|
4102
4202
|
} else if (tx.type === "EVM" || tx.type === "evm") {
|
|
4103
|
-
console.log(tag, "
|
|
4104
|
-
unsignedTx = tx.txParams;
|
|
4203
|
+
console.log(tag, "Building EVM swap transaction with createUnsignedEvmTx");
|
|
4204
|
+
unsignedTx = await createUnsignedEvmTx(caip, tx.txParams.recipientAddress, parseFloat(tx.txParams.amount), tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false);
|
|
4205
|
+
console.log(tag, "✅ Built complete EVM transaction:", {
|
|
4206
|
+
to: unsignedTx.to,
|
|
4207
|
+
from: this.pubkeyContext?.address,
|
|
4208
|
+
value: unsignedTx.value,
|
|
4209
|
+
chainId: unsignedTx.chainId,
|
|
4210
|
+
hasData: !!unsignedTx.data
|
|
4211
|
+
});
|
|
4212
|
+
console.log(tag, "\uD83D\uDEE1️ Running Tenderly simulation for EVM swap...");
|
|
4213
|
+
try {
|
|
4214
|
+
const insightResult = await this.pioneer.Insight({
|
|
4215
|
+
tx: unsignedTx,
|
|
4216
|
+
source: "swap",
|
|
4217
|
+
isThorchainSwap: tx.txParams.isThorchainSwap || false
|
|
4218
|
+
});
|
|
4219
|
+
const insight = insightResult.body;
|
|
4220
|
+
console.log(tag, "Simulation result:", insight?.simulation);
|
|
4221
|
+
if (!insight || !insight.simulation) {
|
|
4222
|
+
console.warn(tag, "⚠️ WARNING: Tenderly simulation unavailable - proceeding without validation");
|
|
4223
|
+
} else if (!insight.simulation.success) {
|
|
4224
|
+
console.warn(tag, `⚠️ WARNING: Swap simulation FAILED - ${insight.simulation.error || "Transaction may revert"}`);
|
|
4225
|
+
console.warn(tag, "⚠️ Proceeding anyway - USE CAUTION");
|
|
4226
|
+
} else {
|
|
4227
|
+
if (tx.txParams.isThorchainSwap) {
|
|
4228
|
+
console.log(tag, "\uD83D\uDD0D Verifying THORChain swap parameters...");
|
|
4229
|
+
const method = insight.simulation.method;
|
|
4230
|
+
if (!method || !method.toLowerCase().includes("deposit")) {
|
|
4231
|
+
throw new Error(`❌ CRITICAL: Invalid THORChain swap method: ${method} - expected depositWithExpiry`);
|
|
4232
|
+
}
|
|
4233
|
+
const routerAddress = unsignedTx.to;
|
|
4234
|
+
const vaultAddress = tx.txParams.vaultAddress;
|
|
4235
|
+
if (routerAddress && vaultAddress) {
|
|
4236
|
+
if (routerAddress.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
4237
|
+
throw new Error(`❌ CRITICAL: Sending directly to vault ${vaultAddress} instead of router!`);
|
|
4238
|
+
}
|
|
4239
|
+
console.log(tag, `✅ Router: ${routerAddress}`);
|
|
4240
|
+
console.log(tag, `✅ Vault: ${vaultAddress}`);
|
|
4241
|
+
}
|
|
4242
|
+
if (insight.simulation.addresses && insight.simulation.addresses.length > 0) {
|
|
4243
|
+
console.log(tag, `✅ Addresses involved: ${insight.simulation.addresses.length}`);
|
|
4244
|
+
} else {
|
|
4245
|
+
console.log(tag, "⚠️ WARNING: No addresses detected in simulation");
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
console.log(tag, `✅ Simulation PASSED - Gas used: ${insight.simulation.gasUsed}`);
|
|
4249
|
+
console.log(tag, `✅ Method: ${insight.simulation.method}`);
|
|
4250
|
+
}
|
|
4251
|
+
} catch (e) {
|
|
4252
|
+
console.error(tag, "❌ Simulation validation failed:", e.message);
|
|
4253
|
+
throw new Error(`Swap blocked by simulation failure: ${e.message}`);
|
|
4254
|
+
}
|
|
4105
4255
|
} else {
|
|
4106
4256
|
if (!tx.txParams.memo)
|
|
4107
4257
|
throw Error("memo required on swaps!");
|
package/dist/index.es.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 (
|
|
1257
|
+
if (isThorchainSwap) {
|
|
1258
1258
|
gasLimit = BigInt(120000);
|
|
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
|
}
|
|
@@ -1279,7 +1279,6 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1279
1279
|
throw new Error("Insufficient funds for the transaction amount and gas fees");
|
|
1280
1280
|
}
|
|
1281
1281
|
}
|
|
1282
|
-
const isThorchainSwap = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1283
1282
|
let txData = "0x";
|
|
1284
1283
|
if (isThorchainSwap) {
|
|
1285
1284
|
console.log(tag, "Detected THORChain swap, encoding deposit data for memo:", memo);
|
|
@@ -1301,14 +1300,25 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1301
1300
|
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1302
1301
|
if (inboundResponse.ok) {
|
|
1303
1302
|
const inboundData = await inboundResponse.json();
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1303
|
+
const chainIdToThorchain = {
|
|
1304
|
+
1: "ETH",
|
|
1305
|
+
43114: "AVAX",
|
|
1306
|
+
8453: "BASE",
|
|
1307
|
+
56: "BSC"
|
|
1308
|
+
};
|
|
1309
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1310
|
+
if (!thorchainName) {
|
|
1311
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1312
|
+
}
|
|
1313
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1314
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1315
|
+
if (chainInbound) {
|
|
1316
|
+
vaultAddress = chainInbound.address;
|
|
1317
|
+
routerAddress = chainInbound.router || to;
|
|
1308
1318
|
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1309
1319
|
to = routerAddress;
|
|
1310
1320
|
} else {
|
|
1311
|
-
throw new Error(
|
|
1321
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1312
1322
|
}
|
|
1313
1323
|
}
|
|
1314
1324
|
} catch (fetchError) {
|
|
@@ -1318,6 +1328,15 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1318
1328
|
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1319
1329
|
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1320
1330
|
}
|
|
1331
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1332
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1333
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1334
|
+
to = routerAddress;
|
|
1335
|
+
}
|
|
1336
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1337
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1338
|
+
}
|
|
1339
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1321
1340
|
const functionSelector = "44bc937b";
|
|
1322
1341
|
const assetAddress = "0x0000000000000000000000000000000000000000";
|
|
1323
1342
|
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
@@ -1406,17 +1425,98 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1406
1425
|
if (estimatedGasFee > balance) {
|
|
1407
1426
|
throw new Error("Insufficient ETH balance to cover gas fees");
|
|
1408
1427
|
}
|
|
1409
|
-
|
|
1428
|
+
let data;
|
|
1429
|
+
let finalTo;
|
|
1430
|
+
if (isThorchainSwap) {
|
|
1431
|
+
console.log(tag, "\uD83D\uDD04 ERC20 THORChain swap detected - encoding depositWithExpiry");
|
|
1432
|
+
let vaultAddress = "0x0000000000000000000000000000000000000000";
|
|
1433
|
+
let routerAddress = to;
|
|
1434
|
+
try {
|
|
1435
|
+
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1436
|
+
if (inboundResponse.ok) {
|
|
1437
|
+
const inboundData = await inboundResponse.json();
|
|
1438
|
+
const chainIdToThorchain = {
|
|
1439
|
+
1: "ETH",
|
|
1440
|
+
43114: "AVAX",
|
|
1441
|
+
8453: "BASE",
|
|
1442
|
+
56: "BSC"
|
|
1443
|
+
};
|
|
1444
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1445
|
+
if (!thorchainName) {
|
|
1446
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1447
|
+
}
|
|
1448
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1449
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1450
|
+
if (chainInbound) {
|
|
1451
|
+
vaultAddress = chainInbound.address;
|
|
1452
|
+
routerAddress = chainInbound.router || to;
|
|
1453
|
+
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1454
|
+
to = routerAddress;
|
|
1455
|
+
} else {
|
|
1456
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
} catch (fetchError) {
|
|
1460
|
+
console.error(tag, "Failed to fetch inbound addresses:", fetchError);
|
|
1461
|
+
throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
|
|
1462
|
+
}
|
|
1463
|
+
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1464
|
+
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1465
|
+
}
|
|
1466
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1467
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1468
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1469
|
+
to = routerAddress;
|
|
1470
|
+
}
|
|
1471
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1472
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1473
|
+
}
|
|
1474
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1475
|
+
console.log(tag, "✅ ERC20 THORChain swap addresses validated");
|
|
1476
|
+
console.log(tag, " Router:", to);
|
|
1477
|
+
console.log(tag, " Vault:", vaultAddress);
|
|
1478
|
+
console.log(tag, " Token:", contractAddress);
|
|
1479
|
+
const functionSelector = "44bc937b";
|
|
1480
|
+
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
1481
|
+
const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1482
|
+
const assetPadded = contractAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1483
|
+
const amountPadded = amountWei.toString(16).padStart(64, "0");
|
|
1484
|
+
const stringOffset = (5 * 32).toString(16).padStart(64, "0");
|
|
1485
|
+
const expiryPadded = expiryTime.toString(16).padStart(64, "0");
|
|
1486
|
+
const fixedMemo = memo || "";
|
|
1487
|
+
const memoBytes = Buffer.from(fixedMemo, "utf8");
|
|
1488
|
+
const memoHex = memoBytes.toString("hex");
|
|
1489
|
+
const stringLength = memoBytes.length.toString(16).padStart(64, "0");
|
|
1490
|
+
const paddingLength = (32 - memoBytes.length % 32) % 32;
|
|
1491
|
+
const memoPadded = memoHex + "0".repeat(paddingLength * 2);
|
|
1492
|
+
data = "0x" + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
|
|
1493
|
+
finalTo = to;
|
|
1494
|
+
console.log(tag, "✅ Encoded ERC20 depositWithExpiry:", {
|
|
1495
|
+
functionSelector: "0x" + functionSelector,
|
|
1496
|
+
vault: vaultAddress,
|
|
1497
|
+
asset: contractAddress,
|
|
1498
|
+
amount: amountWei.toString(),
|
|
1499
|
+
memo: fixedMemo,
|
|
1500
|
+
expiry: expiryTime,
|
|
1501
|
+
dataLength: data.length
|
|
1502
|
+
});
|
|
1503
|
+
gasLimit = BigInt(300000);
|
|
1504
|
+
} else {
|
|
1505
|
+
data = encodeTransferData(to, amountWei);
|
|
1506
|
+
finalTo = contractAddress;
|
|
1507
|
+
}
|
|
1410
1508
|
const ethPriceInUsd = await fetchEthPriceInUsd(pioneer, networkId);
|
|
1411
|
-
const
|
|
1509
|
+
const finalGasFee = gasPrice * gasLimit;
|
|
1510
|
+
const gasFeeUsd = Number(finalGasFee) / 1000000000000000000 * ethPriceInUsd;
|
|
1412
1511
|
const tokenPriceInUsd = await fetchTokenPriceInUsd(pioneer, caip);
|
|
1413
1512
|
const amountUsd = Number(amountWei) / Number(tokenMultiplier) * tokenPriceInUsd;
|
|
1414
1513
|
unsignedTx = {
|
|
1415
1514
|
chainId,
|
|
1515
|
+
from: address,
|
|
1416
1516
|
nonce: toHex(nonce),
|
|
1417
1517
|
gas: toHex(gasLimit),
|
|
1418
1518
|
gasPrice: toHex(gasPrice),
|
|
1419
|
-
to:
|
|
1519
|
+
to: finalTo,
|
|
1420
1520
|
value: "0x0",
|
|
1421
1521
|
data,
|
|
1422
1522
|
gasFeeUsd,
|
|
@@ -4276,8 +4376,58 @@ class SDK {
|
|
|
4276
4376
|
if (tx.type === "deposit") {
|
|
4277
4377
|
unsignedTx = await createUnsignedTendermintTx(caip, tx.type, tx.txParams.amount, tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false, undefined);
|
|
4278
4378
|
} else if (tx.type === "EVM" || tx.type === "evm") {
|
|
4279
|
-
console.log(tag, "
|
|
4280
|
-
unsignedTx = tx.txParams;
|
|
4379
|
+
console.log(tag, "Building EVM swap transaction with createUnsignedEvmTx");
|
|
4380
|
+
unsignedTx = await createUnsignedEvmTx(caip, tx.txParams.recipientAddress, parseFloat(tx.txParams.amount), tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false);
|
|
4381
|
+
console.log(tag, "✅ Built complete EVM transaction:", {
|
|
4382
|
+
to: unsignedTx.to,
|
|
4383
|
+
from: this.pubkeyContext?.address,
|
|
4384
|
+
value: unsignedTx.value,
|
|
4385
|
+
chainId: unsignedTx.chainId,
|
|
4386
|
+
hasData: !!unsignedTx.data
|
|
4387
|
+
});
|
|
4388
|
+
console.log(tag, "\uD83D\uDEE1️ Running Tenderly simulation for EVM swap...");
|
|
4389
|
+
try {
|
|
4390
|
+
const insightResult = await this.pioneer.Insight({
|
|
4391
|
+
tx: unsignedTx,
|
|
4392
|
+
source: "swap",
|
|
4393
|
+
isThorchainSwap: tx.txParams.isThorchainSwap || false
|
|
4394
|
+
});
|
|
4395
|
+
const insight = insightResult.body;
|
|
4396
|
+
console.log(tag, "Simulation result:", insight?.simulation);
|
|
4397
|
+
if (!insight || !insight.simulation) {
|
|
4398
|
+
console.warn(tag, "⚠️ WARNING: Tenderly simulation unavailable - proceeding without validation");
|
|
4399
|
+
} else if (!insight.simulation.success) {
|
|
4400
|
+
console.warn(tag, `⚠️ WARNING: Swap simulation FAILED - ${insight.simulation.error || "Transaction may revert"}`);
|
|
4401
|
+
console.warn(tag, "⚠️ Proceeding anyway - USE CAUTION");
|
|
4402
|
+
} else {
|
|
4403
|
+
if (tx.txParams.isThorchainSwap) {
|
|
4404
|
+
console.log(tag, "\uD83D\uDD0D Verifying THORChain swap parameters...");
|
|
4405
|
+
const method = insight.simulation.method;
|
|
4406
|
+
if (!method || !method.toLowerCase().includes("deposit")) {
|
|
4407
|
+
throw new Error(`❌ CRITICAL: Invalid THORChain swap method: ${method} - expected depositWithExpiry`);
|
|
4408
|
+
}
|
|
4409
|
+
const routerAddress = unsignedTx.to;
|
|
4410
|
+
const vaultAddress = tx.txParams.vaultAddress;
|
|
4411
|
+
if (routerAddress && vaultAddress) {
|
|
4412
|
+
if (routerAddress.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
4413
|
+
throw new Error(`❌ CRITICAL: Sending directly to vault ${vaultAddress} instead of router!`);
|
|
4414
|
+
}
|
|
4415
|
+
console.log(tag, `✅ Router: ${routerAddress}`);
|
|
4416
|
+
console.log(tag, `✅ Vault: ${vaultAddress}`);
|
|
4417
|
+
}
|
|
4418
|
+
if (insight.simulation.addresses && insight.simulation.addresses.length > 0) {
|
|
4419
|
+
console.log(tag, `✅ Addresses involved: ${insight.simulation.addresses.length}`);
|
|
4420
|
+
} else {
|
|
4421
|
+
console.log(tag, "⚠️ WARNING: No addresses detected in simulation");
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
console.log(tag, `✅ Simulation PASSED - Gas used: ${insight.simulation.gasUsed}`);
|
|
4425
|
+
console.log(tag, `✅ Method: ${insight.simulation.method}`);
|
|
4426
|
+
}
|
|
4427
|
+
} catch (e) {
|
|
4428
|
+
console.error(tag, "❌ Simulation validation failed:", e.message);
|
|
4429
|
+
throw new Error(`Swap blocked by simulation failure: ${e.message}`);
|
|
4430
|
+
}
|
|
4281
4431
|
} else {
|
|
4282
4432
|
if (!tx.txParams.memo)
|
|
4283
4433
|
throw Error("memo required on swaps!");
|
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 (
|
|
1257
|
+
if (isThorchainSwap) {
|
|
1258
1258
|
gasLimit = BigInt(120000);
|
|
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
|
}
|
|
@@ -1279,7 +1279,6 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1279
1279
|
throw new Error("Insufficient funds for the transaction amount and gas fees");
|
|
1280
1280
|
}
|
|
1281
1281
|
}
|
|
1282
|
-
const isThorchainSwap = memo && (memo.startsWith("=") || memo.startsWith("SWAP") || memo.includes(":"));
|
|
1283
1282
|
let txData = "0x";
|
|
1284
1283
|
if (isThorchainSwap) {
|
|
1285
1284
|
console.log(tag, "Detected THORChain swap, encoding deposit data for memo:", memo);
|
|
@@ -1301,14 +1300,25 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1301
1300
|
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1302
1301
|
if (inboundResponse.ok) {
|
|
1303
1302
|
const inboundData = await inboundResponse.json();
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1303
|
+
const chainIdToThorchain = {
|
|
1304
|
+
1: "ETH",
|
|
1305
|
+
43114: "AVAX",
|
|
1306
|
+
8453: "BASE",
|
|
1307
|
+
56: "BSC"
|
|
1308
|
+
};
|
|
1309
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1310
|
+
if (!thorchainName) {
|
|
1311
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1312
|
+
}
|
|
1313
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1314
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1315
|
+
if (chainInbound) {
|
|
1316
|
+
vaultAddress = chainInbound.address;
|
|
1317
|
+
routerAddress = chainInbound.router || to;
|
|
1308
1318
|
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1309
1319
|
to = routerAddress;
|
|
1310
1320
|
} else {
|
|
1311
|
-
throw new Error(
|
|
1321
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1312
1322
|
}
|
|
1313
1323
|
}
|
|
1314
1324
|
} catch (fetchError) {
|
|
@@ -1318,6 +1328,15 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1318
1328
|
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1319
1329
|
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1320
1330
|
}
|
|
1331
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1332
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1333
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1334
|
+
to = routerAddress;
|
|
1335
|
+
}
|
|
1336
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1337
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1338
|
+
}
|
|
1339
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1321
1340
|
const functionSelector = "44bc937b";
|
|
1322
1341
|
const assetAddress = "0x0000000000000000000000000000000000000000";
|
|
1323
1342
|
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
@@ -1406,17 +1425,98 @@ async function createUnsignedEvmTx(caip, to, amount, memo, pubkeys, pioneer, pub
|
|
|
1406
1425
|
if (estimatedGasFee > balance) {
|
|
1407
1426
|
throw new Error("Insufficient ETH balance to cover gas fees");
|
|
1408
1427
|
}
|
|
1409
|
-
|
|
1428
|
+
let data;
|
|
1429
|
+
let finalTo;
|
|
1430
|
+
if (isThorchainSwap) {
|
|
1431
|
+
console.log(tag, "\uD83D\uDD04 ERC20 THORChain swap detected - encoding depositWithExpiry");
|
|
1432
|
+
let vaultAddress = "0x0000000000000000000000000000000000000000";
|
|
1433
|
+
let routerAddress = to;
|
|
1434
|
+
try {
|
|
1435
|
+
const inboundResponse = await fetch("https://thornode.ninerealms.com/thorchain/inbound_addresses");
|
|
1436
|
+
if (inboundResponse.ok) {
|
|
1437
|
+
const inboundData = await inboundResponse.json();
|
|
1438
|
+
const chainIdToThorchain = {
|
|
1439
|
+
1: "ETH",
|
|
1440
|
+
43114: "AVAX",
|
|
1441
|
+
8453: "BASE",
|
|
1442
|
+
56: "BSC"
|
|
1443
|
+
};
|
|
1444
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
1445
|
+
if (!thorchainName) {
|
|
1446
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
1447
|
+
}
|
|
1448
|
+
console.log(tag, "Looking for THORChain inbound for chain:", thorchainName, "chainId:", chainId);
|
|
1449
|
+
const chainInbound = inboundData.find((inbound) => inbound.chain === thorchainName && !inbound.halted);
|
|
1450
|
+
if (chainInbound) {
|
|
1451
|
+
vaultAddress = chainInbound.address;
|
|
1452
|
+
routerAddress = chainInbound.router || to;
|
|
1453
|
+
console.log(tag, "Using THORChain inbound addresses - vault:", vaultAddress, "router:", routerAddress);
|
|
1454
|
+
to = routerAddress;
|
|
1455
|
+
} else {
|
|
1456
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
} catch (fetchError) {
|
|
1460
|
+
console.error(tag, "Failed to fetch inbound addresses:", fetchError);
|
|
1461
|
+
throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
|
|
1462
|
+
}
|
|
1463
|
+
if (vaultAddress === "0x0000000000000000000000000000000000000000") {
|
|
1464
|
+
throw new Error("Cannot proceed with THORChain swap - vault address is invalid (0x0)");
|
|
1465
|
+
}
|
|
1466
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
1467
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
1468
|
+
console.warn(tag, "⚠️ Using fetched router address instead:", routerAddress);
|
|
1469
|
+
to = routerAddress;
|
|
1470
|
+
}
|
|
1471
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
1472
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
1473
|
+
}
|
|
1474
|
+
console.log(tag, "✅ Final validation passed - router:", to, "vault:", vaultAddress);
|
|
1475
|
+
console.log(tag, "✅ ERC20 THORChain swap addresses validated");
|
|
1476
|
+
console.log(tag, " Router:", to);
|
|
1477
|
+
console.log(tag, " Vault:", vaultAddress);
|
|
1478
|
+
console.log(tag, " Token:", contractAddress);
|
|
1479
|
+
const functionSelector = "44bc937b";
|
|
1480
|
+
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
1481
|
+
const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1482
|
+
const assetPadded = contractAddress.toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
1483
|
+
const amountPadded = amountWei.toString(16).padStart(64, "0");
|
|
1484
|
+
const stringOffset = (5 * 32).toString(16).padStart(64, "0");
|
|
1485
|
+
const expiryPadded = expiryTime.toString(16).padStart(64, "0");
|
|
1486
|
+
const fixedMemo = memo || "";
|
|
1487
|
+
const memoBytes = Buffer.from(fixedMemo, "utf8");
|
|
1488
|
+
const memoHex = memoBytes.toString("hex");
|
|
1489
|
+
const stringLength = memoBytes.length.toString(16).padStart(64, "0");
|
|
1490
|
+
const paddingLength = (32 - memoBytes.length % 32) % 32;
|
|
1491
|
+
const memoPadded = memoHex + "0".repeat(paddingLength * 2);
|
|
1492
|
+
data = "0x" + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
|
|
1493
|
+
finalTo = to;
|
|
1494
|
+
console.log(tag, "✅ Encoded ERC20 depositWithExpiry:", {
|
|
1495
|
+
functionSelector: "0x" + functionSelector,
|
|
1496
|
+
vault: vaultAddress,
|
|
1497
|
+
asset: contractAddress,
|
|
1498
|
+
amount: amountWei.toString(),
|
|
1499
|
+
memo: fixedMemo,
|
|
1500
|
+
expiry: expiryTime,
|
|
1501
|
+
dataLength: data.length
|
|
1502
|
+
});
|
|
1503
|
+
gasLimit = BigInt(300000);
|
|
1504
|
+
} else {
|
|
1505
|
+
data = encodeTransferData(to, amountWei);
|
|
1506
|
+
finalTo = contractAddress;
|
|
1507
|
+
}
|
|
1410
1508
|
const ethPriceInUsd = await fetchEthPriceInUsd(pioneer, networkId);
|
|
1411
|
-
const
|
|
1509
|
+
const finalGasFee = gasPrice * gasLimit;
|
|
1510
|
+
const gasFeeUsd = Number(finalGasFee) / 1000000000000000000 * ethPriceInUsd;
|
|
1412
1511
|
const tokenPriceInUsd = await fetchTokenPriceInUsd(pioneer, caip);
|
|
1413
1512
|
const amountUsd = Number(amountWei) / Number(tokenMultiplier) * tokenPriceInUsd;
|
|
1414
1513
|
unsignedTx = {
|
|
1415
1514
|
chainId,
|
|
1515
|
+
from: address,
|
|
1416
1516
|
nonce: toHex(nonce),
|
|
1417
1517
|
gas: toHex(gasLimit),
|
|
1418
1518
|
gasPrice: toHex(gasPrice),
|
|
1419
|
-
to:
|
|
1519
|
+
to: finalTo,
|
|
1420
1520
|
value: "0x0",
|
|
1421
1521
|
data,
|
|
1422
1522
|
gasFeeUsd,
|
|
@@ -4276,8 +4376,58 @@ class SDK {
|
|
|
4276
4376
|
if (tx.type === "deposit") {
|
|
4277
4377
|
unsignedTx = await createUnsignedTendermintTx(caip, tx.type, tx.txParams.amount, tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false, undefined);
|
|
4278
4378
|
} else if (tx.type === "EVM" || tx.type === "evm") {
|
|
4279
|
-
console.log(tag, "
|
|
4280
|
-
unsignedTx = tx.txParams;
|
|
4379
|
+
console.log(tag, "Building EVM swap transaction with createUnsignedEvmTx");
|
|
4380
|
+
unsignedTx = await createUnsignedEvmTx(caip, tx.txParams.recipientAddress, parseFloat(tx.txParams.amount), tx.txParams.memo, this.pubkeys, this.pioneer, this.pubkeyContext, false);
|
|
4381
|
+
console.log(tag, "✅ Built complete EVM transaction:", {
|
|
4382
|
+
to: unsignedTx.to,
|
|
4383
|
+
from: this.pubkeyContext?.address,
|
|
4384
|
+
value: unsignedTx.value,
|
|
4385
|
+
chainId: unsignedTx.chainId,
|
|
4386
|
+
hasData: !!unsignedTx.data
|
|
4387
|
+
});
|
|
4388
|
+
console.log(tag, "\uD83D\uDEE1️ Running Tenderly simulation for EVM swap...");
|
|
4389
|
+
try {
|
|
4390
|
+
const insightResult = await this.pioneer.Insight({
|
|
4391
|
+
tx: unsignedTx,
|
|
4392
|
+
source: "swap",
|
|
4393
|
+
isThorchainSwap: tx.txParams.isThorchainSwap || false
|
|
4394
|
+
});
|
|
4395
|
+
const insight = insightResult.body;
|
|
4396
|
+
console.log(tag, "Simulation result:", insight?.simulation);
|
|
4397
|
+
if (!insight || !insight.simulation) {
|
|
4398
|
+
console.warn(tag, "⚠️ WARNING: Tenderly simulation unavailable - proceeding without validation");
|
|
4399
|
+
} else if (!insight.simulation.success) {
|
|
4400
|
+
console.warn(tag, `⚠️ WARNING: Swap simulation FAILED - ${insight.simulation.error || "Transaction may revert"}`);
|
|
4401
|
+
console.warn(tag, "⚠️ Proceeding anyway - USE CAUTION");
|
|
4402
|
+
} else {
|
|
4403
|
+
if (tx.txParams.isThorchainSwap) {
|
|
4404
|
+
console.log(tag, "\uD83D\uDD0D Verifying THORChain swap parameters...");
|
|
4405
|
+
const method = insight.simulation.method;
|
|
4406
|
+
if (!method || !method.toLowerCase().includes("deposit")) {
|
|
4407
|
+
throw new Error(`❌ CRITICAL: Invalid THORChain swap method: ${method} - expected depositWithExpiry`);
|
|
4408
|
+
}
|
|
4409
|
+
const routerAddress = unsignedTx.to;
|
|
4410
|
+
const vaultAddress = tx.txParams.vaultAddress;
|
|
4411
|
+
if (routerAddress && vaultAddress) {
|
|
4412
|
+
if (routerAddress.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
4413
|
+
throw new Error(`❌ CRITICAL: Sending directly to vault ${vaultAddress} instead of router!`);
|
|
4414
|
+
}
|
|
4415
|
+
console.log(tag, `✅ Router: ${routerAddress}`);
|
|
4416
|
+
console.log(tag, `✅ Vault: ${vaultAddress}`);
|
|
4417
|
+
}
|
|
4418
|
+
if (insight.simulation.addresses && insight.simulation.addresses.length > 0) {
|
|
4419
|
+
console.log(tag, `✅ Addresses involved: ${insight.simulation.addresses.length}`);
|
|
4420
|
+
} else {
|
|
4421
|
+
console.log(tag, "⚠️ WARNING: No addresses detected in simulation");
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
console.log(tag, `✅ Simulation PASSED - Gas used: ${insight.simulation.gasUsed}`);
|
|
4425
|
+
console.log(tag, `✅ Method: ${insight.simulation.method}`);
|
|
4426
|
+
}
|
|
4427
|
+
} catch (e) {
|
|
4428
|
+
console.error(tag, "❌ Simulation validation failed:", e.message);
|
|
4429
|
+
throw new Error(`Swap blocked by simulation failure: ${e.message}`);
|
|
4430
|
+
}
|
|
4281
4431
|
} else {
|
|
4282
4432
|
if (!tx.txParams.memo)
|
|
4283
4433
|
throw Error("memo required on swaps!");
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "highlander",
|
|
3
3
|
"name": "@pioneer-platform/pioneer-sdk",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.12.2",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@keepkey/keepkey-sdk": "^0.2.62",
|
|
7
7
|
"@pioneer-platform/loggerdog": "^8.11.0",
|
|
8
8
|
"@pioneer-platform/pioneer-caip": "^9.10.0",
|
|
9
9
|
"@pioneer-platform/pioneer-client": "^9.10.10",
|
|
10
10
|
"@pioneer-platform/pioneer-coins": "^9.11.0",
|
|
11
|
-
"@pioneer-platform/pioneer-discovery": "^8.
|
|
12
|
-
"@pioneer-platform/pioneer-events": "^8.
|
|
11
|
+
"@pioneer-platform/pioneer-discovery": "^8.12.0",
|
|
12
|
+
"@pioneer-platform/pioneer-events": "^8.12.0",
|
|
13
13
|
"coinselect": "^3.1.13",
|
|
14
14
|
"eventemitter3": "^5.0.1",
|
|
15
15
|
"neotraverse": "^0.6.8",
|
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!');
|
|
@@ -281,15 +281,17 @@ export async function createUnsignedEvmTx(
|
|
|
281
281
|
|
|
282
282
|
if (memo === ' ') memo = '';
|
|
283
283
|
|
|
284
|
+
// Check if this is a THORChain swap (memo starts with '=' or 'SWAP' or contains ':')
|
|
285
|
+
// Define this before the switch so it's available in all cases
|
|
286
|
+
const isThorchainSwap =
|
|
287
|
+
memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
|
|
288
|
+
|
|
284
289
|
// Build transaction object based on asset type
|
|
285
290
|
switch (assetType) {
|
|
286
291
|
case 'gas': {
|
|
287
|
-
//
|
|
288
|
-
const isThorchainOperation =
|
|
289
|
-
memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
|
|
290
|
-
|
|
292
|
+
// Use the top-level isThorchainSwap check
|
|
291
293
|
let gasLimit;
|
|
292
|
-
if (
|
|
294
|
+
if (isThorchainSwap) {
|
|
293
295
|
// THORChain depositWithExpiry requires more gas (90-120k typical)
|
|
294
296
|
// Use 120000 to be safe for all network conditions
|
|
295
297
|
gasLimit = BigInt(120000);
|
|
@@ -300,7 +302,7 @@ export async function createUnsignedEvmTx(
|
|
|
300
302
|
gasLimit = chainId === 1 ? BigInt(21000) : BigInt(25000);
|
|
301
303
|
}
|
|
302
304
|
|
|
303
|
-
if (memo && memo !== '' && !
|
|
305
|
+
if (memo && memo !== '' && !isThorchainSwap) {
|
|
304
306
|
const memoBytes = Buffer.from(memo, 'utf8').length;
|
|
305
307
|
gasLimit += BigInt(memoBytes) * 68n; // Approximate additional gas
|
|
306
308
|
//console.log(tag, 'Adjusted gasLimit for memo:', gasLimit.toString());
|
|
@@ -328,10 +330,7 @@ export async function createUnsignedEvmTx(
|
|
|
328
330
|
|
|
329
331
|
//console.log(tag, 'amountWei:', amountWei.toString());
|
|
330
332
|
|
|
331
|
-
//
|
|
332
|
-
const isThorchainSwap =
|
|
333
|
-
memo && (memo.startsWith('=') || memo.startsWith('SWAP') || memo.includes(':'));
|
|
334
|
-
|
|
333
|
+
// Use the top-level isThorchainSwap check (defined before switch statement)
|
|
335
334
|
let txData = '0x';
|
|
336
335
|
|
|
337
336
|
if (isThorchainSwap) {
|
|
@@ -366,19 +365,35 @@ export async function createUnsignedEvmTx(
|
|
|
366
365
|
const inboundResponse = await fetch('https://thornode.ninerealms.com/thorchain/inbound_addresses');
|
|
367
366
|
if (inboundResponse.ok) {
|
|
368
367
|
const inboundData = await inboundResponse.json();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
|
|
369
|
+
// Map chainId to THORChain chain name
|
|
370
|
+
const chainIdToThorchain: Record<number, string> = {
|
|
371
|
+
1: 'ETH', // Ethereum mainnet
|
|
372
|
+
43114: 'AVAX', // Avalanche
|
|
373
|
+
8453: 'BASE', // Base
|
|
374
|
+
56: 'BSC', // Binance Smart Chain
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
378
|
+
if (!thorchainName) {
|
|
379
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(tag, 'Looking for THORChain inbound for chain:', thorchainName, 'chainId:', chainId);
|
|
383
|
+
|
|
384
|
+
// Find inbound data for the specific chain
|
|
385
|
+
const chainInbound = inboundData.find(inbound =>
|
|
386
|
+
inbound.chain === thorchainName && !inbound.halted
|
|
372
387
|
);
|
|
373
|
-
if (
|
|
374
|
-
vaultAddress =
|
|
375
|
-
routerAddress =
|
|
388
|
+
if (chainInbound) {
|
|
389
|
+
vaultAddress = chainInbound.address; // This is the Asgard vault
|
|
390
|
+
routerAddress = chainInbound.router || to; // Use fetched router or fallback to 'to'
|
|
376
391
|
console.log(tag, 'Using THORChain inbound addresses - vault:', vaultAddress, 'router:', routerAddress);
|
|
377
|
-
|
|
392
|
+
|
|
378
393
|
// Update the 'to' address to be the router (in case it wasn't)
|
|
379
394
|
to = routerAddress;
|
|
380
395
|
} else {
|
|
381
|
-
throw new Error(
|
|
396
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
382
397
|
}
|
|
383
398
|
}
|
|
384
399
|
} catch (fetchError) {
|
|
@@ -392,6 +407,21 @@ export async function createUnsignedEvmTx(
|
|
|
392
407
|
throw new Error('Cannot proceed with THORChain swap - vault address is invalid (0x0)');
|
|
393
408
|
}
|
|
394
409
|
|
|
410
|
+
// CRITICAL SAFETY CHECK: Ensure we're sending to the router, not the vault
|
|
411
|
+
// This prevents the original bug where USDC was sent to a user wallet
|
|
412
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
413
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
414
|
+
console.warn(tag, '⚠️ Using fetched router address instead:', routerAddress);
|
|
415
|
+
to = routerAddress; // Override with correct router
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Validate router address format
|
|
419
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
420
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
console.log(tag, '✅ Final validation passed - router:', to, 'vault:', vaultAddress);
|
|
424
|
+
|
|
395
425
|
// Use depositWithExpiry for better safety
|
|
396
426
|
// Function signature: depositWithExpiry(address,address,uint256,string,uint256)
|
|
397
427
|
// Function selector: 0x44bc937b
|
|
@@ -539,10 +569,142 @@ export async function createUnsignedEvmTx(
|
|
|
539
569
|
// For simplicity, we assume user has enough tokens
|
|
540
570
|
// In practice, need to check token balance
|
|
541
571
|
|
|
542
|
-
|
|
572
|
+
let data: string;
|
|
573
|
+
let finalTo: string;
|
|
574
|
+
|
|
575
|
+
// Check if this is a THORChain swap
|
|
576
|
+
if (isThorchainSwap) {
|
|
577
|
+
console.log(tag, '🔄 ERC20 THORChain swap detected - encoding depositWithExpiry');
|
|
578
|
+
|
|
579
|
+
// For ERC20 THORChain swaps, we need to:
|
|
580
|
+
// 1. Approve the router to spend tokens (done separately by user)
|
|
581
|
+
// 2. Call router.depositWithExpiry(vault, asset, amount, memo, expiry)
|
|
582
|
+
|
|
583
|
+
// Fetch router and vault addresses from THORChain (same as native ETH flow)
|
|
584
|
+
let vaultAddress = '0x0000000000000000000000000000000000000000';
|
|
585
|
+
let routerAddress = to; // The 'to' field should already be the router
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
// Fetch inbound addresses from THORChain
|
|
589
|
+
const inboundResponse = await fetch('https://thornode.ninerealms.com/thorchain/inbound_addresses');
|
|
590
|
+
if (inboundResponse.ok) {
|
|
591
|
+
const inboundData = await inboundResponse.json();
|
|
592
|
+
|
|
593
|
+
// Map chainId to THORChain chain name
|
|
594
|
+
const chainIdToThorchain: Record<number, string> = {
|
|
595
|
+
1: 'ETH', // Ethereum mainnet
|
|
596
|
+
43114: 'AVAX', // Avalanche
|
|
597
|
+
8453: 'BASE', // Base
|
|
598
|
+
56: 'BSC', // Binance Smart Chain
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const thorchainName = chainIdToThorchain[chainId];
|
|
602
|
+
if (!thorchainName) {
|
|
603
|
+
throw new Error(`Unsupported chain ID for THORChain swap: ${chainId}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
console.log(tag, 'Looking for THORChain inbound for chain:', thorchainName, 'chainId:', chainId);
|
|
607
|
+
|
|
608
|
+
// Find inbound data for the specific chain
|
|
609
|
+
const chainInbound = inboundData.find(inbound =>
|
|
610
|
+
inbound.chain === thorchainName && !inbound.halted
|
|
611
|
+
);
|
|
612
|
+
if (chainInbound) {
|
|
613
|
+
vaultAddress = chainInbound.address; // This is the Asgard vault
|
|
614
|
+
routerAddress = chainInbound.router || to; // Use fetched router or fallback to 'to'
|
|
615
|
+
console.log(tag, 'Using THORChain inbound addresses - vault:', vaultAddress, 'router:', routerAddress);
|
|
616
|
+
|
|
617
|
+
// Update the 'to' address to be the router (in case it wasn't)
|
|
618
|
+
to = routerAddress;
|
|
619
|
+
} else {
|
|
620
|
+
throw new Error(`${thorchainName} inbound is halted or not found - cannot proceed with swap`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} catch (fetchError) {
|
|
624
|
+
console.error(tag, 'Failed to fetch inbound addresses:', fetchError);
|
|
625
|
+
// ABORT - cannot proceed without proper vault address
|
|
626
|
+
throw new Error(`Cannot proceed with THORChain swap - failed to fetch inbound addresses: ${fetchError.message}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Final validation - never use 0x0 as vault
|
|
630
|
+
if (vaultAddress === '0x0000000000000000000000000000000000000000') {
|
|
631
|
+
throw new Error('Cannot proceed with THORChain swap - vault address is invalid (0x0)');
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// CRITICAL SAFETY CHECK: Ensure we're sending to the router, not the vault
|
|
635
|
+
if (to.toLowerCase() === vaultAddress.toLowerCase()) {
|
|
636
|
+
console.warn(tag, '⚠️ WARNING: "to" address equals vault address - this should be the router!');
|
|
637
|
+
console.warn(tag, '⚠️ Using fetched router address instead:', routerAddress);
|
|
638
|
+
to = routerAddress; // Override with correct router
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Validate router address format
|
|
642
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
643
|
+
throw new Error(`Invalid router address format: ${to}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
console.log(tag, '✅ Final validation passed - router:', to, 'vault:', vaultAddress);
|
|
647
|
+
console.log(tag, '✅ ERC20 THORChain swap addresses validated');
|
|
648
|
+
console.log(tag, ' Router:', to);
|
|
649
|
+
console.log(tag, ' Vault:', vaultAddress);
|
|
650
|
+
console.log(tag, ' Token:', contractAddress);
|
|
651
|
+
|
|
652
|
+
// Encode depositWithExpiry for ERC20 tokens
|
|
653
|
+
// Function signature: depositWithExpiry(address,address,uint256,string,uint256)
|
|
654
|
+
const functionSelector = '44bc937b';
|
|
655
|
+
|
|
656
|
+
// Calculate expiry time (current time + 1 hour)
|
|
657
|
+
const expiryTime = Math.floor(Date.now() / 1000) + 3600;
|
|
658
|
+
|
|
659
|
+
// Encode parameters
|
|
660
|
+
const vaultPadded = vaultAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0');
|
|
661
|
+
const assetPadded = contractAddress.toLowerCase().replace(/^0x/, '').padStart(64, '0'); // ERC20 token address
|
|
662
|
+
const amountPadded = amountWei.toString(16).padStart(64, '0');
|
|
663
|
+
|
|
664
|
+
// String offset for depositWithExpiry with 5 parameters
|
|
665
|
+
// Offset must point after all 5 head words: 5 * 32 = 160 = 0xa0
|
|
666
|
+
const stringOffset = (5 * 32).toString(16).padStart(64, '0');
|
|
667
|
+
|
|
668
|
+
const expiryPadded = expiryTime.toString(16).padStart(64, '0');
|
|
669
|
+
|
|
670
|
+
// Encode memo
|
|
671
|
+
const fixedMemo = memo || '';
|
|
672
|
+
const memoBytes = Buffer.from(fixedMemo, 'utf8');
|
|
673
|
+
const memoHex = memoBytes.toString('hex');
|
|
674
|
+
const stringLength = memoBytes.length.toString(16).padStart(64, '0');
|
|
675
|
+
|
|
676
|
+
// Pad memo to 32-byte boundary
|
|
677
|
+
const paddingLength = (32 - (memoBytes.length % 32)) % 32;
|
|
678
|
+
const memoPadded = memoHex + '0'.repeat(paddingLength * 2);
|
|
679
|
+
|
|
680
|
+
// Construct transaction data
|
|
681
|
+
data = '0x' + functionSelector + vaultPadded + assetPadded + amountPadded + stringOffset + expiryPadded + stringLength + memoPadded;
|
|
682
|
+
|
|
683
|
+
// Set recipient to router (NOT token contract)
|
|
684
|
+
finalTo = to; // 'to' has been validated and updated to be the router
|
|
685
|
+
|
|
686
|
+
console.log(tag, '✅ Encoded ERC20 depositWithExpiry:', {
|
|
687
|
+
functionSelector: '0x' + functionSelector,
|
|
688
|
+
vault: vaultAddress,
|
|
689
|
+
asset: contractAddress,
|
|
690
|
+
amount: amountWei.toString(),
|
|
691
|
+
memo: fixedMemo,
|
|
692
|
+
expiry: expiryTime,
|
|
693
|
+
dataLength: data.length,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Increase gas limit for router call (more complex than simple transfer)
|
|
697
|
+
gasLimit = BigInt(300000); // Router calls require more gas than simple transfers
|
|
698
|
+
} else {
|
|
699
|
+
// Regular ERC20 transfer (non-THORChain)
|
|
700
|
+
data = encodeTransferData(to, amountWei);
|
|
701
|
+
finalTo = contractAddress;
|
|
702
|
+
}
|
|
543
703
|
|
|
544
704
|
const ethPriceInUsd = await fetchEthPriceInUsd(pioneer, networkId);
|
|
545
|
-
|
|
705
|
+
// Recalculate gasFee in case gasLimit was updated for THORChain swap
|
|
706
|
+
const finalGasFee = gasPrice * gasLimit;
|
|
707
|
+
const gasFeeUsd = (Number(finalGasFee) / 1e18) * ethPriceInUsd;
|
|
546
708
|
|
|
547
709
|
// For token price, fetch from Pioneer API using the full CAIP
|
|
548
710
|
const tokenPriceInUsd = await fetchTokenPriceInUsd(pioneer, caip);
|
|
@@ -551,10 +713,11 @@ export async function createUnsignedEvmTx(
|
|
|
551
713
|
|
|
552
714
|
unsignedTx = {
|
|
553
715
|
chainId,
|
|
716
|
+
from: address, // Required for simulation
|
|
554
717
|
nonce: toHex(nonce),
|
|
555
718
|
gas: toHex(gasLimit),
|
|
556
719
|
gasPrice: toHex(gasPrice),
|
|
557
|
-
to:
|
|
720
|
+
to: finalTo,
|
|
558
721
|
value: '0x0',
|
|
559
722
|
data,
|
|
560
723
|
// USD estimations
|