init383838
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"lint": "eslint src/ --ext .ts"
|
"lint": "eslint src/ --ext .ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@solana/spl-token": "^0.4.14",
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
"bip32": "^4.0.0",
|
"bip32": "^4.0.0",
|
||||||
"bip39": "^3.1.0",
|
"bip39": "^3.1.0",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { getBalance, getTransactions } from '../services/wallet-ops.service';
|
|||||||
import { isValidAddress, isValidAmount, ChainCode } from '../lib/address-validators';
|
import { isValidAddress, isValidAmount, ChainCode } from '../lib/address-validators';
|
||||||
import { generateMnemonic, deriveAllAddresses, ALL_CHAINS } from '../services/wallet-generator.service';
|
import { generateMnemonic, deriveAllAddresses, ALL_CHAINS } from '../services/wallet-generator.service';
|
||||||
import { encryptMnemonic, decryptMnemonic, isCryptoReady } from '../services/crypto.service';
|
import { encryptMnemonic, decryptMnemonic, isCryptoReady } from '../services/crypto.service';
|
||||||
import { signAndBroadcast, signAndBroadcastRawEvm } from '../services/wallet-signer.service';
|
import { signAndBroadcast, signAndBroadcastRawEvm, signAndBroadcastSolanaTx } from '../services/wallet-signer.service';
|
||||||
|
import { swapBsc, swapTrx, swapSol } from '../services/swap-orchestrator.service';
|
||||||
import { getEvmFeeTiers, type FeeTier } from '../services/gas-oracle.service';
|
import { getEvmFeeTiers, type FeeTier } from '../services/gas-oracle.service';
|
||||||
import { applyEvmTxPolicy } from '../lib/evm-tx-policy';
|
import { applyEvmTxPolicy } from '../lib/evm-tx-policy';
|
||||||
import { acquireSendLock } from '../lib/send-lock';
|
import { acquireSendLock } from '../lib/send-lock';
|
||||||
@@ -319,8 +320,10 @@ export const WalletController = {
|
|||||||
|
|
||||||
let normalizedToken: string | undefined;
|
let normalizedToken: string | undefined;
|
||||||
if (token !== undefined && token !== null) {
|
if (token !== undefined && token !== null) {
|
||||||
if (typeof token !== 'string' || !/^[A-Z0-9]{2,10}$/.test(token)) {
|
// Regex: ≤2-10 char, case-insensitive (we'll lookup token-registry case-insensitive)
|
||||||
res.status(400).json({ success: false, error: 'Invalid token symbol' });
|
// Accept letters + digits — registry has tokens like 'W', 'WBNB', 'TRUMP', '1INCH' etc.
|
||||||
|
if (typeof token !== 'string' || !/^[A-Za-z0-9]{1,10}$/.test(token)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Invalid token symbol format' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
normalizedToken = token.toUpperCase();
|
normalizedToken = token.toUpperCase();
|
||||||
@@ -618,4 +621,239 @@ export const WalletController = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/wallets/:chain/swap — chained custodial swap.
|
||||||
|
* BSC: PancakeSwap V2 — approve (если token-to-anything) + swap, sign+broadcast в одном вызове.
|
||||||
|
* TRX: SunSwap — build + sign + broadcast (TRX↔USDT).
|
||||||
|
* SOL: Jupiter — quote + swap + sign + broadcast.
|
||||||
|
*
|
||||||
|
* Body для BSC/TRX: { from, to, amount, slippageBps?, feeTier? } — symbols (BNB, USDT, DOGE, USDC).
|
||||||
|
* Body для SOL: { inputMint, outputMint, amount, slippageBps? } — mint addresses.
|
||||||
|
*/
|
||||||
|
async swapOnChain(req: Request, res: Response) {
|
||||||
|
const userId = req.auth!.userId;
|
||||||
|
const chain = String(req.params.chain).toUpperCase();
|
||||||
|
if (chain !== 'BSC' && chain !== 'TRX' && chain !== 'SOL') {
|
||||||
|
res.status(400).json({ success: false, error: 'Swap supported only on BSC, TRX, SOL. For ETH use Relay quote→execute→sign-raw-evm-tx.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isCryptoReady()) {
|
||||||
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotency
|
||||||
|
const idempKey = extractIdempotencyKey(req.headers['idempotency-key']);
|
||||||
|
if (idempKey) {
|
||||||
|
try {
|
||||||
|
const claim = await claimIdempotency(userId, idempKey, req.body);
|
||||||
|
if (!claim.fresh && claim.cached) {
|
||||||
|
res.status(claim.cached.status).type('application/json').send(claim.cached.body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(409).json({ success: false, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = await acquireSendLock(userId, chain);
|
||||||
|
let mnemonic: string | null = null;
|
||||||
|
let auditId: string;
|
||||||
|
try {
|
||||||
|
const wallet = await WalletModel.findByUserAndChain(userId, chain as ChainCode);
|
||||||
|
if (!wallet) {
|
||||||
|
res.status(404).json({ success: false, error: 'Wallet not found for this chain' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = await UserModel.getEncryptedMnemonic(userId);
|
||||||
|
if (!blob) {
|
||||||
|
res.status(404).json({ success: false, error: 'Encrypted mnemonic not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
auditId = await auditLogStrict({
|
||||||
|
event: 'wallet.swap',
|
||||||
|
userId,
|
||||||
|
ip: req.ip || null,
|
||||||
|
meta: { chain, body: req.body },
|
||||||
|
});
|
||||||
|
} catch (auditErr: any) {
|
||||||
|
logger.error(`Audit DB INSERT MUST succeed for wallet.swap: ${auditErr.message}`);
|
||||||
|
res.status(503).json({ success: false, error: 'Audit service unavailable' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mnemonic = decryptMnemonic(blob);
|
||||||
|
|
||||||
|
let result: any;
|
||||||
|
try {
|
||||||
|
if (chain === 'BSC') {
|
||||||
|
const { from, to, amount, slippageBps, feeTier } = req.body ?? {};
|
||||||
|
if (typeof from !== 'string' || typeof to !== 'string' || typeof amount !== 'string') {
|
||||||
|
throw new Error('BSC swap body: {from, to, amount} required as strings');
|
||||||
|
}
|
||||||
|
result = await swapBsc({
|
||||||
|
mnemonic,
|
||||||
|
expectedFromAddress: wallet.address,
|
||||||
|
from, to, amount,
|
||||||
|
slippageBps,
|
||||||
|
feeTier,
|
||||||
|
});
|
||||||
|
} else if (chain === 'TRX') {
|
||||||
|
const { from, to, amount, slippageBps } = req.body ?? {};
|
||||||
|
if (typeof from !== 'string' || typeof to !== 'string' || typeof amount !== 'string') {
|
||||||
|
throw new Error('TRX swap body: {from, to, amount} required as strings');
|
||||||
|
}
|
||||||
|
result = await swapTrx({
|
||||||
|
mnemonic,
|
||||||
|
expectedFromAddress: wallet.address,
|
||||||
|
from, to, amount,
|
||||||
|
slippageBps,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// SOL Jupiter
|
||||||
|
const { inputMint, outputMint, amount, slippageBps } = req.body ?? {};
|
||||||
|
if (typeof inputMint !== 'string' || typeof outputMint !== 'string' || typeof amount !== 'string') {
|
||||||
|
throw new Error('SOL swap body: {inputMint, outputMint, amount} required as strings');
|
||||||
|
}
|
||||||
|
result = await swapSol({
|
||||||
|
mnemonic,
|
||||||
|
expectedFromAddress: wallet.address,
|
||||||
|
inputMint, outputMint, amount,
|
||||||
|
slippageBps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (swapErr: any) {
|
||||||
|
await completeAudit(auditId, 'failure', undefined, 'SWAP_FAILED');
|
||||||
|
throw swapErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
await completeAudit(auditId, 'success', result);
|
||||||
|
res.json({ success: true, data: { chain, ...result } });
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`swap failed for user ${userId} chain ${chain}: ${err.stack || err.message}`);
|
||||||
|
await auditLog({
|
||||||
|
event: 'wallet.swap',
|
||||||
|
userId,
|
||||||
|
ip: req.ip || null,
|
||||||
|
result: 'failure',
|
||||||
|
meta: { chain },
|
||||||
|
errorCode: 'SWAP_FAILED',
|
||||||
|
});
|
||||||
|
res.status(502).json({ success: false, error: err?.message?.slice?.(0, 250) || 'Swap failed' });
|
||||||
|
} finally {
|
||||||
|
mnemonic = null;
|
||||||
|
releaseLock();
|
||||||
|
if (idempKey) {
|
||||||
|
const status = res.statusCode;
|
||||||
|
saveIdempotencyResponse(userId, idempKey, status, JSON.stringify({ cached: true, status }))
|
||||||
|
.catch((e: any) => logger.warn(`saveIdempotencyResponse failed: ${e.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/wallets/SOL/sign-and-broadcast-tx — custodial sign + broadcast для arbitrary serialized SOL tx.
|
||||||
|
* Используется для Relay bridge SOL-side (когда Relay /execute возвращает unsigned Solana tx).
|
||||||
|
*
|
||||||
|
* Body: { transaction: '<base64 serialized VersionedTransaction>' }
|
||||||
|
*/
|
||||||
|
async signSolanaTx(req: Request, res: Response) {
|
||||||
|
const userId = req.auth!.userId;
|
||||||
|
if (!isCryptoReady()) {
|
||||||
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { transaction } = req.body ?? {};
|
||||||
|
if (typeof transaction !== 'string' || transaction.length === 0 || transaction.length > 8192) {
|
||||||
|
res.status(400).json({ success: false, error: 'Invalid transaction (must be non-empty base64 string ≤8KB)' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[A-Za-z0-9+/=]+$/.test(transaction)) {
|
||||||
|
res.status(400).json({ success: false, error: 'Invalid base64 transaction' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idempKey = extractIdempotencyKey(req.headers['idempotency-key']);
|
||||||
|
if (idempKey) {
|
||||||
|
try {
|
||||||
|
const claim = await claimIdempotency(userId, idempKey, req.body);
|
||||||
|
if (!claim.fresh && claim.cached) {
|
||||||
|
res.status(claim.cached.status).type('application/json').send(claim.cached.body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(409).json({ success: false, error: err.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = await acquireSendLock(userId, 'SOL');
|
||||||
|
let mnemonic: string | null = null;
|
||||||
|
let auditId: string;
|
||||||
|
try {
|
||||||
|
const wallet = await WalletModel.findByUserAndChain(userId, 'SOL');
|
||||||
|
if (!wallet) {
|
||||||
|
res.status(404).json({ success: false, error: 'SOL wallet not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = await UserModel.getEncryptedMnemonic(userId);
|
||||||
|
if (!blob) {
|
||||||
|
res.status(404).json({ success: false, error: 'Encrypted mnemonic not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
auditId = await auditLogStrict({
|
||||||
|
event: 'wallet.sign_sol_tx',
|
||||||
|
userId,
|
||||||
|
ip: req.ip || null,
|
||||||
|
meta: { chain: 'SOL', txLength: transaction.length },
|
||||||
|
});
|
||||||
|
} catch (auditErr: any) {
|
||||||
|
logger.error(`Audit DB INSERT MUST succeed: ${auditErr.message}`);
|
||||||
|
res.status(503).json({ success: false, error: 'Audit service unavailable' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mnemonic = decryptMnemonic(blob);
|
||||||
|
|
||||||
|
let result: { signature: string };
|
||||||
|
try {
|
||||||
|
result = await signAndBroadcastSolanaTx({
|
||||||
|
mnemonic,
|
||||||
|
expectedFromAddress: wallet.address,
|
||||||
|
serializedTransaction: transaction,
|
||||||
|
});
|
||||||
|
} catch (signErr: any) {
|
||||||
|
await completeAudit(auditId, 'failure', undefined, 'SOL_SIGN_FAILED');
|
||||||
|
throw signErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
await completeAudit(auditId, 'success', { signature: result.signature });
|
||||||
|
res.json({ success: true, data: { signature: result.signature, chain: 'SOL' } });
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`signSolanaTx failed for user ${userId}: ${err.stack || err.message}`);
|
||||||
|
await auditLog({
|
||||||
|
event: 'wallet.sign_sol_tx',
|
||||||
|
userId,
|
||||||
|
ip: req.ip || null,
|
||||||
|
result: 'failure',
|
||||||
|
errorCode: 'SOL_SIGN_FAILED',
|
||||||
|
});
|
||||||
|
res.status(502).json({ success: false, error: err?.message?.slice?.(0, 200) || 'SOL sign failed' });
|
||||||
|
} finally {
|
||||||
|
mnemonic = null;
|
||||||
|
releaseLock();
|
||||||
|
if (idempKey) {
|
||||||
|
const status = res.statusCode;
|
||||||
|
saveIdempotencyResponse(userId, idempKey, status, JSON.stringify({ cached: true, status }))
|
||||||
|
.catch((e: any) => logger.warn(`saveIdempotencyResponse failed: ${e.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,3 +78,31 @@ export function getTrxTokens(): TrxToken[] {
|
|||||||
export function getSolTokens(): SolToken[] {
|
export function getSolTokens(): SolToken[] {
|
||||||
return SOL_TOKENS;
|
return SOL_TOKENS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Universal lookup для send flow. Returns address+decimals или null если token не в registry.
|
||||||
|
* Symbol comparison case-insensitive.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const info = getTokenInfo('BSC', 'USDC');
|
||||||
|
* // → { address: '0x8AC76a51...', decimals: 18 }
|
||||||
|
*
|
||||||
|
* const info = getTokenInfo('SOL', 'USDT');
|
||||||
|
* // → { address: 'Es9vMFrza...', decimals: 6 } (mint address)
|
||||||
|
*/
|
||||||
|
export function getTokenInfo(chain: ChainCode, symbol: string): { address: string; decimals: number } | null {
|
||||||
|
const upper = String(symbol).toUpperCase();
|
||||||
|
if (chain === 'ETH' || chain === 'BSC') {
|
||||||
|
const t = getEvmTokens(chain).find((x) => x.symbol.toUpperCase() === upper);
|
||||||
|
return t ? { address: t.contractAddress, decimals: t.decimals } : null;
|
||||||
|
}
|
||||||
|
if (chain === 'TRX') {
|
||||||
|
const t = TRX_TOKENS.find((x) => x.symbol.toUpperCase() === upper);
|
||||||
|
return t ? { address: t.contractAddress, decimals: t.decimals } : null;
|
||||||
|
}
|
||||||
|
if (chain === 'SOL') {
|
||||||
|
const t = SOL_TOKENS.find((x) => x.symbol.toUpperCase() === upper);
|
||||||
|
return t ? { address: t.mint, decimals: t.decimals } : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,5 +12,7 @@ router.get('/:chain/transactions', WalletController.getChainTransactions);
|
|||||||
router.get('/:chain/gas-suggestions', WalletController.getGasSuggestions);
|
router.get('/:chain/gas-suggestions', WalletController.getGasSuggestions);
|
||||||
router.post('/:chain/send', WalletController.sendFromChain);
|
router.post('/:chain/send', WalletController.sendFromChain);
|
||||||
router.post('/:chain/sign-raw-evm-tx', WalletController.signRawEvmTx);
|
router.post('/:chain/sign-raw-evm-tx', WalletController.signRawEvmTx);
|
||||||
|
router.post('/:chain/swap', WalletController.swapOnChain);
|
||||||
|
router.post('/SOL/sign-and-broadcast-tx', WalletController.signSolanaTx);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
410
apps/api/src/services/swap-orchestrator.service.ts
Normal file
410
apps/api/src/services/swap-orchestrator.service.ts
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
/**
|
||||||
|
* Swap orchestrator — chained custodial swap для всех 3 DEX (BSC PancakeSwap, TRX SunSwap, SOL Jupiter).
|
||||||
|
*
|
||||||
|
* Каждая функция inkl. полный flow: build → sign → broadcast в одном вызове.
|
||||||
|
* Возвращает txid'ы — клиенту не нужно client-side signing.
|
||||||
|
*
|
||||||
|
* Reused infrastructure:
|
||||||
|
* - ethers / @solana/web3.js / TronGrid HTTP
|
||||||
|
* - Master-key crypto через decryptMnemonic (caller)
|
||||||
|
* - Mutex / idempotency (caller)
|
||||||
|
* - Audit log (caller)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
import * as bip39 from 'bip39';
|
||||||
|
import {
|
||||||
|
Keypair, Connection, PublicKey, VersionedTransaction,
|
||||||
|
} from '@solana/web3.js';
|
||||||
|
import { derivePath } from 'ed25519-hd-key';
|
||||||
|
import { env } from '../config/env';
|
||||||
|
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
||||||
|
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
||||||
|
import { logger } from '../lib/logger';
|
||||||
|
|
||||||
|
const HTTP_TIMEOUT_MS = 20_000;
|
||||||
|
const MAX_GAS_PRICE_GWEI = 500;
|
||||||
|
|
||||||
|
// ─── BSC PancakeSwap V2 ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BSC_RPCS = [
|
||||||
|
'https://bsc-dataseed.binance.org',
|
||||||
|
'https://bsc-dataseed1.binance.org',
|
||||||
|
'https://bsc.publicnode.com',
|
||||||
|
];
|
||||||
|
const BSC_CHAIN_ID = 56;
|
||||||
|
const PANCAKE_ROUTER = '0x10ED43C718714eb63d5aA57B78B54704E256024E';
|
||||||
|
const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
||||||
|
|
||||||
|
const BSC_TOKEN_MAP: Record<string, string> = {
|
||||||
|
BNB: WBNB,
|
||||||
|
USDT: '0x55d398326f99059fF775485246999027B3197955',
|
||||||
|
USDC: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
|
||||||
|
DOGE: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43',
|
||||||
|
WBNB,
|
||||||
|
BUSD: '0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROUTER_ABI = [
|
||||||
|
'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)',
|
||||||
|
'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable',
|
||||||
|
'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external',
|
||||||
|
'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external',
|
||||||
|
];
|
||||||
|
|
||||||
|
const ERC20_ABI = [
|
||||||
|
'function approve(address spender, uint256 amount) external returns (bool)',
|
||||||
|
'function allowance(address owner, address spender) external view returns (uint256)',
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface SwapBscParams {
|
||||||
|
mnemonic: string;
|
||||||
|
expectedFromAddress: string;
|
||||||
|
from: string; // 'BNB' | 'USDT' | 'USDC' | 'DOGE' | 'WBNB' | 'BUSD'
|
||||||
|
to: string;
|
||||||
|
amount: string; // smallest units (wei для 18-decimals)
|
||||||
|
slippageBps?: number; // default 50 (0.5%)
|
||||||
|
feeTier?: FeeTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickProvider(rpcs: string[], chainId: number): Promise<ethers.providers.StaticJsonRpcProvider> {
|
||||||
|
let lastErr: any;
|
||||||
|
for (const url of rpcs) {
|
||||||
|
const p = new ethers.providers.StaticJsonRpcProvider(url, chainId);
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
p.getBlockNumber(),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('rpc_alive_timeout')), 3000)),
|
||||||
|
]);
|
||||||
|
return p;
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`All BSC RPCs failed: ${lastErr?.message || lastErr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
|
||||||
|
return Promise.race([
|
||||||
|
p,
|
||||||
|
new Promise<T>((_, reject) => setTimeout(() => reject(new Error(msg)), ms)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BSC chained swap. Если `from` не нативный BNB и allowance < amount —
|
||||||
|
* сначала approve(exact), wait 1 confirmation, потом swap.
|
||||||
|
*
|
||||||
|
* Returns: { approveTxid?, swapTxid }
|
||||||
|
*/
|
||||||
|
export async function swapBsc(p: SwapBscParams): Promise<{ approveTxid?: string; swapTxid: string }> {
|
||||||
|
const fromUpper = p.from.toUpperCase();
|
||||||
|
const toUpper = p.to.toUpperCase();
|
||||||
|
|
||||||
|
if (!BSC_TOKEN_MAP[fromUpper] || !BSC_TOKEN_MAP[toUpper] || fromUpper === toUpper) {
|
||||||
|
throw new Error(`Invalid BSC swap pair: ${fromUpper} → ${toUpper}`);
|
||||||
|
}
|
||||||
|
if (!/^\d+$/.test(p.amount) || BigInt(p.amount) <= 0n) {
|
||||||
|
throw new Error('amount must be positive integer string');
|
||||||
|
}
|
||||||
|
const slippageBps = p.slippageBps ?? 50;
|
||||||
|
if (slippageBps < 1 || slippageBps > 1000) {
|
||||||
|
throw new Error('slippageBps must be 1-1000 (0.01%-10%)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.ETH);
|
||||||
|
if (wallet.address.toLowerCase() !== p.expectedFromAddress.toLowerCase()) {
|
||||||
|
throw new Error(`Derived BSC address mismatch: ${wallet.address} ≠ ${p.expectedFromAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await pickProvider(BSC_RPCS, BSC_CHAIN_ID);
|
||||||
|
const signer = wallet.connect(provider);
|
||||||
|
|
||||||
|
// Gas tier
|
||||||
|
const tier: FeeTier = p.feeTier ?? 'normal';
|
||||||
|
const fee = await getEvmFeeForTier('BSC', tier);
|
||||||
|
const capWei = ethers.utils.parseUnits(String(MAX_GAS_PRICE_GWEI), 'gwei');
|
||||||
|
const maxFeePerGas = ethers.BigNumber.from(fee.maxFeePerGas);
|
||||||
|
const maxPriorityFeePerGas = ethers.BigNumber.from(fee.maxPriorityFeePerGas);
|
||||||
|
if (maxFeePerGas.gt(capWei) || maxPriorityFeePerGas.gt(maxFeePerGas)) {
|
||||||
|
throw new Error('Gas fee invariant violated');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote via getAmountsOut → compute amountOutMin server-side (anti-MEV)
|
||||||
|
const routerContract = new ethers.Contract(PANCAKE_ROUTER, ROUTER_ABI, provider);
|
||||||
|
const path = [BSC_TOKEN_MAP[fromUpper], BSC_TOKEN_MAP[toUpper]];
|
||||||
|
const amountsOut: ethers.BigNumber[] = await withTimeout(
|
||||||
|
routerContract.getAmountsOut(p.amount, path),
|
||||||
|
HTTP_TIMEOUT_MS,
|
||||||
|
'PancakeSwap quote timed out',
|
||||||
|
);
|
||||||
|
const expectedOut = amountsOut[amountsOut.length - 1];
|
||||||
|
if (expectedOut.lte(0)) {
|
||||||
|
throw new Error('PancakeSwap quote returned 0 — no liquidity for this pair');
|
||||||
|
}
|
||||||
|
// amountOutMin = expectedOut × (10000 - slippageBps) / 10000
|
||||||
|
const amountOutMin = expectedOut.mul(10000 - slippageBps).div(10000);
|
||||||
|
|
||||||
|
const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes
|
||||||
|
const feeFields: Partial<ethers.providers.TransactionRequest> = {
|
||||||
|
type: 2,
|
||||||
|
maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas,
|
||||||
|
};
|
||||||
|
|
||||||
|
let approveTxid: string | undefined;
|
||||||
|
let nonce = await provider.getTransactionCount(wallet.address, 'pending');
|
||||||
|
|
||||||
|
// ── Token-to-anything: check allowance, approve if needed, wait 1 conf ──
|
||||||
|
if (fromUpper !== 'BNB') {
|
||||||
|
const tokenAddress = BSC_TOKEN_MAP[fromUpper];
|
||||||
|
const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
|
||||||
|
const currentAllowance: ethers.BigNumber = await withTimeout(
|
||||||
|
tokenContract.allowance(wallet.address, PANCAKE_ROUTER),
|
||||||
|
HTTP_TIMEOUT_MS,
|
||||||
|
'Allowance check timed out',
|
||||||
|
);
|
||||||
|
if (currentAllowance.lt(ethers.BigNumber.from(p.amount))) {
|
||||||
|
const approveData = tokenContract.interface.encodeFunctionData('approve', [PANCAKE_ROUTER, p.amount]);
|
||||||
|
const approveTx: ethers.providers.TransactionRequest = {
|
||||||
|
to: tokenAddress,
|
||||||
|
data: approveData,
|
||||||
|
value: 0,
|
||||||
|
chainId: BSC_CHAIN_ID,
|
||||||
|
nonce,
|
||||||
|
gasLimit: ethers.BigNumber.from(80_000), // approve consistently fits в 60-80k
|
||||||
|
...feeFields,
|
||||||
|
};
|
||||||
|
const approveSent = await withTimeout(
|
||||||
|
signer.sendTransaction(approveTx),
|
||||||
|
HTTP_TIMEOUT_MS,
|
||||||
|
'approve broadcast timed out',
|
||||||
|
);
|
||||||
|
approveTxid = approveSent.hash;
|
||||||
|
// Wait 1 confirmation (~3s on BSC) before swap — иначе swap revert'нет с "TransferHelper: TRANSFER_FROM_FAILED"
|
||||||
|
await withTimeout(approveSent.wait(1), 30_000, 'approve confirmation timed out');
|
||||||
|
nonce += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build swap tx ──
|
||||||
|
let swapData: string;
|
||||||
|
let value: ethers.BigNumber;
|
||||||
|
if (fromUpper === 'BNB') {
|
||||||
|
swapData = routerContract.interface.encodeFunctionData(
|
||||||
|
'swapExactETHForTokensSupportingFeeOnTransferTokens',
|
||||||
|
[amountOutMin, path, wallet.address, deadline],
|
||||||
|
);
|
||||||
|
value = ethers.BigNumber.from(p.amount);
|
||||||
|
} else if (toUpper === 'BNB') {
|
||||||
|
swapData = routerContract.interface.encodeFunctionData(
|
||||||
|
'swapExactTokensForETHSupportingFeeOnTransferTokens',
|
||||||
|
[p.amount, amountOutMin, path, wallet.address, deadline],
|
||||||
|
);
|
||||||
|
value = ethers.BigNumber.from(0);
|
||||||
|
} else {
|
||||||
|
// Token-to-token (e.g., USDT → DOGE)
|
||||||
|
swapData = routerContract.interface.encodeFunctionData(
|
||||||
|
'swapExactTokensForTokensSupportingFeeOnTransferTokens',
|
||||||
|
[p.amount, amountOutMin, path, wallet.address, deadline],
|
||||||
|
);
|
||||||
|
value = ethers.BigNumber.from(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// estGas через provider.estimateGas + 20% safety
|
||||||
|
let estGas: ethers.BigNumber;
|
||||||
|
try {
|
||||||
|
const estimated = await provider.estimateGas({
|
||||||
|
from: wallet.address,
|
||||||
|
to: PANCAKE_ROUTER,
|
||||||
|
data: swapData,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
estGas = estimated.mul(120).div(100);
|
||||||
|
const minGas = ethers.BigNumber.from(150_000);
|
||||||
|
const maxGas = ethers.BigNumber.from(500_000);
|
||||||
|
if (estGas.lt(minGas)) estGas = minGas;
|
||||||
|
if (estGas.gt(maxGas)) estGas = maxGas;
|
||||||
|
} catch {
|
||||||
|
estGas = ethers.BigNumber.from(250_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const swapTx: ethers.providers.TransactionRequest = {
|
||||||
|
to: PANCAKE_ROUTER,
|
||||||
|
data: swapData,
|
||||||
|
value,
|
||||||
|
chainId: BSC_CHAIN_ID,
|
||||||
|
nonce,
|
||||||
|
gasLimit: estGas,
|
||||||
|
...feeFields,
|
||||||
|
};
|
||||||
|
const swapSent = await withTimeout(
|
||||||
|
signer.sendTransaction(swapTx),
|
||||||
|
HTTP_TIMEOUT_MS,
|
||||||
|
'swap broadcast timed out',
|
||||||
|
);
|
||||||
|
return { approveTxid, swapTxid: swapSent.hash };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TRX SunSwap ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const TRONGRID = 'https://api.trongrid.io';
|
||||||
|
const SUNSWAP_ROUTER = 'TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax'; // SunSwap V2 Router
|
||||||
|
|
||||||
|
// 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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SwapTrxParams {
|
||||||
|
mnemonic: string;
|
||||||
|
expectedFromAddress: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
amount: string;
|
||||||
|
slippageBps?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url: string, init?: RequestInit): Promise<any> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { ...init, signal: controller.signal });
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text().catch(() => '');
|
||||||
|
throw new Error(`Upstream ${res.status}: ${body.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
} finally {
|
||||||
|
clearTimeout(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TRX swap через SunSwap. Для упрощения — пока TRX↔USDT only (как в существующем proxy route).
|
||||||
|
* Расширить через token-registry если потребуется ETH/USDC support.
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
throw new Error(`TRX swap supports only TRX↔USDT pairs (got ${p.from} → ${p.to})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.TRX);
|
||||||
|
const fromTronAddr = ethAddressToTron(wallet.address);
|
||||||
|
if (fromTronAddr !== p.expectedFromAddress) {
|
||||||
|
throw new Error(`TRX address mismatch: derived ${fromTronAddr} ≠ DB ${p.expectedFromAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SOL Jupiter ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SOL_RPC = 'https://api.mainnet-beta.solana.com';
|
||||||
|
const JUPITER_API = 'https://quote-api.jup.ag/v6';
|
||||||
|
|
||||||
|
let _solConnection: Connection | null = null;
|
||||||
|
function getSolConnection(): Connection {
|
||||||
|
if (!_solConnection) {
|
||||||
|
_solConnection = new Connection(SOL_RPC, 'confirmed');
|
||||||
|
}
|
||||||
|
return _solConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwapSolParams {
|
||||||
|
mnemonic: string;
|
||||||
|
expectedFromAddress: string;
|
||||||
|
inputMint: string;
|
||||||
|
outputMint: string;
|
||||||
|
amount: string;
|
||||||
|
slippageBps?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SOL Jupiter chained swap. Получаем quote от Jupiter, build serialized tx, sign keypair'ом, broadcast.
|
||||||
|
*/
|
||||||
|
export async function swapSol(p: SwapSolParams): Promise<{ signature: string }> {
|
||||||
|
const seed = await bip39.mnemonicToSeed(p.mnemonic);
|
||||||
|
const { key } = derivePath(DERIVATION_PATHS.SOL, seed.toString('hex'));
|
||||||
|
if (!key || key.length !== 32) {
|
||||||
|
throw new Error('SOL derivation produced invalid seed length');
|
||||||
|
}
|
||||||
|
const keypair = Keypair.fromSeed(key);
|
||||||
|
if (keypair.publicKey.toBase58() !== p.expectedFromAddress) {
|
||||||
|
throw new Error(`SOL address mismatch: derived ${keypair.publicKey.toBase58()} ≠ DB ${p.expectedFromAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slippageBps = p.slippageBps ?? 50;
|
||||||
|
if (slippageBps < 1 || slippageBps > 1000) {
|
||||||
|
throw new Error('slippageBps must be 1-1000');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Jupiter quote
|
||||||
|
const quoteUrl = `${JUPITER_API}/quote?inputMint=${encodeURIComponent(p.inputMint)}&outputMint=${encodeURIComponent(p.outputMint)}&amount=${encodeURIComponent(p.amount)}&slippageBps=${slippageBps}`;
|
||||||
|
const headers: Record<string, string> = { Accept: 'application/json' };
|
||||||
|
if (env.jupiterApiKey) headers['x-api-key'] = env.jupiterApiKey;
|
||||||
|
const quoteRes = await fetchJson(quoteUrl, { headers });
|
||||||
|
|
||||||
|
// 2. Jupiter swap (build serialized tx)
|
||||||
|
const swapBody: Record<string, unknown> = {
|
||||||
|
quoteResponse: quoteRes,
|
||||||
|
userPublicKey: keypair.publicKey.toBase58(),
|
||||||
|
wrapAndUnwrapSol: true,
|
||||||
|
dynamicComputeUnitLimit: true,
|
||||||
|
prioritizationFeeLamports: 'auto',
|
||||||
|
};
|
||||||
|
if (env.jupiterReferralAccount) swapBody.feeAccount = env.jupiterReferralAccount;
|
||||||
|
|
||||||
|
const swapRes = await fetchJson(`${JUPITER_API}/swap`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(swapBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
const txBase64 = swapRes.swapTransaction;
|
||||||
|
if (!txBase64 || typeof txBase64 !== 'string') {
|
||||||
|
throw new Error('Jupiter swap returned no swapTransaction');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Deserialize → sign → broadcast
|
||||||
|
const txBytes = Buffer.from(txBase64, 'base64');
|
||||||
|
const tx = VersionedTransaction.deserialize(txBytes);
|
||||||
|
|
||||||
|
// Verify fee-payer === our pubkey
|
||||||
|
const feePayer = tx.message.staticAccountKeys[0]?.toBase58();
|
||||||
|
if (feePayer !== keypair.publicKey.toBase58()) {
|
||||||
|
throw new Error(`Jupiter built tx with wrong feePayer ${feePayer} (expected ${keypair.publicKey.toBase58()})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.sign([keypair]);
|
||||||
|
|
||||||
|
const conn = getSolConnection();
|
||||||
|
const sig = await conn.sendRawTransaction(tx.serialize());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const latestBlock = await conn.getLatestBlockhash();
|
||||||
|
await conn.confirmTransaction({
|
||||||
|
signature: sig,
|
||||||
|
blockhash: latestBlock.blockhash,
|
||||||
|
lastValidBlockHeight: latestBlock.lastValidBlockHeight,
|
||||||
|
}, 'confirmed');
|
||||||
|
} catch (err: any) {
|
||||||
|
const name = err?.name || '';
|
||||||
|
if (name === 'TransactionExpiredBlockheightExceededError') {
|
||||||
|
throw new Error(`SOL Jupiter swap EXPIRED. sig=${sig}`);
|
||||||
|
}
|
||||||
|
logger.warn(`SOL Jupiter swap confirm warning (${name}): ${err.message}. sig=${sig}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { signature: sig };
|
||||||
|
}
|
||||||
@@ -14,11 +14,18 @@ import * as bip39 from 'bip39';
|
|||||||
import { BIP32Factory } from 'bip32';
|
import { BIP32Factory } from 'bip32';
|
||||||
import * as ecc from 'tiny-secp256k1';
|
import * as ecc from 'tiny-secp256k1';
|
||||||
import * as bitcoin from 'bitcoinjs-lib';
|
import * as bitcoin from 'bitcoinjs-lib';
|
||||||
import { Keypair, Connection, PublicKey, SystemProgram, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
|
import { Keypair, Connection, PublicKey, SystemProgram, Transaction, ComputeBudgetProgram, VersionedTransaction } from '@solana/web3.js';
|
||||||
|
import {
|
||||||
|
getAssociatedTokenAddressSync,
|
||||||
|
createAssociatedTokenAccountIdempotentInstruction,
|
||||||
|
createTransferCheckedInstruction,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
} from '@solana/spl-token';
|
||||||
import { derivePath } from 'ed25519-hd-key';
|
import { derivePath } from 'ed25519-hd-key';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
||||||
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
||||||
|
import { getTokenInfo } from '../lib/token-registry';
|
||||||
import type { ChainCode } from '../lib/address-validators';
|
import type { ChainCode } from '../lib/address-validators';
|
||||||
|
|
||||||
const bip32 = BIP32Factory(ecc);
|
const bip32 = BIP32Factory(ecc);
|
||||||
@@ -109,8 +116,8 @@ export interface RawEvmSignParams {
|
|||||||
|
|
||||||
export async function signAndBroadcast(p: SendParams): Promise<{ txid: string }> {
|
export async function signAndBroadcast(p: SendParams): Promise<{ txid: string }> {
|
||||||
switch (p.chain) {
|
switch (p.chain) {
|
||||||
case 'ETH': return sendEvm(p, ETH_RPC, 1, USDT_ERC20);
|
case 'ETH': return sendEvm(p, ETH_RPC, 1);
|
||||||
case 'BSC': return sendEvm(p, BSC_RPC, 56, USDT_BEP20);
|
case 'BSC': return sendEvm(p, BSC_RPC, 56);
|
||||||
case 'BTC': return sendBtc(p);
|
case 'BTC': return sendBtc(p);
|
||||||
case 'TRX': return sendTrx(p);
|
case 'TRX': return sendTrx(p);
|
||||||
case 'SOL': return sendSol(p);
|
case 'SOL': return sendSol(p);
|
||||||
@@ -210,7 +217,7 @@ function assertAddressMatch(derived: string, expected: string, chain: ChainCode)
|
|||||||
|
|
||||||
// ─── EVM (ETH / BSC) ───
|
// ─── EVM (ETH / BSC) ───
|
||||||
|
|
||||||
async function sendEvm(p: SendParams, rpc: string, chainId: number, usdtAddr: string): Promise<{ txid: string }> {
|
async function sendEvm(p: SendParams, rpc: string, chainId: number): Promise<{ txid: string }> {
|
||||||
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.ETH);
|
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.ETH);
|
||||||
assertAddressMatch(wallet.address, p.expectedFromAddress, p.chain);
|
assertAddressMatch(wallet.address, p.expectedFromAddress, p.chain);
|
||||||
// H29 — RPC failover (выбираем working RPC из списка для chain)
|
// H29 — RPC failover (выбираем working RPC из списка для chain)
|
||||||
@@ -264,25 +271,29 @@ async function sendEvm(p: SendParams, rpc: string, chainId: number, usdtAddr: st
|
|||||||
throw new Error('Insufficient balance (value + gas)');
|
throw new Error('Insufficient balance (value + gas)');
|
||||||
}
|
}
|
||||||
tx = { to: p.to, value, chainId, nonce, gasLimit: estGas, ...feeFields };
|
tx = { to: p.to, value, chainId, nonce, gasLimit: estGas, ...feeFields };
|
||||||
} else if (p.token.toUpperCase() === 'USDT') {
|
} else {
|
||||||
|
// Generic ERC20/BEP20: lookup в token-registry. Поддерживаются все токены из registry.
|
||||||
|
const tokenInfo = getTokenInfo(evmChain, p.token);
|
||||||
|
if (!tokenInfo) {
|
||||||
|
throw new Error(`Token ${p.token} not in registry for chain ${evmChain}`);
|
||||||
|
}
|
||||||
const iface = new ethers.utils.Interface([
|
const iface = new ethers.utils.Interface([
|
||||||
...ERC20_ABI,
|
...ERC20_ABI,
|
||||||
'function balanceOf(address) view returns (uint256)',
|
'function balanceOf(address) view returns (uint256)',
|
||||||
]);
|
]);
|
||||||
const erc20 = new ethers.Contract(usdtAddr, iface, provider);
|
const erc20 = new ethers.Contract(tokenInfo.address, iface, provider);
|
||||||
const tokenBal: ethers.BigNumber = await erc20.balanceOf(wallet.address);
|
const tokenBal: ethers.BigNumber = await erc20.balanceOf(wallet.address);
|
||||||
if (tokenBal.lt(ethers.BigNumber.from(p.amount))) {
|
if (tokenBal.lt(ethers.BigNumber.from(p.amount))) {
|
||||||
throw new Error('Insufficient token balance');
|
throw new Error('Insufficient token balance');
|
||||||
}
|
}
|
||||||
const nativeBal = await provider.getBalance(wallet.address);
|
const nativeBal = await provider.getBalance(wallet.address);
|
||||||
const data = iface.encodeFunctionData('transfer', [p.to, p.amount]);
|
const data = iface.encodeFunctionData('transfer', [p.to, p.amount]);
|
||||||
// H10 — actual estimateGas + 20% safety. Hardcoded 80000 was too low for cold
|
// H10 — actual estimateGas + 20% safety. Cold storage slots (first transfer to fresh
|
||||||
// storage slots (first transfer to recipient SSTORE costs 81-90k → OOG burn).
|
// recipient) cost 81-90k due to SSTORE; floor 60k, ceiling 200k для sanity.
|
||||||
let estGas: ethers.BigNumber;
|
let estGas: ethers.BigNumber;
|
||||||
try {
|
try {
|
||||||
const estimated = await provider.estimateGas({ from: wallet.address, to: usdtAddr, data, value: 0 });
|
const estimated = await provider.estimateGas({ from: wallet.address, to: tokenInfo.address, data, value: 0 });
|
||||||
estGas = estimated.mul(120).div(100); // +20%
|
estGas = estimated.mul(120).div(100); // +20%
|
||||||
// Floor 60k (minimum realistic), ceiling 200k (sanity)
|
|
||||||
const minGas = ethers.BigNumber.from(60000);
|
const minGas = ethers.BigNumber.from(60000);
|
||||||
const maxGas = ethers.BigNumber.from(200000);
|
const maxGas = ethers.BigNumber.from(200000);
|
||||||
if (estGas.lt(minGas)) estGas = minGas;
|
if (estGas.lt(minGas)) estGas = minGas;
|
||||||
@@ -293,9 +304,7 @@ async function sendEvm(p: SendParams, rpc: string, chainId: number, usdtAddr: st
|
|||||||
if (nativeBal.lt(effectiveGasPrice.mul(estGas))) {
|
if (nativeBal.lt(effectiveGasPrice.mul(estGas))) {
|
||||||
throw new Error('Insufficient native balance for gas');
|
throw new Error('Insufficient native balance for gas');
|
||||||
}
|
}
|
||||||
tx = { to: usdtAddr, data, value: 0, chainId, nonce, gasLimit: estGas, ...feeFields };
|
tx = { to: tokenInfo.address, data, value: 0, chainId, nonce, gasLimit: estGas, ...feeFields };
|
||||||
} else {
|
|
||||||
throw new Error(`Token ${p.token} not supported on chainId ${chainId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// H25 — explicit timeout, иначе slow RPC stalls Express worker indefinitely
|
// H25 — explicit timeout, иначе slow RPC stalls Express worker indefinitely
|
||||||
@@ -306,10 +315,6 @@ async function sendEvm(p: SendParams, rpc: string, chainId: number, usdtAddr: st
|
|||||||
// ─── SOLANA ───
|
// ─── SOLANA ───
|
||||||
|
|
||||||
async function sendSol(p: SendParams): Promise<{ txid: string }> {
|
async function sendSol(p: SendParams): Promise<{ txid: string }> {
|
||||||
if (p.token) {
|
|
||||||
throw new Error('SOL SPL-token signing не реализовано (только native SOL)');
|
|
||||||
}
|
|
||||||
|
|
||||||
const seed = await bip39.mnemonicToSeed(p.mnemonic);
|
const seed = await bip39.mnemonicToSeed(p.mnemonic);
|
||||||
const { key } = derivePath(DERIVATION_PATHS.SOL, seed.toString('hex'));
|
const { key } = derivePath(DERIVATION_PATHS.SOL, seed.toString('hex'));
|
||||||
if (!key || key.length !== 32) {
|
if (!key || key.length !== 32) {
|
||||||
@@ -318,64 +323,82 @@ async function sendSol(p: SendParams): Promise<{ txid: string }> {
|
|||||||
const keypair = Keypair.fromSeed(key);
|
const keypair = Keypair.fromSeed(key);
|
||||||
assertAddressMatch(keypair.publicKey.toBase58(), p.expectedFromAddress, 'SOL');
|
assertAddressMatch(keypair.publicKey.toBase58(), p.expectedFromAddress, 'SOL');
|
||||||
|
|
||||||
// C10 — lamports precision: @solana/web3.js converts BigInt → Number internally
|
// Precision: @solana/web3.js конвертит BigInt → Number внутренне (u64 layout).
|
||||||
// (u64 layout). Above 2^53 lamports = silent truncation. Reject early.
|
const amountBig = BigInt(p.amount);
|
||||||
const lamports = BigInt(p.amount);
|
|
||||||
const MAX_SAFE_LAMPORTS = BigInt(Number.MAX_SAFE_INTEGER);
|
const MAX_SAFE_LAMPORTS = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
if (lamports > MAX_SAFE_LAMPORTS) {
|
if (amountBig > MAX_SAFE_LAMPORTS) {
|
||||||
throw new Error(`SOL amount ${p.amount} lamports exceeds Number precision (max ${MAX_SAFE_LAMPORTS}); split into multiple sends`);
|
throw new Error(`SOL amount ${p.amount} exceeds Number precision (max ${MAX_SAFE_LAMPORTS}); split into multiple sends`);
|
||||||
}
|
}
|
||||||
if (lamports <= 0n) {
|
if (amountBig <= 0n) {
|
||||||
throw new Error('SOL amount must be positive');
|
throw new Error('SOL amount must be positive');
|
||||||
}
|
}
|
||||||
|
|
||||||
// H41 — singleton Connection (per-call new() leaks WebSocket subscriptions)
|
|
||||||
const conn = getSolConnection();
|
const conn = getSolConnection();
|
||||||
const toPk = new PublicKey(p.to);
|
const toPk = new PublicKey(p.to);
|
||||||
|
|
||||||
// C11 — rent-exempt minimum check. Если recipient — fresh account и amount меньше
|
|
||||||
// rent-exempt минимума, tx fails ПОСЛЕ broadcast (5000 lamports fee burned, no transfer).
|
|
||||||
// Pre-check сохраняет fee + user-facing error.
|
|
||||||
try {
|
|
||||||
const accountInfo = await conn.getAccountInfo(toPk);
|
|
||||||
if (accountInfo === null) {
|
|
||||||
const rentMin = BigInt(await conn.getMinimumBalanceForRentExemption(0));
|
|
||||||
if (lamports < rentMin) {
|
|
||||||
throw new Error(`SOL recipient is fresh account; amount ${lamports} lamports < rent-exempt minimum ${rentMin}. Send at least ${rentMin} lamports to create account.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (preErr: any) {
|
|
||||||
// Network error checking — proceed (broadcast will surface real error)
|
|
||||||
if (!preErr.message?.includes('rent-exempt')) {
|
|
||||||
// только network/RPC failures, не наш own throw
|
|
||||||
} else {
|
|
||||||
throw preErr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash();
|
||||||
|
const tx = new Transaction({ feePayer: keypair.publicKey, blockhash, lastValidBlockHeight });
|
||||||
|
|
||||||
const tx = new Transaction({
|
// H40 — compute-unit price (priority fee)
|
||||||
feePayer: keypair.publicKey,
|
|
||||||
blockhash,
|
|
||||||
lastValidBlockHeight,
|
|
||||||
});
|
|
||||||
// H40 — compute-unit price для priority fee (tiers slow/normal/fast).
|
|
||||||
// Без этого tx может dropped в congestion. Default 'normal' = 1_000 microLamports.
|
|
||||||
const tier = p.feeTier ?? 'normal';
|
const tier = p.feeTier ?? 'normal';
|
||||||
const cuPrice = tier === 'fast' ? 10_000n : tier === 'slow' ? 0n : 1_000n;
|
const cuPrice = tier === 'fast' ? 10_000n : tier === 'slow' ? 0n : 1_000n;
|
||||||
if (cuPrice > 0n) {
|
if (cuPrice > 0n) {
|
||||||
tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: cuPrice }));
|
tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: cuPrice }));
|
||||||
}
|
}
|
||||||
tx.add(
|
|
||||||
SystemProgram.transfer({
|
if (!p.token) {
|
||||||
|
// ── Native SOL transfer ──
|
||||||
|
// C11 — rent-exempt check для fresh recipient
|
||||||
|
try {
|
||||||
|
const accountInfo = await conn.getAccountInfo(toPk);
|
||||||
|
if (accountInfo === null) {
|
||||||
|
const rentMin = BigInt(await conn.getMinimumBalanceForRentExemption(0));
|
||||||
|
if (amountBig < rentMin) {
|
||||||
|
throw new Error(`SOL recipient is fresh account; amount ${amountBig} lamports < rent-exempt minimum ${rentMin}. Send at least ${rentMin} lamports to create account.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (preErr: any) {
|
||||||
|
if (preErr.message?.includes('rent-exempt')) throw preErr;
|
||||||
|
// Network error checking — proceed (broadcast surfaces real error)
|
||||||
|
}
|
||||||
|
tx.add(SystemProgram.transfer({
|
||||||
fromPubkey: keypair.publicKey,
|
fromPubkey: keypair.publicKey,
|
||||||
toPubkey: toPk,
|
toPubkey: toPk,
|
||||||
lamports,
|
lamports: amountBig,
|
||||||
}),
|
}));
|
||||||
);
|
} else {
|
||||||
tx.sign(keypair);
|
// ── SPL token transfer ──
|
||||||
|
// Generic SPL: lookup mint в token-registry. Поддерживает USDT/USDC/PUMP/JUP/... (15 mints)
|
||||||
|
const tokenInfo = getTokenInfo('SOL', p.token);
|
||||||
|
if (!tokenInfo) {
|
||||||
|
throw new Error(`Token ${p.token} not in registry for chain SOL`);
|
||||||
|
}
|
||||||
|
const mint = new PublicKey(tokenInfo.address);
|
||||||
|
const sourceAta = getAssociatedTokenAddressSync(mint, keypair.publicKey);
|
||||||
|
const destAta = getAssociatedTokenAddressSync(mint, toPk);
|
||||||
|
|
||||||
|
// Idempotent ATA creation — safe to always include. Если ATA уже есть, instruction skip'нется.
|
||||||
|
// Recipient'у которому никогда не отправляли этот mint — мы создадим ATA (~0.002 SOL rent).
|
||||||
|
tx.add(createAssociatedTokenAccountIdempotentInstruction(
|
||||||
|
keypair.publicKey, // payer (мы платим rent если ATA создаётся)
|
||||||
|
destAta,
|
||||||
|
toPk,
|
||||||
|
mint,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
));
|
||||||
|
|
||||||
|
// CheckedTransfer защищает от decimals mismatch (RPC ложит → token loss)
|
||||||
|
tx.add(createTransferCheckedInstruction(
|
||||||
|
sourceAta,
|
||||||
|
mint,
|
||||||
|
destAta,
|
||||||
|
keypair.publicKey,
|
||||||
|
amountBig,
|
||||||
|
tokenInfo.decimals,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.sign(keypair);
|
||||||
const sig = await conn.sendRawTransaction(tx.serialize());
|
const sig = await conn.sendRawTransaction(tx.serialize());
|
||||||
|
|
||||||
// H37 — distinguished error categories
|
// H37 — distinguished error categories
|
||||||
@@ -404,6 +427,83 @@ function getSolConnection(): Connection {
|
|||||||
return _solConnection;
|
return _solConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── SOL custodial sign-and-broadcast (для Relay bridge SOL-side) ─────
|
||||||
|
|
||||||
|
export interface SignSolanaTxParams {
|
||||||
|
mnemonic: string;
|
||||||
|
expectedFromAddress: string;
|
||||||
|
serializedTransaction: string; // base64-encoded VersionedTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подписать произвольную serialized Solana VersionedTransaction custodially.
|
||||||
|
* Используется когда Relay /execute или Jupiter возвращают unsigned tx — клиент шлёт base64,
|
||||||
|
* сервер deserialize → verify feePayer === user's pubkey → partial-sign → broadcast.
|
||||||
|
*
|
||||||
|
* Security:
|
||||||
|
* - feePayer (staticAccountKeys[0]) ДОЛЖЕН совпадать с user's SOL pubkey
|
||||||
|
* - Tx size limit 8KB (Solana network max — 1232 bytes раз; base64 ~1.65k chars)
|
||||||
|
* - assertAddressMatch — derived address vs DB
|
||||||
|
*/
|
||||||
|
export async function signAndBroadcastSolanaTx(p: SignSolanaTxParams): Promise<{ signature: string }> {
|
||||||
|
const seed = await bip39.mnemonicToSeed(p.mnemonic);
|
||||||
|
const { key } = derivePath(DERIVATION_PATHS.SOL, seed.toString('hex'));
|
||||||
|
if (!key || key.length !== 32) {
|
||||||
|
throw new Error('SOL derivation produced invalid seed length');
|
||||||
|
}
|
||||||
|
const keypair = Keypair.fromSeed(key);
|
||||||
|
assertAddressMatch(keypair.publicKey.toBase58(), p.expectedFromAddress, 'SOL');
|
||||||
|
|
||||||
|
let txBytes: Buffer;
|
||||||
|
try {
|
||||||
|
txBytes = Buffer.from(p.serializedTransaction, 'base64');
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid base64 transaction');
|
||||||
|
}
|
||||||
|
if (txBytes.length === 0 || txBytes.length > 1500) {
|
||||||
|
throw new Error(`Invalid tx size: ${txBytes.length} bytes (expected 1-1500)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx: VersionedTransaction;
|
||||||
|
try {
|
||||||
|
tx = VersionedTransaction.deserialize(txBytes);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(`Failed to deserialize VersionedTransaction: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical: verify feePayer === our pubkey. Без этого attacker может подсунуть tx
|
||||||
|
// с другим feePayer, мы подписали бы fee-deduct из их wallet'а (бесплатно для нас).
|
||||||
|
const feePayer = tx.message.staticAccountKeys[0]?.toBase58();
|
||||||
|
if (feePayer !== keypair.publicKey.toBase58()) {
|
||||||
|
throw new Error(`feePayer mismatch: tx.feePayer=${feePayer} vs user.pubkey=${keypair.publicKey.toBase58()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.sign([keypair]);
|
||||||
|
|
||||||
|
const conn = getSolConnection();
|
||||||
|
const sig = await conn.sendRawTransaction(tx.serialize());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const latestBlock = await conn.getLatestBlockhash();
|
||||||
|
await conn.confirmTransaction({
|
||||||
|
signature: sig,
|
||||||
|
blockhash: latestBlock.blockhash,
|
||||||
|
lastValidBlockHeight: latestBlock.lastValidBlockHeight,
|
||||||
|
}, 'confirmed');
|
||||||
|
} catch (err: any) {
|
||||||
|
const name = err?.name || '';
|
||||||
|
if (name === 'TransactionExpiredBlockheightExceededError') {
|
||||||
|
throw new Error(`SOL tx EXPIRED (blockhash expired before confirm). sig=${sig}`);
|
||||||
|
}
|
||||||
|
if (name === 'TransactionExpiredTimeoutError') {
|
||||||
|
throw new Error(`SOL tx unconfirmed after timeout. sig=${sig}`);
|
||||||
|
}
|
||||||
|
throw new Error(`SOL confirm error (${name}): ${err.message}. sig=${sig}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { signature: sig };
|
||||||
|
}
|
||||||
|
|
||||||
// ─── BITCOIN ───
|
// ─── BITCOIN ───
|
||||||
|
|
||||||
async function sendBtc(p: SendParams): Promise<{ txid: string }> {
|
async function sendBtc(p: SendParams): Promise<{ txid: string }> {
|
||||||
@@ -577,7 +677,12 @@ async function sendTrx(p: SendParams): Promise<{ txid: string }> {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
txBody = built;
|
txBody = built;
|
||||||
} else if (p.token.toUpperCase() === 'USDT') {
|
} else {
|
||||||
|
// Generic TRC20: lookup в token-registry. Поддерживает USDT, USDC и др.
|
||||||
|
const tokenInfo = getTokenInfo('TRX', p.token);
|
||||||
|
if (!tokenInfo) {
|
||||||
|
throw new Error(`Token ${p.token} not in registry for chain TRX`);
|
||||||
|
}
|
||||||
const param =
|
const param =
|
||||||
tronAddressToHex(p.to).padStart(64, '0') +
|
tronAddressToHex(p.to).padStart(64, '0') +
|
||||||
BigInt(p.amount).toString(16).padStart(64, '0');
|
BigInt(p.amount).toString(16).padStart(64, '0');
|
||||||
@@ -586,19 +691,16 @@ async function sendTrx(p: SendParams): Promise<{ txid: string }> {
|
|||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
owner_address: fromTronAddr,
|
owner_address: fromTronAddr,
|
||||||
contract_address: USDT_TRC20,
|
contract_address: tokenInfo.address,
|
||||||
function_selector: 'transfer(address,uint256)',
|
function_selector: 'transfer(address,uint256)',
|
||||||
parameter: param,
|
parameter: param,
|
||||||
// 30 TRX cap — реальный USDT transfer обычно жжёт 15-30 TRX без Energy,
|
// 30 TRX cap — типичный TRC20 transfer жжёт 15-30 TRX без Energy.
|
||||||
// ~0 с Energy. Раньше было 100 TRX — это cap, не actual fee, но завышен.
|
|
||||||
fee_limit: 30_000_000,
|
fee_limit: 30_000_000,
|
||||||
call_value: 0,
|
call_value: 0,
|
||||||
visible: true,
|
visible: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
txBody = built.transaction;
|
txBody = built.transaction;
|
||||||
} else {
|
|
||||||
throw new Error(`Token ${p.token} not supported on TRX`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!txBody?.txID || !txBody?.raw_data_hex || !txBody?.raw_data) {
|
if (!txBody?.txID || !txBody?.raw_data_hex || !txBody?.raw_data) {
|
||||||
@@ -659,8 +761,14 @@ async function sendTrx(p: SendParams): Promise<{ txid: string }> {
|
|||||||
throw new Error(`TRX amount mismatch: expected ${p.amount}, got ${contractValue.amount}`);
|
throw new Error(`TRX amount mismatch: expected ${p.amount}, got ${contractValue.amount}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (contractValue.contract_address !== USDT_TRC20) {
|
// MITM-check: contract_address должен совпадать с тем что lookup'ом из registry для нашего token symbol.
|
||||||
throw new Error(`TRX contract mismatch: expected ${USDT_TRC20}, got ${contractValue.contract_address}`);
|
// Без этого RPC может вернуть legitimate-looking tx но с другим contract → attacker drain.
|
||||||
|
const expectedTokenInfo = getTokenInfo('TRX', p.token);
|
||||||
|
if (!expectedTokenInfo) {
|
||||||
|
throw new Error(`Token ${p.token} not in registry for chain TRX (MITM-check)`);
|
||||||
|
}
|
||||||
|
if (contractValue.contract_address !== expectedTokenInfo.address) {
|
||||||
|
throw new Error(`TRX contract mismatch: expected ${expectedTokenInfo.address}, got ${contractValue.contract_address}`);
|
||||||
}
|
}
|
||||||
const data = String(contractValue.data || '');
|
const data = String(contractValue.data || '');
|
||||||
if (data.length !== 128 + 8) {
|
if (data.length !== 128 + 8) {
|
||||||
|
|||||||
@@ -318,8 +318,8 @@
|
|||||||
},
|
},
|
||||||
"/wallets/{chain}/sign-raw-evm-tx": {
|
"/wallets/{chain}/sign-raw-evm-tx": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Custodial sign + broadcast arbitrary EVM tx (Relay/Swap unsigned tx)",
|
"summary": "Custodial sign + broadcast arbitrary EVM tx (Relay bridge)",
|
||||||
"description": "Подписывает произвольную EVM tx (например `steps[0].items[0].data` из `/relay/execute/swap`). Сервер расшифровывает mnemonic, деривит privkey, ставит nonce, подписывает type-2 EIP-1559 tx, broadcast'ит. Если задан `feeTier` → переопределяет maxFeePerGas/maxPriority из тела актуальным из eth_feeHistory. ⚠️ Security: подписывает arbitrary `to`+`data` — в production надо whitelist'ить `to` (Relay routers) или требовать Relay attestation. Только ETH(1)/BSC(56).",
|
"description": "Подписывает unsigned EVM tx из Relay /execute response. Policy: `to` ДОЛЖЕН быть в Relay router allowlist; selector blacklist (approve/permit/setApprovalForAll). Для DEX swap'ов используй `/wallets/{chain}/swap` — там chained custodial без этих ограничений.",
|
||||||
"tags": ["Wallet Ops"],
|
"tags": ["Wallet Ops"],
|
||||||
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "type": "string", "enum": ["ETH", "BSC"] } }],
|
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "type": "string", "enum": ["ETH", "BSC"] } }],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -328,13 +328,115 @@
|
|||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": { "description": "Broadcast successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TxBroadcastResponse" } } } },
|
"200": { "description": "Broadcast successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TxBroadcastResponse" } } } },
|
||||||
"400": { "description": "Invalid input (bad to/data/value, chainId mismatch, invalid feeTier)" },
|
"400": { "description": "Policy violation: to not in allowlist OR forbidden selector OR cap exceeded" },
|
||||||
"404": { "description": "Wallet/mnemonic not found" },
|
"404": { "description": "Wallet/mnemonic not found" },
|
||||||
"502": { "description": "Broadcast failed" },
|
"502": { "description": "Broadcast failed" },
|
||||||
"503": { "description": "Crypto service not ready" }
|
"503": { "description": "Crypto service not ready" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/wallets/{chain}/swap": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Custodial chained swap (BSC PancakeSwap / TRX SunSwap / SOL Jupiter)",
|
||||||
|
"description": "Полностью custodial swap в один HTTP-вызов. Никакого client-side signing. BSC: approve+swap chained (PancakeSwap V2, поддерживает BNB/USDT/USDC/DOGE/WBNB/BUSD). TRX: SunSwap TRX↔USDT. SOL: Jupiter aggregator (любые mints из registry). Slippage protection — server computes amountOutMin от actual quote с default 50 bps tolerance. Optional Idempotency-Key header для anti double-spend.",
|
||||||
|
"tags": ["Wallet Ops"],
|
||||||
|
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "type": "string", "enum": ["BSC", "TRX", "SOL"] } }],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"title": "BSC/TRX swap (symbols)",
|
||||||
|
"required": ["from", "to", "amount"],
|
||||||
|
"properties": {
|
||||||
|
"from": { "type": "string", "description": "BSC: BNB|USDT|USDC|DOGE|WBNB|BUSD; TRX: TRX|USDT" },
|
||||||
|
"to": { "type": "string" },
|
||||||
|
"amount": { "type": "string", "description": "Smallest units (wei для 18-dec, sun для TRX 6-dec)" },
|
||||||
|
"slippageBps": { "type": "integer", "minimum": 1, "maximum": 1000, "description": "0.01%-10%. Default 50 (0.5%)." },
|
||||||
|
"feeTier": { "type": "string", "enum": ["slow", "normal", "fast"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"title": "SOL swap (mints)",
|
||||||
|
"required": ["inputMint", "outputMint", "amount"],
|
||||||
|
"properties": {
|
||||||
|
"inputMint": { "type": "string", "description": "SPL mint address (base58)" },
|
||||||
|
"outputMint": { "type": "string" },
|
||||||
|
"amount": { "type": "string", "description": "Smallest units (lamports = 9-dec для SOL native)" },
|
||||||
|
"slippageBps": { "type": "integer", "minimum": 1, "maximum": 1000 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "BSC: { approveTxid?, swapTxid }. TRX/SOL: { txid | signature }",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": { "type": "boolean" },
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"chain": { "type": "string" },
|
||||||
|
"approveTxid": { "type": "string", "nullable": true, "description": "BSC only, если token-to-X swap требовал approve" },
|
||||||
|
"swapTxid": { "type": "string", "description": "BSC swap txid" },
|
||||||
|
"txid": { "type": "string", "description": "TRX txid" },
|
||||||
|
"signature": { "type": "string", "description": "SOL tx signature" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": { "description": "Invalid pair / slippage / amount / unsupported chain" },
|
||||||
|
"404": { "description": "Wallet not found" },
|
||||||
|
"409": { "description": "Idempotency-Key reuse with different body, or operation in-flight" },
|
||||||
|
"502": { "description": "Swap failed (no liquidity / network error / contract revert)" },
|
||||||
|
"503": { "description": "Crypto service not ready" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/wallets/SOL/sign-and-broadcast-tx": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Custodial sign + broadcast arbitrary Solana VersionedTransaction",
|
||||||
|
"description": "Подписывает unsigned serialized Solana tx (от Relay /execute SOL-side, или любого aggregator'а). Server verify feePayer === user's pubkey, partial-sign keypair'ом, broadcast, confirm.",
|
||||||
|
"tags": ["Wallet Ops"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["transaction"],
|
||||||
|
"properties": {
|
||||||
|
"transaction": { "type": "string", "description": "Base64-encoded VersionedTransaction (max ~1500 bytes raw)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Signed and broadcast",
|
||||||
|
"content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "data": { "type": "object", "properties": { "signature": { "type": "string" }, "chain": { "type": "string" } } } } } } }
|
||||||
|
},
|
||||||
|
"400": { "description": "Invalid base64 / tx size / feePayer mismatch" },
|
||||||
|
"404": { "description": "SOL wallet/mnemonic not found" },
|
||||||
|
"502": { "description": "Sign or broadcast failed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"/btc/utxos/{address}": {
|
"/btc/utxos/{address}": {
|
||||||
"get": {
|
"get": {
|
||||||
|
|||||||
195
pnpm-lock.yaml
generated
195
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
|
|
||||||
apps/api:
|
apps/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@solana/spl-token':
|
||||||
|
specifier: ^0.4.14
|
||||||
|
version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
'@solana/web3.js':
|
'@solana/web3.js':
|
||||||
specifier: ^1.98.4
|
specifier: ^1.98.4
|
||||||
version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
@@ -360,22 +363,58 @@ packages:
|
|||||||
'@scure/base@1.2.6':
|
'@scure/base@1.2.6':
|
||||||
resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==}
|
resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==}
|
||||||
|
|
||||||
|
'@solana/buffer-layout-utils@0.2.0':
|
||||||
|
resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@solana/buffer-layout@4.0.1':
|
'@solana/buffer-layout@4.0.1':
|
||||||
resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
|
resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
|
||||||
engines: {node: '>=5.10'}
|
engines: {node: '>=5.10'}
|
||||||
|
|
||||||
|
'@solana/codecs-core@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
'@solana/codecs-core@2.3.0':
|
'@solana/codecs-core@2.3.0':
|
||||||
resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==}
|
resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==}
|
||||||
engines: {node: '>=20.18.0'}
|
engines: {node: '>=20.18.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.3.3'
|
typescript: '>=5.3.3'
|
||||||
|
|
||||||
|
'@solana/codecs-data-structures@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
|
'@solana/codecs-numbers@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
'@solana/codecs-numbers@2.3.0':
|
'@solana/codecs-numbers@2.3.0':
|
||||||
resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==}
|
resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==}
|
||||||
engines: {node: '>=20.18.0'}
|
engines: {node: '>=20.18.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.3.3'
|
typescript: '>=5.3.3'
|
||||||
|
|
||||||
|
'@solana/codecs-strings@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==}
|
||||||
|
peerDependencies:
|
||||||
|
fastestsmallesttextencoderdecoder: ^1.0.22
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
|
'@solana/codecs@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
|
'@solana/errors@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
'@solana/errors@2.3.0':
|
'@solana/errors@2.3.0':
|
||||||
resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==}
|
resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==}
|
||||||
engines: {node: '>=20.18.0'}
|
engines: {node: '>=20.18.0'}
|
||||||
@@ -383,6 +422,29 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5.3.3'
|
typescript: '>=5.3.3'
|
||||||
|
|
||||||
|
'@solana/options@2.0.0-rc.1':
|
||||||
|
resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
|
||||||
|
'@solana/spl-token-group@0.0.7':
|
||||||
|
resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
'@solana/web3.js': ^1.95.3
|
||||||
|
|
||||||
|
'@solana/spl-token-metadata@0.1.6':
|
||||||
|
resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
'@solana/web3.js': ^1.95.3
|
||||||
|
|
||||||
|
'@solana/spl-token@0.4.14':
|
||||||
|
resolution: {integrity: sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
'@solana/web3.js': ^1.95.5
|
||||||
|
|
||||||
'@solana/web3.js@1.98.4':
|
'@solana/web3.js@1.98.4':
|
||||||
resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==}
|
resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==}
|
||||||
|
|
||||||
@@ -599,10 +661,20 @@ packages:
|
|||||||
bech32@2.0.0:
|
bech32@2.0.0:
|
||||||
resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==}
|
resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==}
|
||||||
|
|
||||||
|
bigint-buffer@1.1.5:
|
||||||
|
resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
|
bignumber.js@9.3.1:
|
||||||
|
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
bip174@2.1.1:
|
bip174@2.1.1:
|
||||||
resolution: {integrity: sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==}
|
resolution: {integrity: sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -723,6 +795,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
commander@12.1.0:
|
||||||
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
commander@14.0.3:
|
commander@14.0.3:
|
||||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -961,6 +1037,9 @@ packages:
|
|||||||
fast-stable-stringify@1.0.0:
|
fast-stable-stringify@1.0.0:
|
||||||
resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
|
resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==}
|
||||||
|
|
||||||
|
fastestsmallesttextencoderdecoder@1.0.22:
|
||||||
|
resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==}
|
||||||
|
|
||||||
fastq@1.20.1:
|
fastq@1.20.1:
|
||||||
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
|
||||||
|
|
||||||
@@ -968,6 +1047,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2460,27 +2542,124 @@ snapshots:
|
|||||||
|
|
||||||
'@scure/base@1.2.6': {}
|
'@scure/base@1.2.6': {}
|
||||||
|
|
||||||
|
'@solana/buffer-layout-utils@0.2.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/buffer-layout': 4.0.1
|
||||||
|
'@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
|
bigint-buffer: 1.1.5
|
||||||
|
bignumber.js: 9.3.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- encoding
|
||||||
|
- typescript
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@solana/buffer-layout@4.0.1':
|
'@solana/buffer-layout@4.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
|
|
||||||
|
'@solana/codecs-core@2.0.0-rc.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@solana/codecs-core@2.3.0(typescript@5.9.3)':
|
'@solana/codecs-core@2.3.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@solana/errors': 2.3.0(typescript@5.9.3)
|
'@solana/errors': 2.3.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@solana/codecs-numbers@2.3.0(typescript@5.9.3)':
|
'@solana/codecs-numbers@2.3.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@solana/codecs-core': 2.3.0(typescript@5.9.3)
|
'@solana/codecs-core': 2.3.0(typescript@5.9.3)
|
||||||
'@solana/errors': 2.3.0(typescript@5.9.3)
|
'@solana/errors': 2.3.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
fastestsmallesttextencoderdecoder: 1.0.22
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
'@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- fastestsmallesttextencoderdecoder
|
||||||
|
|
||||||
|
'@solana/errors@2.0.0-rc.1(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
chalk: 5.6.2
|
||||||
|
commander: 12.1.0
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@solana/errors@2.3.0(typescript@5.9.3)':
|
'@solana/errors@2.3.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
commander: 14.0.3
|
commander: 14.0.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
|
'@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
'@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
'@solana/errors': 2.0.0-rc.1(typescript@5.9.3)
|
||||||
|
typescript: 5.9.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- fastestsmallesttextencoderdecoder
|
||||||
|
|
||||||
|
'@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
'@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- fastestsmallesttextencoderdecoder
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
'@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
'@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- fastestsmallesttextencoderdecoder
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
'@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)':
|
||||||
|
dependencies:
|
||||||
|
'@solana/buffer-layout': 4.0.1
|
||||||
|
'@solana/buffer-layout-utils': 0.2.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
|
'@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
'@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)
|
||||||
|
'@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
||||||
|
buffer: 6.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- encoding
|
||||||
|
- fastestsmallesttextencoderdecoder
|
||||||
|
- typescript
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)':
|
'@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.2
|
'@babel/runtime': 7.29.2
|
||||||
@@ -2736,8 +2915,18 @@ snapshots:
|
|||||||
|
|
||||||
bech32@2.0.0: {}
|
bech32@2.0.0: {}
|
||||||
|
|
||||||
|
bigint-buffer@1.1.5:
|
||||||
|
dependencies:
|
||||||
|
bindings: 1.5.0
|
||||||
|
|
||||||
|
bignumber.js@9.3.1: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
dependencies:
|
||||||
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
bip174@2.1.1: {}
|
bip174@2.1.1: {}
|
||||||
|
|
||||||
bip32@4.0.0:
|
bip32@4.0.0:
|
||||||
@@ -2895,6 +3084,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@10.0.1: {}
|
commander@10.0.1: {}
|
||||||
|
|
||||||
|
commander@12.1.0: {}
|
||||||
|
|
||||||
commander@14.0.3: {}
|
commander@14.0.3: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
@@ -3214,6 +3405,8 @@ snapshots:
|
|||||||
|
|
||||||
fast-stable-stringify@1.0.0: {}
|
fast-stable-stringify@1.0.0: {}
|
||||||
|
|
||||||
|
fastestsmallesttextencoderdecoder@1.0.22: {}
|
||||||
|
|
||||||
fastq@1.20.1:
|
fastq@1.20.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.1.0
|
reusify: 1.1.0
|
||||||
@@ -3222,6 +3415,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 3.2.0
|
flat-cache: 3.2.0
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0: {}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user