All checks were successful
deploy / deploy (push) Successful in 33s
Proof: Adds repo-owned EURe-to-BTC request preflight, signing, gated live submission, durable request/result/outcome persistence, dashboard request lifecycle rows, and tests proving submitted/relay accepted are not completed without inventory movement. Assumptions: The NEAR Intents solver relay quote, publish_intent, and get_status JSON-RPC methods accept signed raw_ed25519 token_diff payloads with quote_hashes; live validation remains bounded to 5 EUR per attempt, at most five attempts, and 200 bps slippage. Still fake: Venue-native terminal fill linkage and fee-complete realized PnL are still unavailable; request completion is attributed from durable inventory deltas unless the venue later exposes a linked settlement id.
142 lines
5.2 KiB
JavaScript
142 lines
5.2 KiB
JavaScript
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 };
|
|
}
|