swagger2
This commit is contained in:
@@ -24,9 +24,13 @@ const BSC_RPC = 'https://bsc-dataseed.binance.org';
|
||||
|
||||
const RPC: Record<'ETH' | 'BSC', string> = { ETH: ETH_RPC, BSC: BSC_RPC };
|
||||
|
||||
// Realistic mainnet floors (gwei).
|
||||
// Realistic mainnet floors (gwei). 0 = without floor (use raw eth_feeHistory value).
|
||||
// ETH: убран floor — eth_feeHistory сам по себе репрезентативный, искусственный floor
|
||||
// перерасходовал gas в spam/low-traffic блоках.
|
||||
// BSC: оставлен низкий floor — chain не полностью EIP-1559, без минимума получается
|
||||
// 0.001 gwei который reject'ится min-relay.
|
||||
const FLOOR_GWEI: Record<'ETH' | 'BSC', string> = {
|
||||
ETH: '0.5',
|
||||
ETH: '0',
|
||||
BSC: '0.05',
|
||||
};
|
||||
|
||||
|
||||
@@ -746,7 +746,9 @@ export async function swapTrx(
|
||||
// ─── SOL Jupiter ─────────────────────────────────────────────────────
|
||||
|
||||
const SOL_RPC = 'https://api.mainnet-beta.solana.com';
|
||||
const JUPITER_API = 'https://quote-api.jup.ag/v6';
|
||||
// Jupiter migrated в 2025: старый `quote-api.jup.ag/v6` deprecated и DNS удалён.
|
||||
// `lite-api.jup.ag/swap/v1` — public anonymous endpoint (~600 req/min), JSON-schema идентична.
|
||||
const JUPITER_API = 'https://lite-api.jup.ag/swap/v1';
|
||||
|
||||
let _solConnection: Connection | null = null;
|
||||
function getSolConnection(): Connection {
|
||||
|
||||
@@ -14,7 +14,10 @@ import * as bip39 from 'bip39';
|
||||
import { BIP32Factory } from 'bip32';
|
||||
import * as ecc from 'tiny-secp256k1';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import { Keypair, Connection, PublicKey, SystemProgram, Transaction, ComputeBudgetProgram, VersionedTransaction } from '@solana/web3.js';
|
||||
import {
|
||||
Keypair, Connection, PublicKey, SystemProgram, Transaction, ComputeBudgetProgram,
|
||||
VersionedTransaction, TransactionMessage, AddressLookupTableAccount, TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import {
|
||||
getAssociatedTokenAddressSync,
|
||||
createAssociatedTokenAccountIdempotentInstruction,
|
||||
@@ -504,6 +507,117 @@ export async function signAndBroadcastSolanaTx(p: SignSolanaTxParams): Promise<{
|
||||
return { signature: sig };
|
||||
}
|
||||
|
||||
/** Relay-style SOL step body: instructions[] + addressLookupTableAddresses[].
|
||||
*
|
||||
* Когда Relay execute returns SOL bridge tx, format не serialized — а instructions array
|
||||
* с program IDs, keys, и calldata. Серверу нужно:
|
||||
* 1. Validate каждый isSigner=true key === user's SOL pubkey (мы можем подписать только за себя)
|
||||
* 2. Resolve address lookup tables через SOL RPC (Relay не передаёт сами таблицы, только адреса)
|
||||
* 3. Fetch latest blockhash
|
||||
* 4. Build VersionedTransaction с feePayer=user
|
||||
* 5. Sign + broadcast
|
||||
*/
|
||||
export interface SignSolanaInstructionsParams {
|
||||
mnemonic: string;
|
||||
expectedFromAddress: string;
|
||||
instructions: Array<{
|
||||
programId: string;
|
||||
keys: Array<{ pubkey: string; isSigner: boolean; isWritable: boolean }>;
|
||||
data: string; // hex (no 0x prefix) или base64 — autodetect
|
||||
}>;
|
||||
addressLookupTableAddresses?: string[];
|
||||
}
|
||||
|
||||
export async function signAndBroadcastSolanaInstructions(
|
||||
p: SignSolanaInstructionsParams,
|
||||
): 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');
|
||||
const userPubkey = keypair.publicKey;
|
||||
|
||||
if (!Array.isArray(p.instructions) || p.instructions.length === 0) {
|
||||
throw new Error('SOL instructions: empty array');
|
||||
}
|
||||
if (p.instructions.length > 32) {
|
||||
throw new Error('SOL instructions: too many (max 32)');
|
||||
}
|
||||
|
||||
// ─── Build TransactionInstruction[] из Relay-style objects ───
|
||||
const ixs: TransactionInstruction[] = [];
|
||||
for (const raw of p.instructions) {
|
||||
if (!raw || typeof raw !== 'object') throw new Error('SOL instruction: not an object');
|
||||
let programId: PublicKey;
|
||||
try { programId = new PublicKey(raw.programId); } catch { throw new Error(`SOL invalid programId: ${raw.programId}`); }
|
||||
|
||||
if (!Array.isArray(raw.keys)) throw new Error('SOL instruction.keys must be array');
|
||||
const keys = raw.keys.map((k, idx) => {
|
||||
let pubkey: PublicKey;
|
||||
try { pubkey = new PublicKey(k.pubkey); } catch { throw new Error(`SOL invalid pubkey at keys[${idx}]: ${k.pubkey}`); }
|
||||
// SECURITY: any signer-key must be userPubkey (мы можем подписать только за себя)
|
||||
if (k.isSigner && !pubkey.equals(userPubkey)) {
|
||||
throw new Error(`SOL instruction has signer key ${k.pubkey} ≠ user ${userPubkey.toBase58()}`);
|
||||
}
|
||||
return { pubkey, isSigner: Boolean(k.isSigner), isWritable: Boolean(k.isWritable) };
|
||||
});
|
||||
|
||||
// data: hex или base64? Relay обычно отдаёт hex без префикса.
|
||||
let data: Buffer;
|
||||
const dStr = String(raw.data || '');
|
||||
if (/^[0-9a-fA-F]*$/.test(dStr) && dStr.length % 2 === 0) {
|
||||
data = Buffer.from(dStr, 'hex');
|
||||
} else {
|
||||
try { data = Buffer.from(dStr, 'base64'); } catch { throw new Error('SOL instruction.data: not hex or base64'); }
|
||||
}
|
||||
ixs.push(new TransactionInstruction({ programId, keys, data }));
|
||||
}
|
||||
|
||||
const conn = getSolConnection();
|
||||
|
||||
// ─── Resolve address lookup tables через RPC ───
|
||||
const luts: AddressLookupTableAccount[] = [];
|
||||
for (const lutAddr of (p.addressLookupTableAddresses ?? [])) {
|
||||
let lutPk: PublicKey;
|
||||
try { lutPk = new PublicKey(lutAddr); } catch { throw new Error(`SOL invalid LUT address: ${lutAddr}`); }
|
||||
const acc = await conn.getAddressLookupTable(lutPk);
|
||||
if (!acc.value) throw new Error(`SOL LUT not found on-chain: ${lutAddr}`);
|
||||
luts.push(acc.value);
|
||||
}
|
||||
|
||||
// ─── Compile VersionedTransaction ───
|
||||
const latestBlock = await conn.getLatestBlockhash();
|
||||
const msg = new TransactionMessage({
|
||||
payerKey: userPubkey,
|
||||
recentBlockhash: latestBlock.blockhash,
|
||||
instructions: ixs,
|
||||
}).compileToV0Message(luts);
|
||||
|
||||
const tx = new VersionedTransaction(msg);
|
||||
tx.sign([keypair]);
|
||||
|
||||
// ─── Broadcast + confirm ───
|
||||
const sig = await conn.sendRawTransaction(tx.serialize());
|
||||
try {
|
||||
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 Relay-bridge tx EXPIRED. sig=${sig}`);
|
||||
}
|
||||
throw new Error(`SOL Relay-bridge confirm error (${name}): ${err.message}. sig=${sig}`);
|
||||
}
|
||||
|
||||
return { signature: sig };
|
||||
}
|
||||
|
||||
// ─── BITCOIN ───
|
||||
|
||||
async function sendBtc(p: SendParams): Promise<{ txid: string }> {
|
||||
|
||||
Reference in New Issue
Block a user