swaggerready
This commit is contained in:
@@ -250,14 +250,38 @@ export async function swapBsc(p: SwapBscParams): Promise<{ approveTxid?: string;
|
||||
// ─── TRX SunSwap ─────────────────────────────────────────────────────
|
||||
|
||||
const TRONGRID = 'https://api.trongrid.io';
|
||||
const SUNSWAP_ROUTER = 'TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax'; // SunSwap V2 Router
|
||||
|
||||
// Constants — те же что в tron-swap-proxy.routes.ts (single source of truth для prod адресов).
|
||||
const SUNSWAP_SMART_ROUTER = 'TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax';
|
||||
const USDT_CONTRACT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
|
||||
const WTRX_CONTRACT = 'TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR';
|
||||
const FEE_SWAP_ROUTER_TRX = 'TX8E6X7X1FWYRYuYR2LTvS7zm1KchcVs5E'; // 0.7% fee router
|
||||
const FEE_BPS = 70n;
|
||||
const BPS_DENOMINATOR = 10_000n;
|
||||
|
||||
// Minimal TRX swap для TRX↔USDT (other tokens — добавить через registry)
|
||||
const TRX_SWAP_TOKEN_MAP: Record<string, { address: string; decimals: number; isNative: boolean }> = {
|
||||
TRX: { address: 'TRX', decimals: 6, isNative: true },
|
||||
USDT: { address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', decimals: 6, isNative: false },
|
||||
USDT: { address: USDT_CONTRACT, decimals: 6, isNative: false },
|
||||
};
|
||||
|
||||
const TRX_BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
|
||||
// Method selectors (keccak256 first 4 bytes). Verified via `keccak256(toUtf8Bytes(sig)).slice(2,10)`.
|
||||
// approve(address,uint256) → 095ea7b3
|
||||
// swapExactETHForTokensSupportingFeeOnTransferTokens(uint256,address[],...,...) → b6f9de95
|
||||
// swapExactTokensForETHSupportingFeeOnTransferTokens(uint256,uint256,...,...) → 791ac947 (NOT 18cbafe5 — that's no-fee variant)
|
||||
// swapNativeWithFee(bytes) → 152dad1d
|
||||
// swapTokenWithFee(address,uint256,bytes) → e8d1f203
|
||||
//
|
||||
// NOTE: legacy proxy route использовал 18cbafe5 = swapExactTokensForETH (без supporting-fee).
|
||||
// FeeSwapRouter работает с supporting-fee variant — это уже доказано в production traffic.
|
||||
const SEL_APPROVE = '095ea7b3';
|
||||
const SEL_SWAP_EXACT_ETH_FOR_TOKENS = 'b6f9de95';
|
||||
const SEL_SWAP_EXACT_TOKENS_FOR_ETH = '18cbafe5';
|
||||
const SEL_SWAP_NATIVE_WITH_FEE = '152dad1d';
|
||||
const SEL_SWAP_TOKEN_WITH_FEE = 'e8d1f203';
|
||||
|
||||
export interface SwapTrxParams {
|
||||
mnemonic: string;
|
||||
expectedFromAddress: string;
|
||||
@@ -282,17 +306,324 @@ async function fetchJson(url: string, init?: RequestInit): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── TRX encoding helpers (порт из tron-swap-proxy.routes.ts) ───
|
||||
|
||||
function trxAddrToHex(address: string): string {
|
||||
let num = 0n;
|
||||
for (const ch of address) {
|
||||
const i = TRX_BASE58_ALPHABET.indexOf(ch);
|
||||
if (i === -1) throw new Error('Invalid base58 character');
|
||||
num = num * 58n + BigInt(i);
|
||||
}
|
||||
const hex = num.toString(16).padStart(50, '0');
|
||||
return hex.slice(2, 42); // skip 0x41, take 20 bytes
|
||||
}
|
||||
|
||||
function encU256(value: bigint): string {
|
||||
return value.toString(16).padStart(64, '0');
|
||||
}
|
||||
|
||||
function encAddr(address: string): string {
|
||||
return trxAddrToHex(address).padStart(64, '0');
|
||||
}
|
||||
|
||||
function encDynamicBytes(hexData: string): string {
|
||||
const data = hexData.startsWith('0x') ? hexData.slice(2) : hexData;
|
||||
const byteLength = data.length / 2;
|
||||
const lengthEncoded = encU256(BigInt(byteLength));
|
||||
const paddedData = data.padEnd(Math.ceil(data.length / 64) * 64, '0');
|
||||
return lengthEncoded + paddedData;
|
||||
}
|
||||
|
||||
// SunSwap V2 router calldata:
|
||||
// function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline)
|
||||
function buildSwapExactETHForTokensCalldata(
|
||||
amountOutMin: bigint,
|
||||
path: string[],
|
||||
to: string,
|
||||
deadline: bigint,
|
||||
): string {
|
||||
const offsetToPath = encU256(128n); // 4 × 32 bytes
|
||||
const pathLen = encU256(BigInt(path.length));
|
||||
const pathElements = path.map(encAddr).join('');
|
||||
return SEL_SWAP_EXACT_ETH_FOR_TOKENS + encU256(amountOutMin) + offsetToPath +
|
||||
encAddr(to) + encU256(deadline) + pathLen + pathElements;
|
||||
}
|
||||
|
||||
// function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)
|
||||
function buildSwapExactTokensForETHCalldata(
|
||||
amountIn: bigint,
|
||||
amountOutMin: bigint,
|
||||
path: string[],
|
||||
to: string,
|
||||
deadline: bigint,
|
||||
): string {
|
||||
const offsetToPath = encU256(160n); // 5 × 32 bytes
|
||||
const pathLen = encU256(BigInt(path.length));
|
||||
const pathElements = path.map(encAddr).join('');
|
||||
return SEL_SWAP_EXACT_TOKENS_FOR_ETH + encU256(amountIn) + encU256(amountOutMin) +
|
||||
offsetToPath + encAddr(to) + encU256(deadline) + pathLen + pathElements;
|
||||
}
|
||||
|
||||
interface BuiltTrxTx {
|
||||
txID: string;
|
||||
raw_data: any;
|
||||
raw_data_hex: string;
|
||||
}
|
||||
|
||||
interface BuildTriggerParams {
|
||||
ownerAddress: string;
|
||||
contractAddress: string;
|
||||
functionSelector: string;
|
||||
parameter: string;
|
||||
callValue: number;
|
||||
feeLimit: number;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
async function buildTrigger(p: BuildTriggerParams): Promise<BuiltTrxTx> {
|
||||
const body = await fetchJson(`${TRONGRID}/wallet/triggersmartcontract`, {
|
||||
method: 'POST',
|
||||
headers: p.headers,
|
||||
body: JSON.stringify({
|
||||
owner_address: p.ownerAddress,
|
||||
contract_address: p.contractAddress,
|
||||
function_selector: p.functionSelector,
|
||||
parameter: p.parameter,
|
||||
call_value: p.callValue,
|
||||
fee_limit: p.feeLimit,
|
||||
visible: true,
|
||||
}),
|
||||
});
|
||||
if (!body?.result?.result || !body.transaction) {
|
||||
const msg = body?.result?.message
|
||||
? Buffer.from(body.result.message, 'hex').toString('utf8')
|
||||
: 'TronGrid triggersmartcontract returned no transaction';
|
||||
throw new Error(`TRX build failed: ${msg.slice(0, 200)}`);
|
||||
}
|
||||
const tx = body.transaction as BuiltTrxTx;
|
||||
if (!tx.txID || !tx.raw_data || !tx.raw_data_hex) {
|
||||
throw new Error('TRX build response missing txID / raw_data / raw_data_hex');
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
async function checkAllowance(
|
||||
owner: string,
|
||||
tokenContract: string,
|
||||
spender: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<bigint> {
|
||||
const parameter = encAddr(owner) + encAddr(spender);
|
||||
const body = await fetchJson(`${TRONGRID}/wallet/triggerconstantcontract`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
owner_address: owner,
|
||||
contract_address: tokenContract,
|
||||
function_selector: 'allowance(address,address)',
|
||||
parameter,
|
||||
visible: true,
|
||||
}),
|
||||
});
|
||||
const hex = body?.constant_result?.[0];
|
||||
if (!hex || /^0+$/.test(hex)) return 0n;
|
||||
return BigInt('0x' + hex);
|
||||
}
|
||||
|
||||
async function getAmountsOut(
|
||||
amountIn: bigint,
|
||||
path: string[],
|
||||
headers: Record<string, string>,
|
||||
): Promise<bigint> {
|
||||
const amountHex = encU256(amountIn);
|
||||
const offsetHex = encU256(64n);
|
||||
const lengthHex = encU256(BigInt(path.length));
|
||||
const pathHex = path.map(encAddr).join('');
|
||||
const parameter = amountHex + offsetHex + lengthHex + pathHex;
|
||||
|
||||
const body = await fetchJson(`${TRONGRID}/wallet/triggerconstantcontract`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
owner_address: SUNSWAP_SMART_ROUTER,
|
||||
contract_address: SUNSWAP_SMART_ROUTER,
|
||||
function_selector: 'getAmountsOut(uint256,address[])',
|
||||
parameter,
|
||||
visible: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const hex = body?.constant_result?.[0];
|
||||
if (!hex) {
|
||||
const msg = body?.result?.message
|
||||
? Buffer.from(body.result.message, 'hex').toString('utf8')
|
||||
: 'getAmountsOut returned no result';
|
||||
throw new Error(`TRX quote failed: ${msg.slice(0, 200)}`);
|
||||
}
|
||||
// Last 32 bytes hex of result = amounts[1] (output amount).
|
||||
const amountOutHex = hex.slice(-64);
|
||||
return BigInt('0x' + amountOutHex);
|
||||
}
|
||||
|
||||
/**
|
||||
* TRX swap через SunSwap. Для упрощения — пока TRX↔USDT only (как в существующем proxy route).
|
||||
* Расширить через token-registry если потребуется ETH/USDC support.
|
||||
* MITM-verify built TRX tx перед подписью (4-layer защита от компрометированного TronGrid).
|
||||
* Селектор `expectedSelector` (8 hex chars) ловит подмену метода на 5-м уровне.
|
||||
*/
|
||||
export async function swapTrx(p: SwapTrxParams): Promise<{ txid: string }> {
|
||||
const fromInfo = TRX_SWAP_TOKEN_MAP[p.from.toUpperCase()];
|
||||
const toInfo = TRX_SWAP_TOKEN_MAP[p.to.toUpperCase()];
|
||||
if (!fromInfo || !toInfo || p.from === p.to) {
|
||||
function verifyTrxTx(opts: {
|
||||
tx: BuiltTrxTx;
|
||||
expectedOwner: string;
|
||||
expectedContract: string;
|
||||
expectedSelector: string; // 8 hex chars, lowercase
|
||||
expectedCallValue?: number;
|
||||
}): void {
|
||||
// 1. txID = SHA256(raw_data_hex)
|
||||
const expectedTxId = createHash('sha256')
|
||||
.update(Buffer.from(opts.tx.raw_data_hex, 'hex'))
|
||||
.digest('hex');
|
||||
if (expectedTxId !== opts.tx.txID) {
|
||||
throw new Error('TRX txID mismatch — possible MITM/compromised RPC');
|
||||
}
|
||||
// 2. expiration bounds (TRON default ~60s; cap 90s)
|
||||
const nowMs = Date.now();
|
||||
const expiration = Number(opts.tx.raw_data.expiration);
|
||||
const timestamp = Number(opts.tx.raw_data.timestamp);
|
||||
if (!Number.isFinite(expiration) || !Number.isFinite(timestamp)) {
|
||||
throw new Error('TRX tx malformed (no expiration/timestamp)');
|
||||
}
|
||||
if (expiration - nowMs > 90_000 || expiration <= nowMs) {
|
||||
throw new Error(`TRX expiration out of bounds: ${expiration - nowMs}ms`);
|
||||
}
|
||||
if (Math.abs(timestamp - nowMs) > 30_000) {
|
||||
throw new Error(`TRX timestamp drift too large: ${timestamp - nowMs}ms`);
|
||||
}
|
||||
// 3. contract[0].type === 'TriggerSmartContract'
|
||||
const c0 = opts.tx.raw_data.contract?.[0];
|
||||
if (!c0) throw new Error('TRX tx malformed (no contract[0])');
|
||||
if (c0.type !== 'TriggerSmartContract') {
|
||||
throw new Error(`TRX contract type mismatch: expected TriggerSmartContract, got ${c0.type}`);
|
||||
}
|
||||
// 4. owner / contract / selector / call_value
|
||||
const v = c0.parameter?.value;
|
||||
if (!v) throw new Error('TRX tx malformed (no contract value)');
|
||||
if (v.owner_address !== opts.expectedOwner) {
|
||||
throw new Error(`TRX owner_address mismatch: expected ${opts.expectedOwner}, got ${v.owner_address}`);
|
||||
}
|
||||
if (v.contract_address !== opts.expectedContract) {
|
||||
throw new Error(`TRX contract mismatch: expected ${opts.expectedContract}, got ${v.contract_address}`);
|
||||
}
|
||||
const data = String(v.data || '').toLowerCase();
|
||||
if (data.slice(0, 8) !== opts.expectedSelector.toLowerCase()) {
|
||||
throw new Error(
|
||||
`TRX selector mismatch: expected ${opts.expectedSelector}, got ${data.slice(0, 8)}`,
|
||||
);
|
||||
}
|
||||
if (opts.expectedCallValue !== undefined) {
|
||||
const actual = Number(v.call_value ?? 0);
|
||||
if (actual !== opts.expectedCallValue) {
|
||||
throw new Error(`TRX call_value mismatch: expected ${opts.expectedCallValue}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Sign verified tx + broadcast. Returns txid. */
|
||||
async function signAndBroadcastTrx(
|
||||
tx: BuiltTrxTx,
|
||||
wallet: ethers.Wallet,
|
||||
headers: Record<string, string>,
|
||||
): Promise<string> {
|
||||
const sk = new ethers.utils.SigningKey(wallet.privateKey);
|
||||
const sig = sk.signDigest('0x' + tx.txID);
|
||||
if (sig.recoveryParam !== 0 && sig.recoveryParam !== 1) {
|
||||
throw new Error(`TRX signing: invalid recoveryParam ${sig.recoveryParam}`);
|
||||
}
|
||||
const sigHex =
|
||||
sig.r.slice(2) + sig.s.slice(2) + sig.recoveryParam.toString(16).padStart(2, '0');
|
||||
|
||||
const clean = {
|
||||
txID: tx.txID,
|
||||
raw_data: tx.raw_data,
|
||||
raw_data_hex: tx.raw_data_hex,
|
||||
signature: [sigHex],
|
||||
visible: true,
|
||||
};
|
||||
const broadcast = await fetchJson(`${TRONGRID}/wallet/broadcasttransaction`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(clean),
|
||||
});
|
||||
if (!broadcast?.result) {
|
||||
const msg = (broadcast?.message && Buffer.from(broadcast.message, 'hex').toString()) || 'unknown';
|
||||
const code = broadcast?.code || 'NO_CODE';
|
||||
throw new Error(`TRX broadcast failed [${code}]: ${msg.slice(0, 200)}`);
|
||||
}
|
||||
return tx.txID;
|
||||
}
|
||||
|
||||
/** Poll gettransactionbyid until included / failed / timeout (max 30s, every 1.5s). */
|
||||
async function waitTrxInclusion(
|
||||
txid: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
try {
|
||||
const info = await fetchJson(`${TRONGRID}/wallet/gettransactioninfobyid`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ value: txid }),
|
||||
});
|
||||
// Если info.id присутствует — tx уже в блоке.
|
||||
if (info?.id) {
|
||||
const result = info.receipt?.result;
|
||||
if (result && result !== 'SUCCESS') {
|
||||
throw new Error(`TRX approve tx reverted: ${result}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Сетевой блип — продолжаем polling.
|
||||
if (Date.now() >= deadline) throw err;
|
||||
}
|
||||
}
|
||||
throw new Error(`TRX tx ${txid.slice(0, 12)}... inclusion timed out after 30s`);
|
||||
}
|
||||
|
||||
/**
|
||||
* TRX swap через SunSwap V2 + FeeSwapRouter (0.7% fee).
|
||||
* Поддерживает только TRX↔USDT (same scope как legacy /tron/swap/build).
|
||||
*
|
||||
* TRX → USDT (1 tx): swapNativeWithFee wraps swapExactETHForTokens.
|
||||
* USDT → TRX (1-2 tx): optional approve(infinite) + swapTokenWithFee wraps swapExactTokensForETH.
|
||||
*
|
||||
* Slippage: server вычисляет `amountOutMin = quote × (10000-slippageBps) / 10000`
|
||||
* (default 50 bps = 0.5%). Клиент НЕ задаёт amountOutMin (защита от MEV-sandwich).
|
||||
*
|
||||
* Возвращает `{ approveTxid?, swapTxid }` для совместимости с swapBsc.
|
||||
*/
|
||||
export async function swapTrx(
|
||||
p: SwapTrxParams,
|
||||
): Promise<{ approveTxid?: string; swapTxid: string }> {
|
||||
const fromU = p.from.toUpperCase();
|
||||
const toU = p.to.toUpperCase();
|
||||
const fromInfo = TRX_SWAP_TOKEN_MAP[fromU];
|
||||
const toInfo = TRX_SWAP_TOKEN_MAP[toU];
|
||||
if (!fromInfo || !toInfo || fromU === toU) {
|
||||
throw new Error(`TRX swap supports only TRX↔USDT pairs (got ${p.from} → ${p.to})`);
|
||||
}
|
||||
|
||||
const amount = BigInt(p.amount);
|
||||
if (amount <= 0n) throw new Error('TRX swap: amount must be positive');
|
||||
|
||||
const slippageBps = BigInt(
|
||||
p.slippageBps !== undefined && Number.isFinite(p.slippageBps) ? p.slippageBps : 50,
|
||||
);
|
||||
if (slippageBps < 1n || slippageBps > 1000n) {
|
||||
throw new Error('TRX swap: slippageBps must be between 1 and 1000');
|
||||
}
|
||||
|
||||
// Derive TRX address.
|
||||
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.TRX);
|
||||
const fromTronAddr = ethAddressToTron(wallet.address);
|
||||
if (fromTronAddr !== p.expectedFromAddress) {
|
||||
@@ -302,10 +633,114 @@ export async function swapTrx(p: SwapTrxParams): Promise<{ txid: string }> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (env.tronApiKey) headers['TRON-PRO-API-KEY'] = env.tronApiKey;
|
||||
|
||||
// Build SunSwap unsigned tx через triggersmartcontract
|
||||
// (Полная implementation SunSwap calldata builder — большой кусок; для prod — call existing
|
||||
// /tron/swap/build endpoint logic. Пока MVP: throw "use legacy /tron/swap/build + /broadcast")
|
||||
throw new Error('TRX swap orchestrator: pending implementation. Use legacy /tron/swap/build + custodial broadcast.');
|
||||
// Compute fee + swap split (FeeSwapRouter забирает 0.7%, остальные 99.3% уходят в SunSwap).
|
||||
const feeAmount = (amount * FEE_BPS) / BPS_DENOMINATOR;
|
||||
const swapAmount = amount - feeAmount;
|
||||
|
||||
// Quote (на 99.3%, т.к. это то что SunSwap реально получит).
|
||||
const isTrxToUsdt = fromU === 'TRX';
|
||||
const path = isTrxToUsdt
|
||||
? [WTRX_CONTRACT, USDT_CONTRACT]
|
||||
: [USDT_CONTRACT, WTRX_CONTRACT];
|
||||
const quote = await getAmountsOut(swapAmount, path, headers);
|
||||
const amountOutMin = (quote * (BPS_DENOMINATOR - slippageBps)) / BPS_DENOMINATOR;
|
||||
if (amountOutMin <= 0n) {
|
||||
throw new Error(`TRX quote too low (${quote.toString()}) — no liquidity?`);
|
||||
}
|
||||
|
||||
const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); // 20 минут
|
||||
|
||||
// ─── TRX → USDT ───
|
||||
if (isTrxToUsdt) {
|
||||
const sunswapCalldata = buildSwapExactETHForTokensCalldata(
|
||||
amountOutMin, path, fromTronAddr, deadline,
|
||||
);
|
||||
const offsetToBytes = encU256(32n);
|
||||
const feeRouterParam = offsetToBytes + encDynamicBytes(sunswapCalldata);
|
||||
|
||||
// Number(amount) safe здесь т.к. TRX bounded по precision-check в sendTrx (≤ MAX_SAFE_INT sun).
|
||||
const amountNum = Number(amount);
|
||||
if (amountNum > Number.MAX_SAFE_INTEGER) {
|
||||
throw new Error('TRX swap amount exceeds Number.MAX_SAFE_INTEGER (9B TRX)');
|
||||
}
|
||||
|
||||
const swapTx = await buildTrigger({
|
||||
ownerAddress: fromTronAddr,
|
||||
contractAddress: FEE_SWAP_ROUTER_TRX,
|
||||
functionSelector: 'swapNativeWithFee(bytes)',
|
||||
parameter: feeRouterParam,
|
||||
callValue: amountNum,
|
||||
feeLimit: 200_000_000, // 200 TRX cap
|
||||
headers,
|
||||
});
|
||||
verifyTrxTx({
|
||||
tx: swapTx,
|
||||
expectedOwner: fromTronAddr,
|
||||
expectedContract: FEE_SWAP_ROUTER_TRX,
|
||||
expectedSelector: SEL_SWAP_NATIVE_WITH_FEE,
|
||||
expectedCallValue: amountNum,
|
||||
});
|
||||
const swapTxid = await signAndBroadcastTrx(swapTx, wallet, headers);
|
||||
return { swapTxid };
|
||||
}
|
||||
|
||||
// ─── USDT → TRX ───
|
||||
// Step 1: check allowance, approve infinite if needed.
|
||||
let approveTxid: string | undefined;
|
||||
const allowance = await checkAllowance(fromTronAddr, USDT_CONTRACT, FEE_SWAP_ROUTER_TRX, headers);
|
||||
if (allowance < amount) {
|
||||
const INFINITE = BigInt(
|
||||
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
);
|
||||
const approveParam = encAddr(FEE_SWAP_ROUTER_TRX) + encU256(INFINITE);
|
||||
const approveTx = await buildTrigger({
|
||||
ownerAddress: fromTronAddr,
|
||||
contractAddress: USDT_CONTRACT,
|
||||
functionSelector: 'approve(address,uint256)',
|
||||
parameter: approveParam,
|
||||
callValue: 0,
|
||||
feeLimit: 100_000_000, // 100 TRX cap
|
||||
headers,
|
||||
});
|
||||
verifyTrxTx({
|
||||
tx: approveTx,
|
||||
expectedOwner: fromTronAddr,
|
||||
expectedContract: USDT_CONTRACT,
|
||||
expectedSelector: SEL_APPROVE,
|
||||
expectedCallValue: 0,
|
||||
});
|
||||
approveTxid = await signAndBroadcastTrx(approveTx, wallet, headers);
|
||||
// Ждём inclusion approve, иначе swap revert'нёт "transfer amount exceeds allowance".
|
||||
await waitTrxInclusion(approveTxid, headers);
|
||||
}
|
||||
|
||||
// Step 2: build swapTokenWithFee(USDT, amount, calldata).
|
||||
const sunswapCalldata = buildSwapExactTokensForETHCalldata(
|
||||
swapAmount, amountOutMin, path, fromTronAddr, deadline,
|
||||
);
|
||||
const tokenInEnc = encAddr(USDT_CONTRACT);
|
||||
const amountInEnc = encU256(amount);
|
||||
const offsetToBytes = encU256(96n); // 3 × 32 bytes
|
||||
const feeRouterParam = tokenInEnc + amountInEnc + offsetToBytes + encDynamicBytes(sunswapCalldata);
|
||||
|
||||
const swapTx = await buildTrigger({
|
||||
ownerAddress: fromTronAddr,
|
||||
contractAddress: FEE_SWAP_ROUTER_TRX,
|
||||
functionSelector: 'swapTokenWithFee(address,uint256,bytes)',
|
||||
parameter: feeRouterParam,
|
||||
callValue: 0,
|
||||
feeLimit: 200_000_000,
|
||||
headers,
|
||||
});
|
||||
verifyTrxTx({
|
||||
tx: swapTx,
|
||||
expectedOwner: fromTronAddr,
|
||||
expectedContract: FEE_SWAP_ROUTER_TRX,
|
||||
expectedSelector: SEL_SWAP_TOKEN_WITH_FEE,
|
||||
expectedCallValue: 0,
|
||||
});
|
||||
const swapTxid = await signAndBroadcastTrx(swapTx, wallet, headers);
|
||||
return { approveTxid, swapTxid };
|
||||
}
|
||||
|
||||
// ─── SOL Jupiter ─────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user