@pioneer-platform/pioneer-sdk 8.12.0 → 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 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 (isThorchainOperation) {
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 !== "" && !isThorchainOperation) {
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 ethInbound = inboundData.find((inbound) => inbound.chain === "ETH" && !inbound.halted);
1129
- if (ethInbound) {
1130
- vaultAddress = ethInbound.address;
1131
- routerAddress = ethInbound.router || to;
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("ETH inbound is halted or not found - cannot proceed with swap");
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
- const data = encodeTransferData(to, amountWei);
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 gasFeeUsd = Number(gasFee) / 1000000000000000000 * ethPriceInUsd;
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: contractAddress,
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, "Using pre-built EVM transaction from integration");
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 (isThorchainOperation) {
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 !== "" && !isThorchainOperation) {
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 ethInbound = inboundData.find((inbound) => inbound.chain === "ETH" && !inbound.halted);
1305
- if (ethInbound) {
1306
- vaultAddress = ethInbound.address;
1307
- routerAddress = ethInbound.router || to;
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("ETH inbound is halted or not found - cannot proceed with swap");
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
- const data = encodeTransferData(to, amountWei);
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 gasFeeUsd = Number(gasFee) / 1000000000000000000 * ethPriceInUsd;
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: contractAddress,
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, "Using pre-built EVM transaction from integration");
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 (isThorchainOperation) {
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 !== "" && !isThorchainOperation) {
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 ethInbound = inboundData.find((inbound) => inbound.chain === "ETH" && !inbound.halted);
1305
- if (ethInbound) {
1306
- vaultAddress = ethInbound.address;
1307
- routerAddress = ethInbound.router || to;
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("ETH inbound is halted or not found - cannot proceed with swap");
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
- const data = encodeTransferData(to, amountWei);
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 gasFeeUsd = Number(gasFee) / 1000000000000000000 * ethPriceInUsd;
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: contractAddress,
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, "Using pre-built EVM transaction from integration");
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,7 +1,7 @@
1
1
  {
2
2
  "author": "highlander",
3
3
  "name": "@pioneer-platform/pioneer-sdk",
4
- "version": "8.12.0",
4
+ "version": "8.12.2",
5
5
  "dependencies": {
6
6
  "@keepkey/keepkey-sdk": "^0.2.62",
7
7
  "@pioneer-platform/loggerdog": "^8.11.0",
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 - use the pre-built transaction from integration
1307
- console.log(tag, 'Using pre-built EVM transaction from integration');
1308
- unsignedTx = tx.txParams;
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
- // Check if this is a THORChain swap (needs more gas for contract call)
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 (isThorchainOperation) {
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 !== '' && !isThorchainOperation) {
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
- // Check if this is a THORChain swap (memo starts with '=' or 'SWAP' or contains ':')
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
- // Find ETH inbound data
370
- const ethInbound = inboundData.find(inbound =>
371
- inbound.chain === 'ETH' && !inbound.halted
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 (ethInbound) {
374
- vaultAddress = ethInbound.address; // This is the Asgard vault
375
- routerAddress = ethInbound.router || to; // Use fetched router or fallback to 'to'
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('ETH inbound is halted or not found - cannot proceed with swap');
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
- const data = encodeTransferData(to, amountWei);
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
- const gasFeeUsd = (Number(gasFee) / 1e18) * ethPriceInUsd;
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: contractAddress,
720
+ to: finalTo,
558
721
  value: '0x0',
559
722
  data,
560
723
  // USD estimations