const BPS_DENOMINATOR = 10_000n; export function parseDecimalToUnits(value, decimals, { field = 'amount' } = {}) { const raw = String(value ?? '').trim(); if (!raw) throw new Error(`${field} is required`); if (!/^\d+(\.\d+)?$/.test(raw)) throw new Error(`${field} must be a positive decimal`); const [whole, fraction = ''] = raw.split('.'); if (fraction.length > decimals) throw new Error(`${field} has too many decimal places`); const paddedFraction = fraction.padEnd(decimals, '0'); const digits = `${whole}${paddedFraction}`.replace(/^0+/, '') || '0'; const units = BigInt(digits); if (units <= 0n) throw new Error(`${field} must be greater than zero`); return units.toString(); } export function formatUnitsDecimal(units, decimals) { const raw = String(units ?? '0'); const negative = raw.startsWith('-'); const digits = negative ? raw.slice(1) : raw; const padded = digits.padStart(decimals + 1, '0'); const whole = padded.slice(0, padded.length - decimals) || '0'; const fraction = decimals > 0 ? padded.slice(-decimals).replace(/0+$/, '') : ''; return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`; } export function computeBtcReceiveUnitsFromEure({ eureUnits, eurPerBtc, eureDecimals, btcDecimals, }) { const price = parsePositiveDecimal(eurPerBtc, { field: 'eur_per_btc' }); const sourceUnits = BigInt(String(eureUnits || '0')); if (sourceUnits <= 0n) throw new Error('eureUnits must be greater than zero'); const numerator = sourceUnits * (10n ** BigInt(btcDecimals)) * price.scale; const denominator = (10n ** BigInt(eureDecimals)) * price.units; if (denominator <= 0n) throw new Error('invalid price denominator'); return (numerator / denominator).toString(); } export function applySlippageBps(units, slippageBps) { const parsed = Number(slippageBps); if (!Number.isInteger(parsed) || parsed < 0 || parsed >= 10_000) { throw new Error('slippage_bps must be an integer between 0 and 9999'); } return ((BigInt(String(units || '0')) * (BPS_DENOMINATOR - BigInt(parsed))) / BPS_DENOMINATOR).toString(); } export function buildSolverQuoteRequest({ sourceAssetId, destinationAssetId, sourceAmountUnits, minDeadlineMs, }) { return { defuse_asset_identifier_in: sourceAssetId, defuse_asset_identifier_out: destinationAssetId, exact_amount_in: String(sourceAmountUnits), min_deadline_ms: Number(minDeadlineMs), }; } export function normalizeSolverQuotes(response) { const quotes = Array.isArray(response) ? response : Array.isArray(response?.quotes) ? response.quotes : Array.isArray(response?.result) ? response.result : response?.quote_hash || response?.hash ? [response] : []; return quotes .map((quote) => { const quoteHash = quote.quote_hash || quote.quoteHash || quote.hash || null; const amountIn = quote.amount_in ?? quote.exact_amount_in ?? quote.amountIn ?? null; const amountOut = quote.amount_out ?? quote.exact_amount_out ?? quote.amountOut ?? null; if (!quoteHash || amountOut == null) return null; return { quote_hash: String(quoteHash), amount_in: amountIn == null ? null : String(amountIn), amount_out: String(amountOut), expiration_time: quote.expiration_time || quote.expires_at || quote.expirationTime || null, raw: quote, }; }) .filter(Boolean); } export function selectBestSolverQuote(quotes, { minDestinationAmountUnits }) { const minimum = BigInt(String(minDestinationAmountUnits || '0')); let best = null; for (const quote of quotes || []) { const amountOut = BigInt(String(quote.amount_out || '0')); if (amountOut < minimum) continue; if (!best || amountOut > BigInt(String(best.amount_out || '0'))) best = quote; } return best; } export function normalizeRelayPublishResponse(response) { const body = response && typeof response === 'object' ? response : { status: response }; const intentHash = body.intent_hash || body.intentHash || body.hash || body.tx_hash || null; const relayStatus = body.status || body.result || (response === 'OK' ? 'OK' : null); const accepted = response === 'OK' || relayStatus === 'OK' || relayStatus === 'PENDING' || relayStatus === 'TX_BROADCASTED' || relayStatus === 'SETTLED' || Boolean(intentHash); return { accepted, intent_hash: intentHash ? String(intentHash) : null, relay_status: relayStatus ? String(relayStatus) : null, raw: response, }; } export function isExpired(timestamp, nowMs = Date.now()) { const parsed = Date.parse(timestamp || ''); return Number.isFinite(parsed) && parsed <= nowMs; } export function futureIso(nowMs, durationMs) { return new Date(nowMs + Number(durationMs || 0)).toISOString(); } function parsePositiveDecimal(value, { field }) { const raw = String(value ?? '').trim(); if (!/^\d+(\.\d+)?$/.test(raw)) throw new Error(`${field} must be a positive decimal`); const [whole, fraction = ''] = raw.split('.'); const scale = 10n ** BigInt(fraction.length); const units = BigInt(`${whole}${fraction}`.replace(/^0+/, '') || '0'); if (units <= 0n) throw new Error(`${field} must be greater than zero`); return { units, scale }; }