unrip/test/intent-requests.test.mjs
philipp f34f27065a
All checks were successful
deploy / deploy (push) Successful in 33s
Implement NEAR Intents request creation flow
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.
2026-04-12 18:43:40 +02:00

275 lines
9.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { KeyPair } from 'near-api-js';
import { createIntentRequestController } from '../src/core/intent-request-controller.mjs';
import {
applySlippageBps,
buildSolverQuoteRequest,
computeBtcReceiveUnitsFromEure,
normalizeSolverQuotes,
parseDecimalToUnits,
selectBestSolverQuote,
} from '../src/core/intent-requests.mjs';
const BTC = {
assetId: 'nep141:btc.omft.near',
symbol: 'BTC',
decimals: 8,
};
const EURE = {
assetId: 'nep141:eure.omft.near',
symbol: 'EURe',
decimals: 18,
};
function buildConfig() {
return {
tradingBtc: BTC,
tradingEure: EURE,
nearIntentsAccountId: 'unrip.test.near',
nearVerifierContract: 'intents.near',
intentRequestDefaultAmountEure: 5,
intentRequestMaxAmountEure: 5,
intentRequestDefaultSlippageBps: 200,
intentRequestMaxSlippageBps: 200,
intentRequestMinDeadlineMs: 60_000,
intentRequestQuoteTimeoutMs: 10_000,
intentRequestPublishTimeoutMs: 10_000,
intentRequestStatusTimeoutMs: 10_000,
intentRequestInventoryMaxAgeMs: 30_000,
intentRequestPriceMaxAgeMs: 30_000,
executorResponseTimeoutMs: 10_000,
};
}
function buildStore({
inventoryUnits = '5000000000000000000',
nowIso = '2026-04-12T10:00:00.000Z',
inventorySyncedAt = nowIso,
priceObservedAt = nowIso,
} = {}) {
const preflights = [];
const submissions = [];
return {
preflights,
submissions,
async loadLatestInventorySnapshot() {
return {
ingested_at: nowIso,
payload: {
synced_at: inventorySyncedAt,
spendable: {
[EURE.assetId]: inventoryUnits,
[BTC.assetId]: '1000',
},
pending_inbound: {
[EURE.assetId]: '100000000000000000000',
},
},
};
},
async loadLatestMarketPrice() {
return {
ingested_at: nowIso,
payload: {
observed_at: priceObservedAt,
eure_per_btc: '50000',
},
};
},
async insertPreflight(payload) {
preflights.push(payload);
},
async findPreflight({ requestId, idempotencyKey }) {
return preflights.find((entry) => (
(requestId && entry.request_id === requestId)
|| (idempotencyKey && entry.idempotency_key === idempotencyKey)
)) || null;
},
async findSubmissionByRequest({ requestId }) {
return [...submissions].reverse().find((entry) => entry.request_id === requestId) || null;
},
async insertSubmissionResult(payload) {
submissions.push(payload);
},
async refreshOutcomes() {
return [];
},
};
}
function buildRelay() {
return {
quoteCalls: 0,
publishCalls: 0,
async quote() {
this.quoteCalls += 1;
return [{
quote_hash: 'quote-hash-1',
amount_out: '10000',
expiration_time: '2026-04-12T10:01:00.000Z',
}];
},
async publishIntent() {
this.publishCalls += 1;
return { status: 'OK', intent_hash: 'intent-hash-1' };
},
async getStatus() {
return { status: 'PENDING' };
},
};
}
function buildController({ store = buildStore(), relay = buildRelay(), armed = true, verifierRegistered = true } = {}) {
return {
store,
relay,
controller: createIntentRequestController({
config: buildConfig(),
store,
relayRpcClient: relay,
verifierClient: {
async isPublicKeyRegistered() { return verifierRegistered; },
async currentSalt() { return '252812b3'; },
},
signer: KeyPair.fromRandom('ed25519'),
isArmed: () => armed,
isPaused: () => false,
now: () => Date.parse('2026-04-12T10:00:00.000Z'),
uuid: (() => {
let next = 1;
return () => `id-${next++}`;
})(),
}),
};
}
test('EURe decimal parsing, BTC expected receive, and slippage math are exact enough for request limits', () => {
const sourceUnits = parseDecimalToUnits('5', EURE.decimals, { field: 'amount_eure' });
const expectedBtc = computeBtcReceiveUnitsFromEure({
eureUnits: sourceUnits,
eurPerBtc: '50000',
eureDecimals: EURE.decimals,
btcDecimals: BTC.decimals,
});
assert.equal(sourceUnits, '5000000000000000000');
assert.equal(expectedBtc, '10000');
assert.equal(applySlippageBps(expectedBtc, 200), '9800');
});
test('solver quote normalization selects the best quote above explicit min receive', () => {
const quotes = normalizeSolverQuotes({
quotes: [
{ quote_hash: 'low', amount_out: '9700' },
{ quote_hash: 'best', amount_out: '10050' },
{ quote_hash: 'ok', amount_out: '9900' },
],
});
const selected = selectBestSolverQuote(quotes, { minDestinationAmountUnits: '9800' });
assert.equal(selected.quote_hash, 'best');
assert.deepEqual(buildSolverQuoteRequest({
sourceAssetId: EURE.assetId,
destinationAssetId: BTC.assetId,
sourceAmountUnits: '5000000000000000000',
minDeadlineMs: 60_000,
}), {
defuse_asset_identifier_in: EURE.assetId,
defuse_asset_identifier_out: BTC.assetId,
exact_amount_in: '5000000000000000000',
min_deadline_ms: 60000,
});
});
test('preflight is side-effect-free and does not publish a live intent', async () => {
const { controller, relay } = buildController();
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
assert.equal(preflight.state, 'draft');
assert.equal(preflight.live_submit_capable, true);
assert.equal(preflight.reason_code, 'quote_available');
assert.equal(relay.quoteCalls, 1);
assert.equal(relay.publishCalls, 0);
});
test('insufficient spendable EURe blocks before solver quote or signing', async () => {
const store = buildStore({ inventoryUnits: '0' });
const relay = buildRelay();
const { controller } = buildController({ store, relay });
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
assert.equal(preflight.state, 'blocked');
assert.equal(preflight.reason_code, 'insufficient_spendable_eure');
assert.equal(preflight.inventory_snapshot.pending_inbound[EURE.assetId], '100000000000000000000');
assert.equal(relay.quoteCalls, 0);
assert.equal(relay.publishCalls, 0);
});
test('duplicate request submit returns stored result and never publishes twice', async () => {
const { controller, store, relay } = buildController();
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
const first = await controller.submit({ request_id: preflight.request_id });
const second = await controller.submit({ request_id: preflight.request_id });
assert.equal(first.submission_result.status, 'accepted_by_relay');
assert.equal(second.duplicate, true);
assert.equal(relay.publishCalls, 1);
assert.equal(store.submissions.filter((entry) => entry.status === 'submit_requested').length, 1);
assert.equal(store.submissions.filter((entry) => entry.status === 'accepted_by_relay').length, 1);
});
test('executor disarmed blocks request submission without calling relay publish', async () => {
const { controller, relay } = buildController({ armed: false });
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
const result = await controller.submit({ request_id: preflight.request_id });
assert.equal(result.submission_result.status, 'blocked');
assert.equal(result.submission_result.result_code, 'executor_disarmed');
assert.equal(relay.publishCalls, 0);
});
test('stale reference price blocks request preflight before solver quote', async () => {
const store = buildStore({ priceObservedAt: '2026-04-12T09:59:00.000Z' });
const relay = buildRelay();
const { controller } = buildController({ store, relay });
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
assert.equal(preflight.state, 'blocked');
assert.equal(preflight.reason_code, 'stale_reference_price');
assert.equal(relay.quoteCalls, 0);
assert.equal(relay.publishCalls, 0);
});
test('unregistered signer blocks request preflight before solver quote or signing', async () => {
const relay = buildRelay();
const { controller } = buildController({ relay, verifierRegistered: false });
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
assert.equal(preflight.state, 'blocked');
assert.equal(preflight.reason_code, 'signer_not_registered');
assert.equal(relay.quoteCalls, 0);
assert.equal(relay.publishCalls, 0);
});
test('relay publish failure records submit_requested first and never reports completion', async () => {
const relay = buildRelay();
relay.publishIntent = async function publishIntent() {
this.publishCalls += 1;
throw new Error('relay publish unavailable');
};
const { controller, store } = buildController({ relay });
const preflight = await controller.preflight({ amount_eure: '5', slippage_bps: 200 });
const result = await controller.submit({ request_id: preflight.request_id });
assert.equal(result.submission_result.status, 'failed');
assert.equal(result.submission_result.result_code, 'publish_intent_failed');
assert.equal(result.submission_result.result_text, 'relay publish unavailable');
assert.equal(relay.publishCalls, 1);
assert.equal(store.submissions[0].status, 'submit_requested');
assert.equal(store.submissions[1].status, 'failed');
assert.notEqual(result.submission_result.status, 'completed');
});