@pioneer-platform/pioneer-sdk 8.12.0 → 8.12.4

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