add project

This commit is contained in:
ZOMBIIIIIII
2026-04-08 14:11:27 +03:00
parent bfa95223a0
commit a81e29807c
115 changed files with 18413 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
export const RELAY_PROXY_BASE_URL = '/api/relay';
export const RELAY_REQUEST_TIMEOUT_MS = 15_000;
// ── Bridge platform fee (0.7%) ──
export const BRIDGE_FEE_BPS = 70; // 0.7%
export const BRIDGE_FEE_RECIPIENT_EVM = '0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718';
export const BRIDGE_FEE_RECIPIENT_SOL = 'Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ';
export const BRIDGE_FEE_RECIPIENT_TRX = 'TYTfrem65362TFyQSARTheeYza1GQA37Ug';
// ─── Chain types ───
export type BridgeChainKey = 'ETH' | 'BSC' | 'SOL' | 'TRX';
export interface BridgeCurrencyConfig {
symbol: string;
address: string;
decimals: number;
}
export interface BridgeChainConfig {
key: BridgeChainKey;
label: string;
chainId: number;
walletChain: 'ETH' | 'BSC' | 'SOL' | 'TRX';
explorerTxBaseUrl: string;
tokens: Record<string, BridgeCurrencyConfig>;
}
export const BRIDGE_CHAINS: Record<BridgeChainKey, BridgeChainConfig> = {
ETH: {
key: 'ETH',
label: 'Ethereum',
chainId: 1,
walletChain: 'ETH',
explorerTxBaseUrl: 'https://etherscan.io/tx/',
tokens: {
ETH: { symbol: 'ETH', address: '0x0000000000000000000000000000000000000000', decimals: 18 },
USDT: { symbol: 'USDT', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 },
USDC: { symbol: 'USDC', address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6 },
},
},
SOL: {
key: 'SOL',
label: 'Solana',
chainId: 792703809,
walletChain: 'SOL',
explorerTxBaseUrl: 'https://solscan.io/tx/',
tokens: {
SOL: { symbol: 'SOL', address: '11111111111111111111111111111111', decimals: 9 },
USDT: { symbol: 'USDT', address: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', decimals: 6 },
USDC: { symbol: 'USDC', address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 },
},
},
BSC: {
key: 'BSC',
label: 'BNB Smart Chain',
chainId: 56,
walletChain: 'BSC',
explorerTxBaseUrl: 'https://bscscan.com/tx/',
tokens: {
BNB: { symbol: 'BNB', address: '0x0000000000000000000000000000000000000000', decimals: 18 },
USDT: { symbol: 'USDT', address: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 },
},
},
TRX: {
key: 'TRX',
label: 'TRON',
chainId: 728126428,
walletChain: 'TRX',
explorerTxBaseUrl: 'https://tronscan.org/#/transaction/',
tokens: {
USDT: { symbol: 'USDT', address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', decimals: 6 },
},
},
};
export const BRIDGE_CHAIN_OPTIONS: BridgeChainKey[] = ['ETH', 'BSC', 'SOL', 'TRX'];
// ─── Helpers ───
export function getDestinationChainOptions(sourceChain: BridgeChainKey): BridgeChainKey[] {
return BRIDGE_CHAIN_OPTIONS.filter((c) => c !== sourceChain);
}
export function getTokenOptions(chainKey: BridgeChainKey): string[] {
return Object.keys(BRIDGE_CHAINS[chainKey].tokens);
}
export function getDefaultToken(chainKey: BridgeChainKey): string {
const tokens = getTokenOptions(chainKey);
return tokens[0];
}
export function getTokenConfig(chainKey: BridgeChainKey, tokenSymbol: string): BridgeCurrencyConfig {
const token = BRIDGE_CHAINS[chainKey].tokens[tokenSymbol];
if (!token) {
throw new Error(`Token ${tokenSymbol} not found on ${BRIDGE_CHAINS[chainKey].label}`);
}
return token;
}
// ─── Request type ───
export interface BridgeQuoteRequest {
sourceChain: BridgeChainKey;
sourceToken: string;
destChain: BridgeChainKey;
destToken: string;
amount: string;
userAddress: string;
recipientAddress: string;
}

View File

@@ -0,0 +1,482 @@
import { ethers } from 'ethers';
import {
Connection,
Keypair,
PublicKey,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
AddressLookupTableAccount,
} from '@solana/web3.js';
import { createEthProvider } from '@/lib/eth-provider';
import { webEnv } from '@/lib/env';
import { BSC_GAS_PRICE } from '@/lib/crypto/bsc-constants';
import {
BRIDGE_CHAINS,
BRIDGE_FEE_BPS,
BRIDGE_FEE_RECIPIENT_EVM,
BRIDGE_FEE_RECIPIENT_SOL,
BRIDGE_FEE_RECIPIENT_TRX,
RELAY_PROXY_BASE_URL,
RELAY_REQUEST_TIMEOUT_MS,
type BridgeChainKey,
} from './constants';
import type { RelayQuoteResponse, RelayStep } from './quote';
const provider = createEthProvider();
// TYTfrem65362TFyQSARTheeYza1GQA37Ug → hex (20 bytes, no 0x prefix)
const BRIDGE_FEE_RECIPIENT_TRX_HEX = 'f6b4d4e650fc67982894f37ba97ab2496781ddb6';
interface ExecuteBridgeParams {
sourceChain: BridgeChainKey;
sourceToken: string;
originalAmount: string;
sourceTokenDecimals: number;
sourceTokenAddress: string;
privateKey: string;
quote: RelayQuoteResponse;
maxFeeGwei?: string | null;
priorityFeeGwei?: string | null;
}
export interface ExecuteBridgeResult {
requestId: string | null;
txHashes: string[];
}
export async function executeBridge(params: ExecuteBridgeParams): Promise<ExecuteBridgeResult> {
switch (params.sourceChain) {
case 'ETH':
return executeEvmBridge(params, provider);
case 'BSC':
return executeEvmBridge(params, new ethers.providers.StaticJsonRpcProvider(webEnv.bscRpcUrl, 56));
case 'SOL':
return executeSolBridge(params);
case 'TRX':
return executeTrxBridge(params);
}
}
// ─── EVM origin (existing logic) ───
async function executeEvmBridge(
params: ExecuteBridgeParams,
evmProvider: ethers.providers.Provider,
): Promise<ExecuteBridgeResult> {
const wallet = new ethers.Wallet(params.privateKey, evmProvider);
const isBsc = params.sourceChain === 'BSC';
const txHashes: string[] = [];
let requestId = params.quote.steps.find((s) => s.requestId)?.requestId ?? null;
// ── Send 0.7% platform fee before bridge ──
await sendEvmBridgeFee(wallet, params, isBsc);
for (const step of params.quote.steps) {
if (!step.items?.length) continue;
for (const item of step.items) {
if (step.kind === 'signature') {
await executeSignatureStep(wallet, step, item.data);
} else {
const hash = await executeEvmTransactionStep(wallet, item.data, params.maxFeeGwei, params.priorityFeeGwei, isBsc);
txHashes.push(hash);
}
requestId = requestId ?? step.requestId ?? extractRequestId(item.check?.endpoint);
}
}
return { requestId, txHashes };
}
async function executeEvmTransactionStep(
wallet: ethers.Wallet,
data: Record<string, any>,
maxFeeGwei?: string | null,
priorityFeeGwei?: string | null,
isBsc?: boolean,
): Promise<string> {
const gasOverrides = isBsc
? { gasPrice: BSC_GAS_PRICE }
: maxFeeGwei?.trim()
? {
maxFeePerGas: ethers.utils.parseUnits(maxFeeGwei, 'gwei'),
maxPriorityFeePerGas: ethers.utils.parseUnits(priorityFeeGwei?.trim() || '0.01', 'gwei'),
}
: {
...(data.maxFeePerGas ? { maxFeePerGas: ethers.BigNumber.from(data.maxFeePerGas) } : {}),
...(data.maxPriorityFeePerGas ? { maxPriorityFeePerGas: ethers.BigNumber.from(data.maxPriorityFeePerGas) } : {}),
};
const response = await wallet.sendTransaction({
to: data.to,
data: data.data,
value: data.value ? ethers.BigNumber.from(data.value) : ethers.constants.Zero,
gasLimit: data.gas ? ethers.BigNumber.from(data.gas) : undefined,
...gasOverrides,
});
const receipt = await response.wait();
if (!receipt || receipt.status !== 1) {
throw new Error('Bridge transaction reverted');
}
return response.hash;
}
// ─── Bridge Fee Helpers ───
async function sendEvmBridgeFee(wallet: ethers.Wallet, params: ExecuteBridgeParams, isBsc?: boolean): Promise<void> {
const fullAmountRaw = ethers.utils.parseUnits(params.originalAmount, params.sourceTokenDecimals);
const feeAmount = fullAmountRaw.mul(BRIDGE_FEE_BPS).div(10000);
if (feeAmount.isZero()) return;
const gasOverrides = isBsc ? { gasPrice: BSC_GAS_PRICE } : {};
const isNative = params.sourceTokenAddress === '0x0000000000000000000000000000000000000000';
if (isNative) {
const tx = await wallet.sendTransaction({
to: BRIDGE_FEE_RECIPIENT_EVM,
value: feeAmount,
...gasOverrides,
});
await tx.wait();
} else {
const tokenContract = new ethers.Contract(
params.sourceTokenAddress,
['function transfer(address to, uint256 amount) returns (bool)'],
wallet,
);
const tx = await tokenContract.transfer(BRIDGE_FEE_RECIPIENT_EVM, feeAmount, gasOverrides);
await tx.wait();
}
}
async function sendSolBridgeFee(
connection: Connection,
keypair: Keypair,
params: ExecuteBridgeParams,
): Promise<void> {
const { SystemProgram } = await import('@solana/web3.js');
const fullAmountRaw = BigInt(
Math.round(Number(params.originalAmount) * 10 ** params.sourceTokenDecimals),
);
const feeAmount = (fullAmountRaw * BigInt(BRIDGE_FEE_BPS)) / 10000n;
if (feeAmount === 0n) return;
const feeRecipient = new PublicKey(BRIDGE_FEE_RECIPIENT_SOL);
const isNative = params.sourceTokenAddress === '11111111111111111111111111111111';
// Bridge fee only supports native SOL transfers
// (SOL bridge primarily uses SOL, USDT, USDC — SPL fee handled off-chain if needed)
if (!isNative) return;
const instruction = SystemProgram.transfer({
fromPubkey: keypair.publicKey,
toPubkey: feeRecipient,
lamports: feeAmount,
});
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
const messageV0 = new TransactionMessage({
payerKey: keypair.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions: [instruction],
}).compileToV0Message();
const tx = new VersionedTransaction(messageV0);
tx.sign([keypair]);
const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: false });
await connection.confirmTransaction(
{ signature: sig, blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight },
'confirmed',
);
}
async function sendTrxBridgeFee(
signingKey: ethers.utils.SigningKey,
apiUrl: string,
params: ExecuteBridgeParams,
): Promise<void> {
const decimals = params.sourceTokenDecimals;
const fullAmountRaw = BigInt(
Math.round(Number(params.originalAmount) * 10 ** decimals),
);
const feeAmount = (fullAmountRaw * BigInt(BRIDGE_FEE_BPS)) / 10000n;
if (feeAmount === 0n) return;
// TRX bridge only supports USDT (TRC-20) — build a TRC20 transfer via API
// Use the tron proxy to build a transfer tx, sign it, and broadcast
const buildResp = await fetch(`${apiUrl}/api/tron/triggersmartcontract`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
owner_address: params.quote.steps[0]?.items?.[0]?.data?.parameter?.owner_address
?? ethers.utils.computeAddress(signingKey.publicKey).toLowerCase(),
contract_address: params.sourceTokenAddress,
function_selector: 'transfer(address,uint256)',
parameter:
BRIDGE_FEE_RECIPIENT_TRX_HEX.padStart(64, '0') +
feeAmount.toString(16).padStart(64, '0'),
call_value: 0,
fee_limit: 100000000,
visible: false,
}),
});
if (!buildResp.ok) return; // Fee transfer is best-effort; don't block bridge
const buildResult = await buildResp.json();
const tx = buildResult.transaction;
if (!tx?.txID) return;
// Sign and broadcast
const digest = ethers.utils.arrayify('0x' + tx.txID);
const signature = signingKey.signDigest(digest);
const sigHex = ethers.utils.joinSignature(signature).slice(2);
const broadcastResp = await fetch(`${apiUrl}/api/tron/broadcasttransaction`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...tx, signature: [sigHex] }),
});
// Wait for broadcast, but don't fail the bridge if fee transfer fails
await broadcastResp.json();
}
async function executeSignatureStep(wallet: ethers.Wallet, step: RelayStep, data: Record<string, any>): Promise<void> {
const signData = data.sign;
const postData = data.post;
if (!signData || !postData?.endpoint) {
throw new Error(`Invalid signature step payload for ${step.id}`);
}
const signature = await signRelayPayload(wallet, signData);
const endpoint = new URL(`${webEnv.apiUrl}${RELAY_PROXY_BASE_URL}${postData.endpoint}`);
endpoint.searchParams.set('signature', signature);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), RELAY_REQUEST_TIMEOUT_MS);
try {
const response = await fetch(endpoint.toString(), {
method: postData.method ?? 'POST',
signal: controller.signal,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData.body ?? {}),
});
if (!response.ok) {
const payload = await response.text();
throw new Error(payload || 'Relay signature submission failed');
}
} finally {
clearTimeout(timeoutId);
}
}
async function signRelayPayload(wallet: ethers.Wallet, signData: Record<string, any>): Promise<string> {
if (signData.signatureKind === 'eip191') {
const message = typeof signData.message === 'string' && signData.message.startsWith('0x')
? ethers.utils.arrayify(signData.message)
: signData.message;
return wallet.signMessage(message);
}
if (signData.signatureKind === 'eip712') {
const { EIP712Domain, ...types } = signData.types ?? {};
return wallet._signTypedData(signData.domain ?? {}, types, signData.value ?? {});
}
throw new Error(`Unsupported Relay signature kind: ${signData.signatureKind}`);
}
// ─── SOL origin ───
async function executeSolBridge(params: ExecuteBridgeParams): Promise<ExecuteBridgeResult> {
const connection = new Connection(webEnv.solRpcUrl, 'confirmed');
const keypair = Keypair.fromSecretKey(Buffer.from(params.privateKey, 'hex'));
const txHashes: string[] = [];
let requestId = params.quote.steps.find((s) => s.requestId)?.requestId ?? null;
// ── Send 0.7% platform fee before bridge ──
await sendSolBridgeFee(connection, keypair, params);
for (const step of params.quote.steps) {
if (!step.items?.length) continue;
for (const item of step.items) {
const data = item.data;
if (!data.instructions || !Array.isArray(data.instructions)) {
throw new Error('Expected Solana instructions in bridge step');
}
const hash = await executeSolTransactionStep(connection, keypair, data);
txHashes.push(hash);
requestId = requestId ?? step.requestId ?? extractRequestId(item.check?.endpoint);
}
}
return { requestId, txHashes };
}
async function executeSolTransactionStep(
connection: Connection,
keypair: Keypair,
data: Record<string, any>,
): Promise<string> {
// Build instructions from Relay response
const instructions: TransactionInstruction[] = data.instructions.map((ix: any) => ({
programId: new PublicKey(ix.programId),
keys: ix.keys.map((k: any) => ({
pubkey: new PublicKey(k.pubkey),
isSigner: k.isSigner,
isWritable: k.isWritable,
})),
data: Buffer.from(ix.data, 'hex'),
}));
// Load address lookup tables
const lookupTableAddresses: string[] = data.addressLookupTableAddresses ?? [];
const lookupTables: AddressLookupTableAccount[] = [];
for (const addr of lookupTableAddresses) {
const account = await connection.getAddressLookupTable(new PublicKey(addr));
if (account.value) {
lookupTables.push(account.value);
}
}
// Build versioned transaction
const latestBlockhash = await connection.getLatestBlockhash('confirmed');
const messageV0 = new TransactionMessage({
payerKey: keypair.publicKey,
recentBlockhash: latestBlockhash.blockhash,
instructions,
}).compileToV0Message(lookupTables);
const transaction = new VersionedTransaction(messageV0);
transaction.sign([keypair]);
// Send and confirm
const signature = await connection.sendRawTransaction(transaction.serialize(), {
skipPreflight: false,
maxRetries: 2,
});
await connection.confirmTransaction(
{
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
},
'confirmed',
);
return signature;
}
// ─── TRX origin ───
async function executeTrxBridge(params: ExecuteBridgeParams): Promise<ExecuteBridgeResult> {
const signingKey = new ethers.utils.SigningKey('0x' + params.privateKey);
const apiUrl = webEnv.apiUrl || 'http://localhost:3001';
const txHashes: string[] = [];
let requestId = params.quote.steps.find((s) => s.requestId)?.requestId ?? null;
// ── Send 0.7% platform fee before bridge ──
await sendTrxBridgeFee(signingKey, apiUrl, params);
for (const step of params.quote.steps) {
if (!step.items?.length) continue;
for (const item of step.items) {
const data = item.data;
if (data.type !== 'TriggerSmartContract') {
throw new Error(`Unsupported TRX step type: ${data.type}`);
}
const hash = await executeTrxTransactionStep(signingKey, apiUrl, data);
txHashes.push(hash);
requestId = requestId ?? step.requestId ?? extractRequestId(item.check?.endpoint);
}
// Small delay between steps (e.g., approve → deposit)
if (step.items.length > 0 && step !== params.quote.steps[params.quote.steps.length - 1]) {
await delay(3000);
}
}
return { requestId, txHashes };
}
async function executeTrxTransactionStep(
signingKey: ethers.utils.SigningKey,
apiUrl: string,
data: Record<string, any>,
): Promise<string> {
// 1. Build transaction via TronGrid triggersmartcontract
const buildResponse = await fetch(`${apiUrl}/api/tron/triggersmartcontract`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data.parameter),
});
if (!buildResponse.ok) {
const body = await buildResponse.json().catch(() => ({ error: 'Failed to build TRX bridge tx' }));
throw new Error(body.error || `TRX bridge build failed (${buildResponse.status})`);
}
const buildResult = await buildResponse.json();
const tx = buildResult.transaction;
if (!tx?.txID) {
throw new Error('TronGrid did not return a valid transaction');
}
// 2. Sign txID with secp256k1
const digest = ethers.utils.arrayify('0x' + tx.txID);
const signature = signingKey.signDigest(digest);
const sigHex = ethers.utils.joinSignature(signature).slice(2); // 65 bytes hex, no 0x
const signedTx = {
...tx,
signature: [sigHex],
};
// 3. Broadcast
const broadcastResponse = await fetch(`${apiUrl}/api/tron/broadcasttransaction`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedTx),
});
const result = await broadcastResponse.json();
if (!result.result) {
const errorMsg = result.message || result.code || 'TRX broadcast failed';
throw new Error(`TRX bridge broadcast error: ${errorMsg}`);
}
return tx.txID;
}
// ─── Utils ───
function extractRequestId(endpoint?: string): string | null {
if (!endpoint) return null;
try {
const url = new URL(endpoint, 'https://api.relay.link');
return url.searchParams.get('requestId');
} catch {
return null;
}
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,184 @@
import { ethers } from 'ethers';
import { webEnv } from '@/lib/env';
import {
BRIDGE_CHAINS,
BRIDGE_FEE_BPS,
RELAY_PROXY_BASE_URL,
RELAY_REQUEST_TIMEOUT_MS,
getTokenConfig,
type BridgeQuoteRequest,
} from './constants';
export interface RelayStep {
id: string;
kind: 'transaction' | 'signature';
requestId?: string;
items: Array<{
status: 'complete' | 'incomplete';
data: Record<string, any>;
check?: {
endpoint: string;
method: 'GET' | 'POST';
};
}>;
}
export interface RelayQuoteResponse {
steps: RelayStep[];
fees?: Record<string, { amountUsd?: string; amountFormatted?: string; currency?: { symbol?: string } }>;
details?: {
timeEstimate?: number;
currencyOut?: {
currency?: {
symbol?: string;
decimals?: number;
};
amount?: string;
amountFormatted?: string;
};
totalImpact?: {
usd?: string;
percent?: string;
};
slippageTolerance?: {
destination?: {
percent?: string;
};
};
};
}
export interface BridgeQuoteResult {
quote: RelayQuoteResponse;
sourceChain: string;
requestId: string | null;
outputAmountFormatted: string;
outputSymbol: string;
minimumAmountFormatted: string;
feeSummary: string;
timeEstimateSeconds: number | null;
}
export async function getBridgeQuote(request: BridgeQuoteRequest): Promise<BridgeQuoteResult> {
if (!request.amount || Number(request.amount) <= 0) {
throw new Error('Enter a valid bridge amount');
}
const sourceChainConfig = BRIDGE_CHAINS[request.sourceChain];
const destChainConfig = BRIDGE_CHAINS[request.destChain];
const sourceTokenConfig = getTokenConfig(request.sourceChain, request.sourceToken);
const destTokenConfig = getTokenConfig(request.destChain, request.destToken);
// Apply 0.7% platform fee — bridge only 99.3% of input
const fullAmountRaw = BigInt(parseAmountToRaw(request.amount, sourceTokenConfig.decimals));
const feeAmount = (fullAmountRaw * BigInt(BRIDGE_FEE_BPS)) / 10000n;
const amount = (fullAmountRaw - feeAmount).toString();
const quote = await fetchRelayJson<RelayQuoteResponse>(
`${webEnv.apiUrl}${RELAY_PROXY_BASE_URL}/quote/v2`,
{
method: 'POST',
body: JSON.stringify({
user: request.userAddress,
recipient: request.recipientAddress,
originChainId: sourceChainConfig.chainId,
destinationChainId: destChainConfig.chainId,
originCurrency: sourceTokenConfig.address,
destinationCurrency: destTokenConfig.address,
amount,
tradeType: 'EXACT_INPUT',
}),
},
);
const requestId = quote.steps.find((step) => step.requestId)?.requestId ?? null;
const currencyOut = quote.details?.currencyOut;
return {
quote,
sourceChain: request.sourceChain,
requestId,
outputAmountFormatted: currencyOut?.amountFormatted ?? 'Unavailable',
outputSymbol: currencyOut?.currency?.symbol ?? destTokenConfig.symbol,
minimumAmountFormatted: computeMinimumAmount(currencyOut, destTokenConfig.decimals),
feeSummary: buildFeeSummary(quote.fees),
timeEstimateSeconds: quote.details?.timeEstimate ?? null,
};
}
async function fetchRelayJson<T>(url: string, options: RequestInit): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), RELAY_REQUEST_TIMEOUT_MS);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...(options.headers ?? {}),
},
});
const payload = (await response.json()) as T & { message?: string };
if (!response.ok) {
throw new Error((payload as { message?: string }).message || 'Relay quote request failed');
}
return payload;
} finally {
clearTimeout(timeoutId);
}
}
function buildFeeSummary(fees: RelayQuoteResponse['fees']): string {
if (!fees) return 'Unavailable';
const usdTotal = Object.values(fees).reduce((total, fee) => {
const amountUsd = Number(fee.amountUsd ?? 0);
return Number.isFinite(amountUsd) ? total + amountUsd : total;
}, 0);
if (usdTotal > 0) return `$${usdTotal.toFixed(4)}`;
const relayerFee = fees.relayer;
if (relayerFee?.amountFormatted && relayerFee.currency?.symbol) {
return `${relayerFee.amountFormatted} ${relayerFee.currency.symbol}`;
}
return 'Unavailable';
}
function computeMinimumAmount(
currencyOut: NonNullable<RelayQuoteResponse['details']>['currencyOut'] | undefined,
decimals: number,
): string {
if (!currencyOut?.amount || !currencyOut.currency?.decimals) {
return 'Unavailable';
}
// Apply 2% slippage to displayed minimum
const raw = BigInt(currencyOut.amount);
const minimum = (raw * 98n) / 100n;
return formatRawUnits(minimum.toString(), currencyOut.currency.decimals);
}
function parseAmountToRaw(amount: string, decimals: number): string {
const parts = amount.split('.');
const whole = parts[0] || '0';
const fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals);
const raw = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction);
return raw.toString();
}
function formatRawUnits(raw: string, decimals: number): string {
const value = BigInt(raw);
if (value === 0n) return '0';
const divisor = 10n ** BigInt(decimals);
const whole = value / divisor;
const fraction = value % divisor;
if (fraction === 0n) return whole.toString();
return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`;
}

View File

@@ -0,0 +1,40 @@
import { webEnv } from '@/lib/env';
import { RELAY_PROXY_BASE_URL, RELAY_REQUEST_TIMEOUT_MS } from './constants';
export interface BridgeStatusResult {
status: 'waiting' | 'pending' | 'submitted' | 'success' | 'delayed' | 'refunded' | 'failure';
details?: string;
inTxHashes?: string[];
txHashes?: string[];
updatedAt?: number;
originChainId?: number;
destinationChainId?: number;
}
export async function getBridgeStatus(requestId: string): Promise<BridgeStatusResult> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), RELAY_REQUEST_TIMEOUT_MS);
try {
const response = await fetch(
`${webEnv.apiUrl}${RELAY_PROXY_BASE_URL}/intents/status/v3?requestId=${encodeURIComponent(requestId)}`,
{
signal: controller.signal,
cache: 'no-store',
}
);
const payload = (await response.json()) as BridgeStatusResult & { message?: string };
if (!response.ok) {
throw new Error(payload.message || 'Unable to fetch bridge status');
}
return payload;
} finally {
clearTimeout(timeoutId);
}
}
export function isBridgeTerminalStatus(status: BridgeStatusResult['status']): boolean {
return status === 'success' || status === 'failure' || status === 'refunded';
}