Implement NEAR Intents request creation flow
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.
This commit is contained in:
philipp 2026-04-12 18:43:40 +02:00
parent 55ece8f5f0
commit f34f27065a
20 changed files with 2781 additions and 18 deletions

View file

@ -85,6 +85,16 @@ data:
STRATEGY_INVENTORY_MAX_AGE_MS: "30000"
EXECUTOR_INITIAL_ARMED: "false"
EXECUTOR_RESPONSE_TIMEOUT_MS: "10000"
INTENT_REQUEST_DEFAULT_AMOUNT_EURE: "5"
INTENT_REQUEST_MAX_AMOUNT_EURE: "5"
INTENT_REQUEST_DEFAULT_SLIPPAGE_BPS: "200"
INTENT_REQUEST_MAX_SLIPPAGE_BPS: "200"
INTENT_REQUEST_MIN_DEADLINE_MS: "60000"
INTENT_REQUEST_QUOTE_TIMEOUT_MS: "10000"
INTENT_REQUEST_PUBLISH_TIMEOUT_MS: "10000"
INTENT_REQUEST_STATUS_TIMEOUT_MS: "10000"
INTENT_REQUEST_INVENTORY_MAX_AGE_MS: "30000"
INTENT_REQUEST_PRICE_MAX_AGE_MS: "30000"
LIQUIDITY_WITHDRAWALS_FROZEN: "true"
BTC_FUNDING_OBSERVER_ENABLED: "true"
BTC_FUNDING_OBSERVER_BASE_URL: https://mempool.space/api

View file

@ -13,6 +13,7 @@ import {
insertHistoryEvent,
loadLatestPortfolioMetric,
loadPortfolioMetricInputs,
refreshIntentRequestOutcomes,
refreshQuoteOutcomes,
upsertPortfolioMetric,
} from '../lib/postgres.mjs';
@ -60,6 +61,9 @@ const quoteOutcomeTopics = new Set([
config.kafkaTopicCmdExecuteTrade,
config.kafkaTopicExecTradeResult,
]);
const intentRequestOutcomeTopics = new Set([
config.kafkaTopicStateIntentInventory,
]);
for (const topic of topics) {
await consumer.subscribe({ topic, fromBeginning: false });
@ -80,6 +84,9 @@ const state = {
last_quote_outcomes_at: null,
latest_quote_outcomes: null,
quote_outcomes_error: null,
last_intent_request_outcomes_at: null,
latest_intent_request_outcomes: null,
intent_request_outcomes_error: null,
};
await refreshPortfolioMetrics().catch((error) => {
@ -88,6 +95,9 @@ await refreshPortfolioMetrics().catch((error) => {
await refreshQuoteOutcomeAttributions().catch((error) => {
state.quote_outcomes_error = serializeError(error);
});
await refreshIntentRequestOutcomeAttributions().catch((error) => {
state.intent_request_outcomes_error = serializeError(error);
});
await consumer.run({
eachMessage: async ({ topic, partition, message }) => {
@ -141,6 +151,19 @@ await consumer.run({
});
}
}
if (intentRequestOutcomeTopics.has(topic)) {
try {
await refreshIntentRequestOutcomeAttributions();
} catch (error) {
state.intent_request_outcomes_error = serializeError(error);
logger.error('intent_request_outcomes_refresh_failed', {
topic,
details: {
error: serializeError(error),
},
});
}
}
} catch (error) {
state.last_error = serializeError(error);
state.error_count += 1;
@ -282,6 +305,22 @@ async function refreshPortfolioMetrics() {
return state.latest_portfolio_metrics;
}
async function refreshIntentRequestOutcomeAttributions() {
const records = await refreshIntentRequestOutcomes(pool, {
btcAsset: config.tradingBtc,
eureAsset: config.tradingEure,
});
state.last_intent_request_outcomes_at = new Date().toISOString();
state.latest_intent_request_outcomes = {
refreshed_count: records.length,
completed_count: records.filter((entry) => entry.outcome_status === 'completed').length,
not_filled_count: records.filter((entry) => entry.outcome_status === 'not_filled').length,
awaiting_settlement_count: records.filter((entry) => entry.outcome_status === 'awaiting_settlement').length,
};
state.intent_request_outcomes_error = null;
return records;
}
async function refreshQuoteOutcomeAttributions() {
const records = await refreshQuoteOutcomes(pool, {
btcAsset: config.tradingBtc,

View file

@ -26,6 +26,7 @@ import { loadConfig } from '../lib/config.mjs';
import { fetchJson } from '../lib/http.mjs';
import {
createPostgresPool,
ensureHistorySchema,
loadCurrentFundingObservations,
loadLatestInventorySnapshot,
loadLatestMarketPrice,
@ -34,6 +35,7 @@ import {
loadRecentDepositStatuses,
loadRecentExecuteTradeCommands,
loadRecentExecutionResults,
loadRecentIntentRequests,
loadRecentQuoteOutcomes,
loadRecentTradeDecisions,
loadRecentQuotes,
@ -72,6 +74,7 @@ if (
const pool = createPostgresPool({
connectionString: config.postgresUrl,
});
await ensureHistorySchema(pool);
const staticAssets = await loadStaticAssets();
const initialServiceSnapshots = await loadServiceSnapshots();
@ -345,6 +348,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
recentExecuteTradeCommands,
recentExecutionResults,
recentQuoteOutcomes,
recentIntentRequests,
recentAlertTransitions,
serviceSnapshots,
] = await Promise.all([
@ -411,6 +415,16 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
[],
sourceErrors,
),
safeSourceLoad(
'recent_intent_requests',
() => loadRecentIntentRequests(pool, {
limit: 20,
btcAsset: config.tradingBtc,
eureAsset: config.tradingEure,
}),
[],
sourceErrors,
),
safeSourceLoad(
'recent_alert_transitions',
() => loadRecentAlertTransitions(pool, { limit: 20 }),
@ -435,6 +449,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
recentExecuteTradeCommands,
recentExecutionResults,
recentQuoteOutcomes,
recentIntentRequests,
recentAlertTransitions,
serviceSnapshots,
sourceErrors,

View file

@ -6,12 +6,28 @@ import { createArmedStateStore } from '../core/armed-state-store.mjs';
import { startControlApi } from '../core/control-api.mjs';
import { buildEventEnvelope, parseEventMessage } from '../core/event-envelope.mjs';
import { createExecutorStateStore } from '../core/executor-state-store.mjs';
import { createIntentRequestController } from '../core/intent-request-controller.mjs';
import { createLogger, serializeError } from '../core/log.mjs';
import { assertExecuteTradeCommand, assertTradeResult } from '../core/schemas.mjs';
import {
assertExecuteTradeCommand,
assertIntentRequestPreflightEvent,
assertIntentRequestSubmissionResultEvent,
assertTradeResult,
} from '../core/schemas.mjs';
import { loadConfig } from '../lib/config.mjs';
import {
createPostgresPool,
ensureHistorySchema,
insertHistoryEvent,
loadIntentRequestPreflightByIdOrKey,
loadLatestIntentRequestSubmission,
loadLatestInventorySnapshot,
loadLatestMarketPrice,
refreshIntentRequestOutcomes,
} from '../lib/postgres.mjs';
import { buildQuoteResponseSubmission } from '../venues/near-intents/signing.mjs';
import { startSolverRelayWs } from '../venues/near-intents/solver-relay-ws.mjs';
import { createVerifierClient } from '../venues/near-intents/verifier-client.mjs';
import { createSolverRelayRpcClient, createVerifierClient } from '../venues/near-intents/verifier-client.mjs';
import { ageMs } from '../core/runtime-health.mjs';
const config = loadConfig();
@ -53,6 +69,14 @@ const verifierClient = createVerifierClient({
signerPrivateKey: config.nearIntentsSignerPrivateKey,
});
const signer = verifierClient.getSigner();
const solverRelayRpcClient = createSolverRelayRpcClient({
rpcUrl: config.nearIntentsRpcUrl,
apiKey: config.nearIntentsApiKey,
});
const requestPool = createPostgresPool({
connectionString: config.postgresUrl,
});
await ensureHistorySchema(requestPool);
const relayClient = await startSolverRelayWs({
apiKey: config.nearIntentsApiKey,
wsUrl: config.nearIntentsWsUrl,
@ -80,8 +104,27 @@ const state = {
last_error: null,
in_flight_count: 0,
submitted_count: 0,
request_creation: {
last_preflight: null,
last_submission_result: null,
preflight_count: 0,
accepted_count: 0,
blocked_count: 0,
failed_count: 0,
},
};
const requestController = createIntentRequestController({
config,
store: createIntentRequestStore(),
relayRpcClient: solverRelayRpcClient,
verifierClient,
signer,
isArmed: () => state.armed,
isPaused: () => state.paused,
logger: logger.child({ component: 'intent-request-controller' }),
});
await consumer.subscribe({ topic: config.kafkaTopicCmdExecuteTrade, fromBeginning: false });
await consumer.run({
eachMessage: async ({ message }) => {
@ -286,6 +329,36 @@ const controlApi = startControlApi({
return { ok: true, paused: false };
},
},
{
method: 'POST',
path: '/intent-request/preflight',
handler: async ({ body }) => {
const result = await requestController.preflight(body || {});
state.request_creation.last_preflight = result;
state.request_creation.preflight_count += 1;
if (result.state === 'blocked') state.request_creation.blocked_count += 1;
return result;
},
},
{
method: 'POST',
path: '/intent-request/submit',
handler: async ({ body }) => {
const result = await requestController.submit(body || {});
if (result?.statusCode != null) return result;
state.request_creation.last_submission_result = result.submission_result || null;
const status = result.submission_result?.status;
if (status === 'accepted_by_relay') state.request_creation.accepted_count += 1;
if (status === 'blocked') state.request_creation.blocked_count += 1;
if (status === 'failed') state.request_creation.failed_count += 1;
return result;
},
},
{
method: 'POST',
path: '/intent-request/refresh-outcomes',
handler: async () => requestController.refreshOutcomes(),
},
{
method: 'POST',
path: '/drain',
@ -302,11 +375,69 @@ const controlApi = startControlApi({
],
});
function createIntentRequestStore() {
return {
loadLatestInventorySnapshot: () => loadLatestInventorySnapshot(requestPool),
loadLatestMarketPrice: () => loadLatestMarketPrice(requestPool),
findPreflight: ({ requestId = null, idempotencyKey = null } = {}) => (
loadIntentRequestPreflightByIdOrKey(requestPool, { requestId, idempotencyKey })
),
findSubmissionByRequest: ({ requestId } = {}) => (
loadLatestIntentRequestSubmission(requestPool, { requestId })
),
async insertPreflight(payload) {
const event = buildEventEnvelope({
source: 'trade-executor',
venue: 'near-intents',
eventType: 'intent_request_preflight',
observedAt: payload.created_at,
payload,
});
assertIntentRequestPreflightEvent(event);
await insertHistoryEvent(requestPool, {
table: 'intent_request_preflights',
topic: 'intent.request.preflight',
event,
record: {
quote_id: null,
pair: payload.source_asset_id + '->' + payload.destination_asset_id,
decision_key: payload.request_id,
},
});
},
async insertSubmissionResult(payload) {
const event = buildEventEnvelope({
source: 'trade-executor',
venue: 'near-intents',
eventType: 'intent_request_submission_result',
observedAt: payload.submitted_at,
payload,
});
assertIntentRequestSubmissionResultEvent(event);
await insertHistoryEvent(requestPool, {
table: 'intent_request_submission_results',
topic: 'intent.request.submission_result',
event,
record: {
quote_id: null,
pair: payload.source_asset_id + '->' + payload.destination_asset_id,
decision_key: payload.request_id,
},
});
},
refreshOutcomes: () => refreshIntentRequestOutcomes(requestPool, {
btcAsset: config.tradingBtc,
eureAsset: config.tradingEure,
}),
};
}
async function shutdown() {
await controlApi.close().catch(() => {});
relayClient.close();
await consumer.disconnect();
await producer.disconnect();
await requestPool.end().catch(() => {});
process.exit(0);
}

View file

@ -1,6 +1,8 @@
import {
assertExecuteTradeCommand,
assertFundingObservationEvent,
assertIntentRequestPreflightEvent,
assertIntentRequestSubmissionResultEvent,
assertInventorySnapshotEvent,
assertLiquidityActionEvent,
assertMarketPriceEvent,
@ -140,6 +142,32 @@ export function routeHistoryRecord({ topic, event }) {
decision_key: event.payload.command_id,
},
};
case 'intent.request.preflight':
assertIntentRequestPreflightEvent(event);
return {
table: 'intent_request_preflights',
record: {
event_id: event.event_id,
observed_at: event.observed_at,
ingested_at: event.ingested_at,
quote_id: null,
pair: `${event.payload.source_asset_id}->${event.payload.destination_asset_id}`,
decision_key: event.payload.request_id,
},
};
case 'intent.request.submission_result':
assertIntentRequestSubmissionResultEvent(event);
return {
table: 'intent_request_submission_results',
record: {
event_id: event.event_id,
observed_at: event.observed_at,
ingested_at: event.ingested_at,
quote_id: null,
pair: `${event.payload.source_asset_id}->${event.payload.destination_asset_id}`,
decision_key: event.payload.request_id,
},
};
default:
throw new Error(`Unsupported topic: ${topic}`);
}

View file

@ -0,0 +1,402 @@
import crypto from 'node:crypto';
import { serializeError } from './log.mjs';
import {
applySlippageBps,
buildSolverQuoteRequest,
computeBtcReceiveUnitsFromEure,
futureIso,
isExpired,
normalizeRelayPublishResponse,
normalizeSolverQuotes,
parseDecimalToUnits,
selectBestSolverQuote,
} from './intent-requests.mjs';
import { buildIntentRequestSubmission } from '../venues/near-intents/signing.mjs';
export function createIntentRequestController({
config,
store,
relayRpcClient,
verifierClient,
signer,
isArmed = () => false,
isPaused = () => false,
now = () => Date.now(),
uuid = () => crypto.randomUUID(),
logger = null,
} = {}) {
if (!config) throw new Error('config is required');
if (!store) throw new Error('store is required');
if (!relayRpcClient) throw new Error('relayRpcClient is required');
if (!verifierClient) throw new Error('verifierClient is required');
if (!signer) throw new Error('signer is required');
async function preflight(body = {}) {
const createdAt = new Date(now()).toISOString();
const requestId = String(body.request_id || uuid());
const idempotencyKey = String(body.idempotency_key || `intent-request:${requestId}`);
const sourceAsset = config.tradingEure;
const destinationAsset = config.tradingBtc;
const amountEure = String(body.amount_eure || config.intentRequestDefaultAmountEure || '5');
const slippageBps = Number(body.slippage_bps ?? config.intentRequestDefaultSlippageBps ?? 200);
const minDeadlineMs = Number(body.min_deadline_ms || config.intentRequestMinDeadlineMs || 60_000);
const maxAmountUnits = parseDecimalToUnits(
String(config.intentRequestMaxAmountEure || 5),
sourceAsset.decimals,
{ field: 'intent_request_max_amount_eure' },
);
let sourceAmountUnits = '0';
let expectedDestinationAmountUnits = '0';
let minDestinationAmountUnits = '0';
let inventorySnapshot = null;
let marketPrice = null;
let signerRegistered = null;
let solverQuoteResponse = null;
let solverQuotes = [];
let selectedQuote = null;
let state = 'blocked';
let reasonCode = 'preflight_failed';
let reasonText = 'Preflight failed before a solver quote was requested.';
let deadlineAt = futureIso(now(), minDeadlineMs);
let blockedBeforeQuote = false;
try {
sourceAmountUnits = parseDecimalToUnits(amountEure, sourceAsset.decimals, { field: 'amount_eure' });
if (BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
blockedBeforeQuote = true;
throw codedError(
'amount_exceeds_request_limit',
`Requested ${amountEure} EURe exceeds configured live request limit ${config.intentRequestMaxAmountEure || 5} EURe.`,
);
}
if (!Number.isInteger(slippageBps) || slippageBps < 0) {
blockedBeforeQuote = true;
throw codedError('invalid_slippage', 'Slippage must be a non-negative integer in basis points.');
}
if (slippageBps > Number(config.intentRequestMaxSlippageBps ?? 200)) {
blockedBeforeQuote = true;
throw codedError(
'slippage_exceeds_request_limit',
`Slippage ${slippageBps} bps exceeds configured limit ${config.intentRequestMaxSlippageBps ?? 200} bps.`,
);
}
[inventorySnapshot, marketPrice, signerRegistered] = await Promise.all([
store.loadLatestInventorySnapshot(),
store.loadLatestMarketPrice(),
verifierClient.isPublicKeyRegistered({ accountId: config.nearIntentsAccountId }),
]);
const inventoryObservedAt = inventorySnapshot?.payload?.synced_at || inventorySnapshot?.ingested_at || null;
const priceObservedAt = marketPrice?.payload?.observed_at || marketPrice?.ingested_at || null;
if (!inventorySnapshot?.payload?.spendable) {
blockedBeforeQuote = true;
throw codedError('inventory_unavailable', 'No spendable inventory snapshot is available.');
}
if (!isFresh(inventoryObservedAt, config.intentRequestInventoryMaxAgeMs ?? config.strategyInventoryMaxAgeMs, now())) {
blockedBeforeQuote = true;
throw codedError('stale_inventory', 'Inventory snapshot is too stale for request creation.');
}
if (!marketPrice?.payload?.eure_per_btc) {
blockedBeforeQuote = true;
throw codedError('reference_price_unavailable', 'No BTC/EUR reference price is available.');
}
if (!isFresh(priceObservedAt, config.intentRequestPriceMaxAgeMs ?? config.strategyPriceMaxAgeMs, now())) {
blockedBeforeQuote = true;
throw codedError('stale_reference_price', 'Reference price is too stale for request creation.');
}
if (signerRegistered !== true) {
blockedBeforeQuote = true;
throw codedError('signer_not_registered', 'Configured signer public key is not registered on the verifier contract.');
}
const spendableUnits = String(inventorySnapshot.payload.spendable[sourceAsset.assetId] || '0');
if (BigInt(spendableUnits) < BigInt(sourceAmountUnits)) {
blockedBeforeQuote = true;
throw codedError('insufficient_spendable_eure', 'Spendable EURe is below the requested amount.');
}
expectedDestinationAmountUnits = computeBtcReceiveUnitsFromEure({
eureUnits: sourceAmountUnits,
eurPerBtc: marketPrice.payload.eure_per_btc,
eureDecimals: sourceAsset.decimals,
btcDecimals: destinationAsset.decimals,
});
minDestinationAmountUnits = applySlippageBps(expectedDestinationAmountUnits, slippageBps);
solverQuoteResponse = await relayRpcClient.quote(
buildSolverQuoteRequest({
sourceAssetId: sourceAsset.assetId,
destinationAssetId: destinationAsset.assetId,
sourceAmountUnits,
minDeadlineMs,
}),
{ timeoutMs: config.intentRequestQuoteTimeoutMs || config.executorResponseTimeoutMs },
);
solverQuotes = normalizeSolverQuotes(solverQuoteResponse);
selectedQuote = selectBestSolverQuote(solverQuotes, { minDestinationAmountUnits });
if (!solverQuotes.length) {
throw codedError('solver_quote_unanswered', 'The relay returned no solver quotes for this request.');
}
if (!selectedQuote) {
throw codedError('quote_below_min_receive', 'Solver quotes were below the explicit minimum BTC receive amount.');
}
state = 'draft';
reasonCode = 'quote_available';
reasonText = 'Solver quote meets the explicit slippage/minimum receive policy.';
deadlineAt = selectedQuote.expiration_time || deadlineAt;
} catch (error) {
state = 'blocked';
reasonCode = error.code || 'preflight_failed';
reasonText = error.message || 'Preflight failed.';
if (!blockedBeforeQuote) logger?.warn?.('intent_request_preflight_blocked', {
details: { request_id: requestId, reason_code: reasonCode, error: serializeError(error) },
});
}
const payload = {
request_id: requestId,
idempotency_key: idempotencyKey,
state,
reason_code: reasonCode,
reason_text: reasonText,
source_asset_id: sourceAsset.assetId,
source_symbol: sourceAsset.symbol,
source_decimals: sourceAsset.decimals,
destination_asset_id: destinationAsset.assetId,
destination_symbol: destinationAsset.symbol,
destination_decimals: destinationAsset.decimals,
source_amount_units: sourceAmountUnits,
amount_eure: amountEure,
expected_destination_amount_units: expectedDestinationAmountUnits,
min_destination_amount_units: minDestinationAmountUnits,
quoted_destination_amount_units: selectedQuote?.amount_out || null,
slippage_bps: slippageBps,
min_deadline_ms: minDeadlineMs,
deadline_at: deadlineAt,
signer_account_id: config.nearIntentsAccountId,
signer_public_key: signer.getPublicKey().toString(),
signer_registered: signerRegistered,
verifier_contract: config.nearVerifierContract,
nonce_policy: 'current_salt_plus_random_28_bytes_on_submit',
inventory_snapshot: inventorySnapshot ? {
ingested_at: inventorySnapshot.ingested_at,
synced_at: inventorySnapshot.payload?.synced_at || null,
spendable: inventorySnapshot.payload?.spendable || {},
pending_inbound: inventorySnapshot.payload?.pending_inbound || {},
} : null,
market_price: marketPrice ? {
ingested_at: marketPrice.ingested_at,
observed_at: marketPrice.payload?.observed_at || null,
eure_per_btc: marketPrice.payload?.eure_per_btc || null,
} : null,
solver_quote_count: solverQuotes.length,
selected_quote: selectedQuote,
relay_quote_response: solverQuoteResponse,
live_submit_capable: state === 'draft',
created_at: createdAt,
lifecycle: {
came_in_at: createdAt,
preflight_at: createdAt,
quote_requested_at: blockedBeforeQuote ? null : createdAt,
decision: state,
decisive_reason: reasonCode,
},
};
await store.insertPreflight(payload);
return payload;
}
async function submit(body = {}) {
const preflight = await store.findPreflight({
requestId: body.request_id || null,
idempotencyKey: body.idempotency_key || null,
});
if (!preflight) {
return { statusCode: 404, payload: { error: 'request_preflight_not_found' } };
}
const existing = await store.findSubmissionByRequest({ requestId: preflight.request_id });
if (existing) {
return {
duplicate: true,
preflight,
submission_result: existing,
};
}
if (preflight.state !== 'draft' || !preflight.live_submit_capable || !preflight.selected_quote?.quote_hash) {
const blocked = await recordSubmissionResult(preflight, {
status: 'blocked',
result_code: preflight.reason_code || 'preflight_not_submittable',
result_text: 'Preflight is not live-submit capable.',
});
return { preflight, submission_result: blocked };
}
if (isPaused()) {
const blocked = await recordSubmissionResult(preflight, {
status: 'blocked',
result_code: 'executor_paused',
result_text: 'Executor request submission is paused.',
});
return { preflight, submission_result: blocked };
}
if (!isArmed()) {
const blocked = await recordSubmissionResult(preflight, {
status: 'blocked',
result_code: 'executor_disarmed',
result_text: 'Executor is disarmed; live request was not submitted.',
});
return { preflight, submission_result: blocked };
}
if (isExpired(preflight.deadline_at, now())) {
const blocked = await recordSubmissionResult(preflight, {
status: 'blocked',
result_code: 'selected_quote_expired',
result_text: 'Selected solver quote expired before submit.',
});
return { preflight, submission_result: blocked };
}
const latestInventory = await store.loadLatestInventorySnapshot();
const spendableUnits = String(latestInventory?.payload?.spendable?.[preflight.source_asset_id] || '0');
if (BigInt(spendableUnits) < BigInt(preflight.source_amount_units)) {
const blocked = await recordSubmissionResult(preflight, {
status: 'blocked',
result_code: 'insufficient_spendable_eure',
result_text: 'Spendable EURe changed below the requested amount before submit.',
});
return { preflight, submission_result: blocked };
}
const submissionId = String(body.submission_id || uuid());
await recordSubmissionResult(preflight, {
submissionId,
status: 'submit_requested',
result_code: 'submit_requested',
result_text: 'Durable submit request was recorded before relay publish.',
});
try {
const currentSaltHex = await verifierClient.currentSalt();
const submission = buildIntentRequestSubmission({
request: preflight,
signerAccountId: config.nearIntentsAccountId,
signer,
verifierContract: config.nearVerifierContract,
currentSaltHex,
});
const relayResponse = await relayRpcClient.publishIntent({
quoteHashes: submission.quote_hashes,
signedData: submission.signed_data,
}, { timeoutMs: config.intentRequestPublishTimeoutMs || config.executorResponseTimeoutMs });
const normalized = normalizeRelayPublishResponse(relayResponse);
let relayStatusResponse = null;
let relayStatus = normalized.relay_status;
let statusCheckedAt = null;
if (normalized.intent_hash) {
statusCheckedAt = new Date(now()).toISOString();
relayStatusResponse = await relayRpcClient.getStatus(
normalized.intent_hash,
{ timeoutMs: config.intentRequestStatusTimeoutMs || config.executorResponseTimeoutMs },
).catch((error) => ({ error: serializeError(error) }));
relayStatus = relayStatusResponse?.status || relayStatus;
}
const result = await recordSubmissionResult(preflight, {
submissionId,
status: normalized.accepted ? 'accepted_by_relay' : 'failed',
result_code: normalized.accepted ? 'publish_intent_accepted' : 'publish_intent_rejected',
result_text: normalized.accepted
? 'Relay accepted the signed request. This is not settlement.'
: 'Relay rejected the signed request.',
quote_hash: submission.quote_hashes[0],
intent_hash: normalized.intent_hash,
destination_amount_units: submission.destination_amount_units,
nonce: submission.nonce,
signed_payload: submission.signed_payload,
relay_response: relayResponse,
relay_status: relayStatus || null,
relay_status_response: relayStatusResponse,
status_checked_at: statusCheckedAt,
});
await store.refreshOutcomes?.();
return { preflight, submission_result: result };
} catch (error) {
const failed = await recordSubmissionResult(preflight, {
submissionId,
status: 'failed',
result_code: 'publish_intent_failed',
result_text: error.message || 'Relay publish failed.',
error: serializeError(error),
});
await store.refreshOutcomes?.();
return { preflight, submission_result: failed };
}
}
async function refreshOutcomes() {
const outcomes = await store.refreshOutcomes?.();
return { ok: true, outcomes: outcomes || [] };
}
async function recordSubmissionResult(preflight, {
submissionId = null,
status,
result_code,
result_text,
...extra
}) {
const submittedAt = new Date(now()).toISOString();
const payload = {
request_id: preflight.request_id,
idempotency_key: preflight.idempotency_key,
submission_id: submissionId || uuid(),
status,
result_code,
result_text,
submitted_at: submittedAt,
source_asset_id: preflight.source_asset_id,
destination_asset_id: preflight.destination_asset_id,
source_amount_units: preflight.source_amount_units,
min_destination_amount_units: preflight.min_destination_amount_units,
quote_hash: preflight.selected_quote?.quote_hash || extra.quote_hash || null,
lifecycle: {
submit_requested_at: submittedAt,
relay_result_at: status === 'submit_requested' ? null : submittedAt,
state: status,
decisive_reason: result_code,
},
...extra,
};
await store.insertSubmissionResult(payload);
return payload;
}
return {
preflight,
submit,
refreshOutcomes,
};
}
function isFresh(timestamp, maxAgeMs, nowMs) {
const parsed = Date.parse(timestamp || '');
if (!Number.isFinite(parsed)) return false;
return nowMs - parsed <= Number(maxAgeMs || 0);
}
function codedError(code, message) {
const error = new Error(message);
error.code = code;
return error;
}

View file

@ -0,0 +1,459 @@
const DEFAULT_ATTRIBUTION_WINDOW_MS = 10 * 60 * 1000;
const DEFAULT_SETTLEMENT_GRACE_MS = 60 * 1000;
export const REQUEST_TERMINAL_SETTLEMENT_ATTRIBUTION_STATUSES = new Set([
'linked_settlement',
'heuristic_match',
]);
export function deriveIntentRequestOutcomeRecords({
preflights = [],
submissions = [],
inventorySnapshots = [],
btcAsset,
eureAsset,
now = Date.now(),
attributionWindowMs = DEFAULT_ATTRIBUTION_WINDOW_MS,
settlementGraceMs = DEFAULT_SETTLEMENT_GRACE_MS,
} = {}) {
const activeAssetIds = [btcAsset?.assetId, eureAsset?.assetId].filter(Boolean);
const preflightsByRequest = new Map(
preflights
.map(normalizePreflight)
.filter((entry) => entry?.request_id)
.map((entry) => [entry.request_id, entry]),
);
const latestSubmissionByRequest = new Map();
for (const submission of submissions.map(normalizeSubmission).filter(Boolean)) {
if (!submission.request_id) continue;
const previous = latestSubmissionByRequest.get(submission.request_id);
if (!previous || timestampValue(submission.submitted_at) >= timestampValue(previous.submitted_at)) {
latestSubmissionByRequest.set(submission.request_id, submission);
}
}
const inventoryDeltas = deriveInventoryDeltas({
inventorySnapshots,
activeAssetIds,
});
const latestInventoryAt = latestSnapshotTimestamp(inventorySnapshots);
return [...preflightsByRequest.values()]
.map((preflight) => deriveOneOutcome({
preflight,
submission: latestSubmissionByRequest.get(preflight.request_id) || null,
inventoryDeltas,
latestInventoryAt,
now,
attributionWindowMs,
settlementGraceMs,
}))
.filter(Boolean);
}
export function deriveInventoryDeltas({ inventorySnapshots = [], activeAssetIds = [] } = {}) {
const sorted = inventorySnapshots
.map(normalizeInventorySnapshot)
.filter((entry) => entry?.observed_at)
.sort((left, right) => timestampValue(left.observed_at) - timestampValue(right.observed_at));
const deltas = [];
for (let index = 1; index < sorted.length; index += 1) {
const previous = sorted[index - 1];
const current = sorted[index];
const deltaUnits = {};
let changed = false;
for (const assetId of activeAssetIds) {
const delta = safeBigInt(current.spendable?.[assetId]) - safeBigInt(previous.spendable?.[assetId]);
deltaUnits[assetId] = delta.toString();
if (delta !== 0n) changed = true;
}
if (!changed) continue;
deltas.push({
movement_id: `${previous.observed_at}->${current.observed_at}`,
observed_at: current.observed_at,
previous_observed_at: previous.observed_at,
inventory_id: current.inventory_id,
previous_inventory_id: previous.inventory_id,
delta_units: deltaUnits,
});
}
return deltas;
}
function deriveOneOutcome({
preflight,
submission,
inventoryDeltas,
latestInventoryAt,
now,
attributionWindowMs,
settlementGraceMs,
}) {
if (!submission) {
return baseOutcomeRecord({
preflight,
submission: null,
outcome_status: preflight.state === 'draft' ? 'draft' : 'blocked',
outcome_observed_at: preflight.created_at,
outcome_source: 'request_preflight',
outcome_reason: preflight.reason_code || 'preflight_recorded',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: { preflight_state: preflight.state },
});
}
if (['failed', 'blocked'].includes(submission.status)) {
return baseOutcomeRecord({
preflight,
submission,
outcome_status: submission.status,
outcome_observed_at: submission.submitted_at,
outcome_source: 'request_submission_result',
outcome_reason: submission.result_code || submission.status,
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: { relay_status: submission.relay_status || null },
});
}
const expectedDelta = buildExpectedRequestDelta(preflight, submission);
const matches = expectedDelta
? inventoryDeltas.filter((movement) => movementMatchesExpectedDelta({
movement,
expectedDelta,
submittedAt: submission.submitted_at,
attributionWindowMs,
}))
: [];
if (matches.length === 1) {
const movement = matches[0];
return baseOutcomeRecord({
preflight,
submission,
outcome_status: 'completed',
outcome_observed_at: movement.observed_at,
outcome_source: 'intent_inventory_spendable_delta',
outcome_reason: 'matched_inventory_delta',
attribution_status: 'heuristic_match',
attribution_method: 'exact_asset_delta_within_window',
attributed_inventory_delta: {
inventory_id: movement.inventory_id,
previous_inventory_id: movement.previous_inventory_id,
observed_at: movement.observed_at,
previous_observed_at: movement.previous_observed_at,
delta_units: movement.delta_units,
attribution_window_ms: attributionWindowMs,
uncertainty:
'Matched by exact asset-unit delta after request submission; no venue-native fill id is linked.',
},
evidence: {
settlement_movement_id: movement.movement_id,
settlement_source: 'intent_inventory_snapshots',
relay_status: submission.relay_status || null,
},
});
}
if (matches.length > 1) {
return baseOutcomeRecord({
preflight,
submission,
outcome_status: 'awaiting_settlement',
outcome_observed_at: submission.submitted_at,
outcome_source: 'request_submission_and_inventory_snapshots',
outcome_reason: 'ambiguous_inventory_delta_match',
attribution_status: 'ambiguous',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
candidate_movement_count: matches.length,
candidate_movement_ids: matches.map((entry) => entry.movement_id),
relay_status: submission.relay_status || null,
},
});
}
if (submission.relay_status === 'NOT_FOUND_OR_NOT_VALID') {
return baseOutcomeRecord({
preflight,
submission,
outcome_status: 'not_filled',
outcome_observed_at: submission.status_checked_at || submission.submitted_at,
outcome_source: 'solver_relay_get_status',
outcome_reason: 'relay_not_found_or_not_valid',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
relay_status: submission.relay_status,
relay_status_response: submission.relay_status_response,
},
});
}
const expiredWindow = getExpiredSettlementWindow({
submission,
preflight,
latestInventoryAt,
now,
settlementGraceMs,
});
if (expiredWindow) {
return baseOutcomeRecord({
preflight,
submission,
outcome_status: 'not_filled',
outcome_observed_at: expiredWindow.latestInventoryAt || expiredWindow.expiresAt || submission.submitted_at,
outcome_source: 'request_deadline_and_inventory_snapshots',
outcome_reason: 'deadline_elapsed_without_settlement',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
deadline_at: preflight.deadline_at || null,
settlement_grace_ms: settlementGraceMs,
settlement_window_expired_at: expiredWindow.expiresAt,
latest_inventory_observed_at: expiredWindow.latestInventoryAt,
relay_status: submission.relay_status || null,
},
});
}
return baseOutcomeRecord({
preflight,
submission,
outcome_status: 'awaiting_settlement',
outcome_observed_at: submission.submitted_at,
outcome_source: 'request_submission_result',
outcome_reason: submission.relay_status === 'SETTLED'
? 'relay_settled_but_inventory_delta_missing'
: 'accepted_by_relay_without_settlement',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
relay_status: submission.relay_status || null,
relay_status_response: submission.relay_status_response || null,
uncertainty:
'Relay acceptance or status is not counted as completed until inventory movement is linked.',
},
});
}
function buildExpectedRequestDelta(preflight, submission) {
const destinationAmount = submission?.destination_amount_units
|| preflight?.selected_quote?.amount_out
|| preflight?.quoted_destination_amount_units;
if (!preflight?.source_asset_id || !preflight?.destination_asset_id) return null;
if (!preflight?.source_amount_units || !destinationAmount) return null;
return {
[preflight.source_asset_id]: -safeBigInt(preflight.source_amount_units),
[preflight.destination_asset_id]: safeBigInt(destinationAmount),
};
}
function baseOutcomeRecord({
preflight,
submission,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
evidence,
}) {
const payload = {
request_id: preflight.request_id,
idempotency_key: preflight.idempotency_key,
submission_id: submission?.submission_id || null,
intent_hash: submission?.intent_hash || null,
source_asset_id: preflight.source_asset_id,
destination_asset_id: preflight.destination_asset_id,
source_amount_units: preflight.source_amount_units,
destination_amount_units: submission?.destination_amount_units || null,
min_destination_amount_units: preflight.min_destination_amount_units,
quote_hash: submission?.quote_hash || preflight.selected_quote?.quote_hash || null,
submitted_at: submission?.submitted_at || null,
deadline_at: preflight.deadline_at || null,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
evidence,
};
return {
request_id: preflight.request_id,
idempotency_key: preflight.idempotency_key,
submission_id: submission?.submission_id || null,
intent_hash: submission?.intent_hash || null,
submission_status: submission?.status || null,
relay_status: submission?.relay_status || null,
submitted_at: submission?.submitted_at || null,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
payload,
};
}
function movementMatchesExpectedDelta({
movement,
expectedDelta,
submittedAt,
attributionWindowMs,
}) {
const submittedTs = timestampValue(submittedAt);
const movementTs = timestampValue(movement.observed_at);
if (!Number.isFinite(submittedTs) || !Number.isFinite(movementTs)) return false;
if (movementTs < submittedTs) return false;
if (movementTs - submittedTs > attributionWindowMs) return false;
for (const [assetId, expected] of Object.entries(expectedDelta)) {
if (safeBigInt(movement.delta_units?.[assetId]) !== expected) return false;
}
return true;
}
function getExpiredSettlementWindow({
submission,
preflight,
latestInventoryAt,
now,
settlementGraceMs,
}) {
const submittedTs = timestampValue(submission.submitted_at);
if (!Number.isFinite(submittedTs)) return null;
const deadlineTs = timestampValue(preflight.deadline_at);
const fallbackDeadlineMs = Number(preflight.min_deadline_ms || 60_000);
const expiresAt = Number.isFinite(deadlineTs)
? deadlineTs + settlementGraceMs
: submittedTs + (Number.isFinite(fallbackDeadlineMs) ? fallbackDeadlineMs : 60_000) + settlementGraceMs;
const nowTs = typeof now === 'number' ? now : timestampValue(now);
const latestInventoryTs = timestampValue(latestInventoryAt);
if (
Number.isFinite(nowTs)
&& nowTs >= expiresAt
&& Number.isFinite(latestInventoryTs)
&& latestInventoryTs >= expiresAt
) {
return {
expiresAt: new Date(expiresAt).toISOString(),
latestInventoryAt: toIsoTimestamp(latestInventoryAt),
};
}
return null;
}
function normalizePreflight(entry) {
const payload = payloadOf(entry);
if (!payload) return null;
return {
request_id: payload.request_id || entry?.request_id || null,
idempotency_key: payload.idempotency_key || entry?.idempotency_key || null,
state: payload.state || null,
reason_code: payload.reason_code || null,
source_asset_id: payload.source_asset_id || null,
destination_asset_id: payload.destination_asset_id || null,
source_amount_units: payload.source_amount_units || null,
min_destination_amount_units: payload.min_destination_amount_units || null,
quoted_destination_amount_units: payload.quoted_destination_amount_units || null,
selected_quote: payload.selected_quote || null,
min_deadline_ms: payload.min_deadline_ms || null,
deadline_at: payload.deadline_at || null,
created_at: toIsoTimestamp(
payload.created_at
|| entry?.observed_at
|| entry?.ingested_at,
),
};
}
function normalizeSubmission(entry) {
const payload = payloadOf(entry);
if (!payload) return null;
return {
request_id: payload.request_id || entry?.request_id || null,
idempotency_key: payload.idempotency_key || entry?.idempotency_key || null,
submission_id: payload.submission_id || null,
status: payload.status || null,
result_code: payload.result_code || null,
quote_hash: payload.quote_hash || null,
intent_hash: payload.intent_hash || null,
destination_amount_units: payload.destination_amount_units || null,
submitted_at: toIsoTimestamp(
payload.submitted_at
|| entry?.observed_at
|| entry?.ingested_at,
),
relay_status: payload.relay_status || null,
relay_status_response: payload.relay_status_response || null,
status_checked_at: payload.status_checked_at || null,
};
}
function normalizeInventorySnapshot(entry) {
const payload = payloadOf(entry);
if (!payload?.spendable) return null;
return {
inventory_id: payload.inventory_id || null,
observed_at: toIsoTimestamp(
entry?.observed_at
|| entry?.ingested_at
|| payload.observed_at
|| payload.synced_at,
),
spendable: payload.spendable || {},
};
}
function latestSnapshotTimestamp(inventorySnapshots) {
const timestamps = inventorySnapshots
.map((entry) => normalizeInventorySnapshot(entry)?.observed_at)
.filter(Boolean)
.sort((left, right) => timestampValue(right) - timestampValue(left));
return timestamps[0] || null;
}
function payloadOf(entry) {
if (!entry) return null;
return entry.payload || entry;
}
function safeBigInt(value) {
if (value == null || value === '') return 0n;
return BigInt(String(value));
}
function toIsoTimestamp(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function timestampValue(value) {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : NaN;
}

View file

@ -0,0 +1,142 @@
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 };
}

View file

@ -46,6 +46,36 @@ const CONTROL_DEFINITIONS = [
page: 'funds',
risk_class: 'safe',
},
{
service: 'trade-executor',
action: 'intent-request-preflight',
method: 'POST',
path: '/intent-request/preflight',
label: 'Preflight BTC Request',
description: 'Ask solvers for an EURe-to-BTC request quote without submitting live funds.',
page: 'funds',
risk_class: 'safe',
},
{
service: 'trade-executor',
action: 'intent-request-submit',
method: 'POST',
path: '/intent-request/submit',
label: 'Submit BTC Request',
description: 'Submit a previously drafted EURe-to-BTC request. Relay acceptance is not settlement.',
page: 'funds',
risk_class: 'live_funds',
},
{
service: 'trade-executor',
action: 'intent-request-refresh-outcomes',
method: 'POST',
path: '/intent-request/refresh-outcomes',
label: 'Refresh Request Outcomes',
description: 'Recompute own-request settlement attribution from durable inventory snapshots.',
page: 'funds',
risk_class: 'safe',
},
{
service: 'liquidity-manager',
action: 'refresh',
@ -317,6 +347,7 @@ export function buildDashboardBootstrap({
recentExecuteTradeCommands,
recentExecutionResults,
recentQuoteOutcomes = [],
recentIntentRequests = [],
recentAlertTransitions,
serviceSnapshots,
sourceErrors = [],
@ -378,6 +409,11 @@ export function buildDashboardBootstrap({
submissions: normalizedSubmissionPage.items,
}),
recent_quotes: (recentQuotes || []).slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
intent_requests: buildIntentRequestSummary({
config,
intentRequests: recentIntentRequests,
executorState: servicesByName['trade-executor']?.state || {},
}),
submission_ledger: normalizedSubmissionPage,
controls: listDashboardControls({ page: 'funds' }),
caveats: profitability.caveats,
@ -645,6 +681,52 @@ function buildFundingSummary({ config, fundingObservations, recentDepositStatuse
};
}
function buildIntentRequestSummary({ config, intentRequests = [], executorState = {} } = {}) {
return {
defaults: {
source_symbol: config.tradingEure.symbol,
destination_symbol: config.tradingBtc.symbol,
amount_eure: String(config.intentRequestDefaultAmountEure || 5),
max_amount_eure: String(config.intentRequestMaxAmountEure || 5),
slippage_bps: Number(config.intentRequestDefaultSlippageBps ?? 200),
max_slippage_bps: Number(config.intentRequestMaxSlippageBps ?? 200),
},
executor_armed: executorState.armed ?? null,
executor_paused: executorState.paused ?? null,
request_creation_state: executorState.request_creation || null,
items: (intentRequests || []).map((request) => normalizeIntentRequestForUi({ config, request })),
caveat:
'Own request relay acceptance is not a completed trade. Completed requires durable EURe decrease and BTC increase evidence.',
};
}
function normalizeIntentRequestForUi({ config, request }) {
const sourceAsset = config.assetRegistry.get(request.source_asset_id) || config.tradingEure;
const destinationAsset = config.assetRegistry.get(request.destination_asset_id) || config.tradingBtc;
return {
...request,
source_amount: formatUnits(request.source_amount_units || '0', sourceAsset?.decimals || 0),
expected_destination_amount: formatUnits(
request.expected_destination_amount_units || '0',
destinationAsset?.decimals || 0,
),
min_destination_amount: formatUnits(
request.min_destination_amount_units || '0',
destinationAsset?.decimals || 0,
),
quoted_destination_amount: request.quoted_destination_amount_units == null
? null
: formatUnits(request.quoted_destination_amount_units || '0', destinationAsset?.decimals || 0),
settlement_summary: buildSettlementSummary({
config,
delta: request.attributed_inventory_delta,
attributionStatus: request.attribution_status,
attributionMethod: request.attribution_method,
subject: 'request',
}),
};
}
function buildRecentWithdrawals({ config, liquidityState }) {
return Object.values(liquidityState?.tracked_withdrawals || {})
.sort((left, right) => sortTimestamps(
@ -1425,15 +1507,21 @@ function estimateGrossEdgeValueEure(row) {
return value.toFixed(8).replace(/\.?0+$/, '');
}
function buildSettlementSummary({ config, delta, attributionStatus, attributionMethod }) {
function buildSettlementSummary({
config,
delta,
attributionStatus,
attributionMethod,
subject = 'quote',
}) {
if (!delta?.delta_units) {
return {
status: attributionStatus || 'unattributed',
method: attributionMethod || null,
lines: [],
text: attributionStatus === 'ambiguous'
? 'Inventory movement is ambiguous and is not assigned to this quote.'
: 'No settled inventory delta is linked to this quote.',
? `Inventory movement is ambiguous and is not assigned to this ${subject}.`
: `No settled inventory delta is linked to this ${subject}.`,
};
}

View file

@ -169,3 +169,74 @@ export function assertTradeResult(event) {
if (payload.result_code != null) requireString(payload.result_code, 'payload.result_code');
return event;
}
export function assertIntentRequestPreflightEvent(event) {
assertEventEnvelope(event);
if (event.event_type !== 'intent_request_preflight') {
throw new Error(`Unexpected event_type: ${event.event_type}`);
}
const payload = event.payload;
requireString(payload.request_id, 'payload.request_id');
requireString(payload.idempotency_key, 'payload.idempotency_key');
requireString(payload.state, 'payload.state');
requireOneOf(payload.state, 'payload.state', ['draft', 'blocked']);
requireString(payload.reason_code, 'payload.reason_code');
requireString(payload.source_asset_id, 'payload.source_asset_id');
requireString(payload.destination_asset_id, 'payload.destination_asset_id');
requireString(payload.source_amount_units, 'payload.source_amount_units');
requireString(payload.min_destination_amount_units, 'payload.min_destination_amount_units');
requireNumber(payload.slippage_bps, 'payload.slippage_bps');
requireString(payload.signer_account_id, 'payload.signer_account_id');
requireString(payload.verifier_contract, 'payload.verifier_contract');
requireString(payload.created_at, 'payload.created_at');
requireObject(payload.lifecycle, 'payload.lifecycle');
return event;
}
export function assertIntentRequestSubmissionResultEvent(event) {
assertEventEnvelope(event);
if (event.event_type !== 'intent_request_submission_result') {
throw new Error(`Unexpected event_type: ${event.event_type}`);
}
const payload = event.payload;
requireString(payload.request_id, 'payload.request_id');
requireString(payload.idempotency_key, 'payload.idempotency_key');
requireString(payload.submission_id, 'payload.submission_id');
requireString(payload.status, 'payload.status');
requireOneOf(payload.status, 'payload.status', [
'submit_requested',
'accepted_by_relay',
'failed',
'blocked',
]);
requireString(payload.result_code, 'payload.result_code');
requireString(payload.submitted_at, 'payload.submitted_at');
requireObject(payload.lifecycle, 'payload.lifecycle');
return event;
}
export function assertIntentRequestOutcomeEvent(event) {
assertEventEnvelope(event);
if (event.event_type !== 'intent_request_outcome') {
throw new Error(`Unexpected event_type: ${event.event_type}`);
}
const payload = event.payload;
requireString(payload.request_id, 'payload.request_id');
requireString(payload.idempotency_key, 'payload.idempotency_key');
requireString(payload.outcome_status, 'payload.outcome_status');
requireOneOf(payload.outcome_status, 'payload.outcome_status', [
'draft',
'blocked',
'failed',
'awaiting_settlement',
'not_filled',
'completed',
]);
requireString(payload.outcome_source, 'payload.outcome_source');
requireString(payload.outcome_reason, 'payload.outcome_reason');
requireString(payload.attribution_status, 'payload.attribution_status');
return event;
}

View file

@ -69,6 +69,16 @@ const DEFAULTS = {
strategyInventoryMaxAgeMs: 30_000,
executorInitialArmed: false,
executorResponseTimeoutMs: 10_000,
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,
withdrawalsFrozen: true,
btcFundingObserverEnabled: true,
btcFundingObserverBaseUrl: 'https://mempool.space/api',
@ -417,6 +427,46 @@ export function loadConfig({ envPath = '.env' } = {}) {
process.env.EXECUTOR_RESPONSE_TIMEOUT_MS,
DEFAULTS.executorResponseTimeoutMs,
),
intentRequestDefaultAmountEure: parseNumber(
process.env.INTENT_REQUEST_DEFAULT_AMOUNT_EURE,
DEFAULTS.intentRequestDefaultAmountEure,
),
intentRequestMaxAmountEure: parseNumber(
process.env.INTENT_REQUEST_MAX_AMOUNT_EURE,
DEFAULTS.intentRequestMaxAmountEure,
),
intentRequestDefaultSlippageBps: parseNumber(
process.env.INTENT_REQUEST_DEFAULT_SLIPPAGE_BPS,
DEFAULTS.intentRequestDefaultSlippageBps,
),
intentRequestMaxSlippageBps: parseNumber(
process.env.INTENT_REQUEST_MAX_SLIPPAGE_BPS,
DEFAULTS.intentRequestMaxSlippageBps,
),
intentRequestMinDeadlineMs: parseNumber(
process.env.INTENT_REQUEST_MIN_DEADLINE_MS,
DEFAULTS.intentRequestMinDeadlineMs,
),
intentRequestQuoteTimeoutMs: parseNumber(
process.env.INTENT_REQUEST_QUOTE_TIMEOUT_MS,
DEFAULTS.intentRequestQuoteTimeoutMs,
),
intentRequestPublishTimeoutMs: parseNumber(
process.env.INTENT_REQUEST_PUBLISH_TIMEOUT_MS,
DEFAULTS.intentRequestPublishTimeoutMs,
),
intentRequestStatusTimeoutMs: parseNumber(
process.env.INTENT_REQUEST_STATUS_TIMEOUT_MS,
DEFAULTS.intentRequestStatusTimeoutMs,
),
intentRequestInventoryMaxAgeMs: parseNumber(
process.env.INTENT_REQUEST_INVENTORY_MAX_AGE_MS,
DEFAULTS.intentRequestInventoryMaxAgeMs,
),
intentRequestPriceMaxAgeMs: parseNumber(
process.env.INTENT_REQUEST_PRICE_MAX_AGE_MS,
DEFAULTS.intentRequestPriceMaxAgeMs,
),
withdrawalsFrozen: parseBoolean(
process.env.LIQUIDITY_WITHDRAWALS_FROZEN,
DEFAULTS.withdrawalsFrozen,

View file

@ -1,5 +1,6 @@
import { Pool } from 'pg';
import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs';
import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
const TABLES = [
@ -13,10 +14,13 @@ const TABLES = [
'trade_decisions',
'execute_trade_commands',
'trade_execution_results',
'intent_request_preflights',
'intent_request_submission_results',
];
const PORTFOLIO_METRICS_TABLE = 'portfolio_metrics_snapshots';
const QUOTE_OUTCOMES_TABLE = 'quote_outcome_attributions';
const INTENT_REQUEST_OUTCOMES_TABLE = 'intent_request_outcomes';
const CREDITED_LIQUIDITY_STATUSES = ['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED'];
const COMPLETED_WITHDRAWAL_STATUSES = ['COMPLETED', 'FINALIZED', 'SETTLED'];
@ -140,8 +144,59 @@ export async function ensureHistorySchema(pool) {
CREATE INDEX IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE}_outcome_status_idx
ON ${QUOTE_OUTCOMES_TABLE} (outcome_status)
`);
await ensureExpressionIndex(pool, {
name: 'intent_request_preflights_request_id_idx',
table: 'intent_request_preflights',
expression: "(payload->>'request_id')",
});
await ensureExpressionIndex(pool, {
name: 'intent_request_preflights_idempotency_key_idx',
table: 'intent_request_preflights',
expression: "(payload->>'idempotency_key')",
});
await ensureExpressionIndex(pool, {
name: 'intent_request_submission_results_request_id_idx',
table: 'intent_request_submission_results',
expression: "(payload->>'request_id')",
});
await ensureExpressionIndex(pool, {
name: 'intent_request_submission_results_idempotency_key_idx',
table: 'intent_request_submission_results',
expression: "(payload->>'idempotency_key')",
});
await pool.query(`
CREATE TABLE IF NOT EXISTS ${INTENT_REQUEST_OUTCOMES_TABLE} (
request_id TEXT PRIMARY KEY,
idempotency_key TEXT NOT NULL,
submission_id TEXT,
intent_hash TEXT,
submission_status TEXT,
relay_status TEXT,
submitted_at TIMESTAMPTZ,
outcome_status TEXT NOT NULL,
outcome_observed_at TIMESTAMPTZ,
outcome_source TEXT NOT NULL,
outcome_reason TEXT NOT NULL,
attribution_status TEXT NOT NULL,
attribution_method TEXT,
attributed_inventory_delta JSONB,
computed_at TIMESTAMPTZ NOT NULL,
payload JSONB NOT NULL
)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS ${INTENT_REQUEST_OUTCOMES_TABLE}_outcome_observed_at_idx
ON ${INTENT_REQUEST_OUTCOMES_TABLE} (outcome_observed_at DESC)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS ${INTENT_REQUEST_OUTCOMES_TABLE}_outcome_status_idx
ON ${INTENT_REQUEST_OUTCOMES_TABLE} (outcome_status)
`);
}
export async function insertHistoryEvent(pool, { table, topic, event, record }) {
await pool.query(
`
@ -461,6 +516,254 @@ export async function loadRecentQuoteOutcomes(pool, { limit = 200 } = {}) {
return result.rows.map(normalizeQuoteOutcomeRow);
}
export async function loadIntentRequestPreflightByIdOrKey(pool, {
requestId = null,
idempotencyKey = null,
} = {}) {
const result = await pool.query(
`
SELECT observed_at, ingested_at, payload
FROM intent_request_preflights
WHERE ($1::text IS NOT NULL AND payload->>'request_id' = $1)
OR ($2::text IS NOT NULL AND payload->>'idempotency_key' = $2)
ORDER BY
COALESCE(observed_at, ingested_at) DESC,
CASE payload->>'status'
WHEN 'accepted_by_relay' THEN 0
WHEN 'failed' THEN 1
WHEN 'blocked' THEN 2
WHEN 'submit_requested' THEN 3
ELSE 4
END
LIMIT 1
`,
[requestId, idempotencyKey],
);
return normalizeEventPayloadRow(result.rows[0])?.payload || null;
}
export async function loadLatestIntentRequestSubmission(pool, { requestId } = {}) {
if (!requestId) return null;
const result = await pool.query(
`
SELECT observed_at, ingested_at, payload
FROM intent_request_submission_results
WHERE payload->>'request_id' = $1
ORDER BY
COALESCE(observed_at, ingested_at) DESC,
CASE payload->>'status'
WHEN 'accepted_by_relay' THEN 0
WHEN 'failed' THEN 1
WHEN 'blocked' THEN 2
WHEN 'submit_requested' THEN 3
ELSE 4
END
LIMIT 1
`,
[requestId],
);
return normalizeEventPayloadRow(result.rows[0])?.payload || null;
}
export async function refreshIntentRequestOutcomes(pool, {
btcAsset = null,
eureAsset = null,
now = Date.now(),
} = {}) {
if (!btcAsset?.assetId || !eureAsset?.assetId) return [];
const [preflightResult, submissionResult, inventoryResult] = await Promise.all([
pool.query(`
SELECT DISTINCT ON (payload->>'request_id')
event_id, observed_at, ingested_at, payload
FROM intent_request_preflights
WHERE COALESCE(payload->>'request_id', '') <> ''
ORDER BY payload->>'request_id', COALESCE(observed_at, ingested_at) DESC
`),
pool.query(`
SELECT event_id, observed_at, ingested_at, payload
FROM intent_request_submission_results
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
pool.query(`
SELECT event_id, observed_at, ingested_at, payload
FROM intent_inventory_snapshots
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
]);
const records = deriveIntentRequestOutcomeRecords({
preflights: preflightResult.rows,
submissions: submissionResult.rows,
inventorySnapshots: inventoryResult.rows,
btcAsset,
eureAsset,
now,
});
if (!records.length) return [];
const computedAt = new Date(
typeof now === 'number' ? now : Date.parse(now),
).toISOString();
for (const record of records) {
await upsertIntentRequestOutcome(pool, {
...record,
computedAt,
});
}
return records;
}
export async function upsertIntentRequestOutcome(pool, {
request_id,
idempotency_key,
submission_id = null,
intent_hash = null,
submission_status = null,
relay_status = null,
submitted_at = null,
outcome_status,
outcome_observed_at = null,
outcome_source,
outcome_reason,
attribution_status,
attribution_method = null,
attributed_inventory_delta = null,
computedAt,
payload,
}) {
await pool.query(
`
INSERT INTO ${INTENT_REQUEST_OUTCOMES_TABLE} (
request_id,
idempotency_key,
submission_id,
intent_hash,
submission_status,
relay_status,
submitted_at,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
computed_at,
payload
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb,$15,$16::jsonb
)
ON CONFLICT (request_id) DO UPDATE SET
idempotency_key = EXCLUDED.idempotency_key,
submission_id = EXCLUDED.submission_id,
intent_hash = EXCLUDED.intent_hash,
submission_status = EXCLUDED.submission_status,
relay_status = EXCLUDED.relay_status,
submitted_at = EXCLUDED.submitted_at,
outcome_status = EXCLUDED.outcome_status,
outcome_observed_at = EXCLUDED.outcome_observed_at,
outcome_source = EXCLUDED.outcome_source,
outcome_reason = EXCLUDED.outcome_reason,
attribution_status = EXCLUDED.attribution_status,
attribution_method = EXCLUDED.attribution_method,
attributed_inventory_delta = EXCLUDED.attributed_inventory_delta,
computed_at = EXCLUDED.computed_at,
payload = EXCLUDED.payload
`,
[
request_id,
idempotency_key,
submission_id,
intent_hash,
submission_status,
relay_status,
submitted_at,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta ? JSON.stringify(attributed_inventory_delta) : null,
computedAt,
JSON.stringify(payload || {}),
],
);
}
export async function loadRecentIntentRequests(pool, {
limit = 20,
btcAsset = null,
eureAsset = null,
now = Date.now(),
} = {}) {
if (btcAsset?.assetId && eureAsset?.assetId) {
await refreshIntentRequestOutcomes(pool, { btcAsset, eureAsset, now }).catch(() => []);
}
const result = await pool.query(
`
WITH latest_preflights AS (
SELECT DISTINCT ON (payload->>'request_id')
observed_at AS preflight_observed_at,
ingested_at AS preflight_ingested_at,
payload AS preflight_payload
FROM intent_request_preflights
WHERE COALESCE(payload->>'request_id', '') <> ''
ORDER BY payload->>'request_id', COALESCE(observed_at, ingested_at) DESC
), latest_submissions AS (
SELECT DISTINCT ON (payload->>'request_id')
observed_at AS submission_observed_at,
ingested_at AS submission_ingested_at,
payload AS submission_payload
FROM intent_request_submission_results
WHERE COALESCE(payload->>'request_id', '') <> ''
ORDER BY
payload->>'request_id',
COALESCE(observed_at, ingested_at) DESC,
CASE payload->>'status'
WHEN 'accepted_by_relay' THEN 0
WHEN 'failed' THEN 1
WHEN 'blocked' THEN 2
WHEN 'submit_requested' THEN 3
ELSE 4
END
)
SELECT
p.preflight_observed_at,
p.preflight_ingested_at,
p.preflight_payload,
s.submission_observed_at,
s.submission_ingested_at,
s.submission_payload,
o.outcome_observed_at,
o.computed_at AS outcome_computed_at,
o.payload AS outcome_payload
FROM latest_preflights p
LEFT JOIN latest_submissions s
ON s.submission_payload->>'request_id' = p.preflight_payload->>'request_id'
LEFT JOIN ${INTENT_REQUEST_OUTCOMES_TABLE} o
ON o.request_id = p.preflight_payload->>'request_id'
ORDER BY COALESCE(
o.outcome_observed_at,
s.submission_observed_at,
s.submission_ingested_at,
p.preflight_observed_at,
p.preflight_ingested_at
) DESC
LIMIT $1
`,
[Math.max(1, Number(limit) || 20)],
);
return result.rows.map(normalizeIntentRequestRow);
}
export async function loadLatestInventorySnapshot(pool) {
const latest = await loadLatestEventPayload(pool, 'intent_inventory_snapshots');
if (!latest) return null;
@ -899,6 +1202,118 @@ function normalizeQuoteOutcomeRow(row) {
};
}
function normalizeEventPayloadRow(row) {
if (!row) return null;
return {
observed_at: toIsoTimestamp(row.observed_at),
ingested_at: toIsoTimestamp(row.ingested_at),
payload: row.payload || {},
};
}
function normalizeIntentRequestRow(row) {
const preflight = row.preflight_payload || {};
const submission = row.submission_payload || null;
const outcome = row.outcome_payload || null;
const state = outcome?.outcome_status
|| mapSubmissionStatusToRequestState(submission?.status)
|| preflight.state
|| 'unknown';
const reasonCode = outcome?.outcome_reason
|| submission?.result_code
|| preflight.reason_code
|| 'reason_unknown';
const reasonText = submission?.result_text
|| preflight.reason_text
|| reasonCode.replaceAll('_', ' ');
return {
request_id: preflight.request_id || null,
idempotency_key: preflight.idempotency_key || null,
submission_id: submission?.submission_id || outcome?.submission_id || null,
intent_hash: submission?.intent_hash || outcome?.intent_hash || null,
quote_hash: submission?.quote_hash || preflight.selected_quote?.quote_hash || null,
created_at: toIsoTimestamp(preflight.created_at || row.preflight_observed_at || row.preflight_ingested_at),
submitted_at: toIsoTimestamp(submission?.submitted_at || outcome?.submitted_at || row.submission_observed_at || row.submission_ingested_at),
resolved_at: isTerminalIntentRequestState(state)
? toIsoTimestamp(outcome?.outcome_observed_at || row.outcome_observed_at || submission?.submitted_at)
: null,
state,
state_label: labelIntentRequestState(state),
reason_code: reasonCode,
reason_text: reasonText,
source_asset_id: preflight.source_asset_id || null,
source_symbol: preflight.source_symbol || null,
source_decimals: preflight.source_decimals ?? null,
destination_asset_id: preflight.destination_asset_id || null,
destination_symbol: preflight.destination_symbol || null,
destination_decimals: preflight.destination_decimals ?? null,
source_amount_units: preflight.source_amount_units || null,
expected_destination_amount_units: preflight.expected_destination_amount_units || null,
min_destination_amount_units: preflight.min_destination_amount_units || null,
quoted_destination_amount_units:
submission?.destination_amount_units
|| preflight.quoted_destination_amount_units
|| preflight.selected_quote?.amount_out
|| null,
slippage_bps: preflight.slippage_bps ?? null,
deadline_at: preflight.deadline_at || null,
signer_account_id: preflight.signer_account_id || null,
signer_public_key: preflight.signer_public_key || null,
verifier_contract: preflight.verifier_contract || null,
nonce: submission?.nonce || null,
nonce_policy: preflight.nonce_policy || null,
live_submit_capable: preflight.live_submit_capable === true && !submission,
solver_quote_count: preflight.solver_quote_count || 0,
selected_quote: preflight.selected_quote || null,
submission_status: submission?.status || outcome?.submission_status || null,
relay_status: submission?.relay_status || outcome?.evidence?.relay_status || null,
relay_response: submission?.relay_response || null,
relay_status_response: submission?.relay_status_response || null,
outcome_status: outcome?.outcome_status || null,
outcome_source: outcome?.outcome_source || null,
attribution_status: outcome?.attribution_status || null,
attribution_method: outcome?.attribution_method || null,
attributed_inventory_delta: outcome?.attributed_inventory_delta || null,
has_settlement_evidence: Boolean(
outcome?.attributed_inventory_delta
&& ['heuristic_match', 'linked_settlement'].includes(outcome?.attribution_status),
),
lifecycle: {
preflight,
submission,
outcome,
},
};
}
function mapSubmissionStatusToRequestState(status) {
if (status === 'accepted_by_relay') return 'awaiting_settlement';
if (status === 'submit_requested') return 'submitted';
if (status === 'blocked') return 'blocked';
if (status === 'failed') return 'failed';
return null;
}
function labelIntentRequestState(state) {
const labels = {
draft: 'Draft',
blocked: 'Blocked',
submitted: 'Submitted',
accepted_by_relay: 'Accepted by relay',
awaiting_settlement: 'Awaiting settlement',
failed: 'Failed',
not_filled: 'Not filled',
completed: 'Completed',
};
return labels[state] || state || 'Unknown';
}
function isTerminalIntentRequestState(state) {
return ['blocked', 'failed', 'not_filled', 'completed'].includes(state);
}
function normalizeRecentQuoteRow(row) {
const payload = row.payload || {};
return {

View file

@ -51,6 +51,7 @@ export default function App() {
if (reload) {
await loadBootstrap(1);
}
return response.result;
} catch (error) {
dispatch({ type: 'error.changed', error: error.message });
}

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import EmptyState from '../components/EmptyState.jsx';
import MetricCard from '../components/MetricCard.jsx';
@ -252,6 +252,245 @@ function WithdrawalEstimateForm({ balances, withdrawalDefaults, onControl }) {
);
}
async function copyIdentifier(value) {
if (!value || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) return;
try {
await navigator.clipboard.writeText(value);
} catch {
// Full ids remain visible when clipboard access is unavailable.
}
}
function IdentifierLine({ label, value }) {
if (!value) return <div className="status-subtle">{`${label}: unavailable`}</div>;
return (
<div className="trace-row">
<span className="status-subtle">{`${label}:`}</span>
<span className="mono trace-id">{value}</span>
<button className="button secondary trace-copy-button" onClick={() => copyIdentifier(value)} type="button">
Copy
</button>
</div>
);
}
function IntentRequestForm({ summary, onControl }) {
const defaults = summary?.defaults || {};
const [form, setForm] = useState({
amount_eure: defaults.amount_eure || '5',
slippage_bps: String(defaults.slippage_bps ?? 200),
});
useEffect(() => {
setForm({
amount_eure: defaults.amount_eure || '5',
slippage_bps: String(defaults.slippage_bps ?? 200),
});
}, [defaults.amount_eure, defaults.slippage_bps]);
async function handlePreflight(event) {
event.preventDefault();
await onControl('trade-executor', 'intent-request-preflight', {
amount_eure: form.amount_eure,
slippage_bps: Number(form.slippage_bps),
});
}
return (
<form onSubmit={handlePreflight}>
<div className="form-grid">
<div className="field">
<label htmlFor="intent-request-amount">Spend EURe</label>
<input
id="intent-request-amount"
max={defaults.max_amount_eure || '5'}
min="0.01"
name="amount_eure"
onChange={(event) => setForm((current) => ({ ...current, amount_eure: event.target.value }))}
step="0.01"
type="number"
value={form.amount_eure}
/>
<div className="status-subtle">{`Max ${defaults.max_amount_eure || '5'} EURe per live test request`}</div>
</div>
<div className="field">
<label htmlFor="intent-request-slippage">Max slippage bps</label>
<input
id="intent-request-slippage"
max={defaults.max_slippage_bps ?? 200}
min="0"
name="slippage_bps"
onChange={(event) => setForm((current) => ({ ...current, slippage_bps: event.target.value }))}
step="1"
type="number"
value={form.slippage_bps}
/>
<div className="status-subtle">{`Max ${defaults.max_slippage_bps ?? 200} bps / 2%`}</div>
</div>
</div>
<div className="button-row">
<button className="button" type="submit">Preflight EURe to BTC</button>
<button
className="button secondary"
onClick={() => onControl('trade-executor', 'intent-request-refresh-outcomes')}
type="button"
>
Refresh Request Outcomes
</button>
</div>
<div className="panel-subtitle">Preflight asks solvers for a quote. It does not submit live funds.</div>
</form>
);
}
function IntentRequestLifecycle({ item }) {
return (
<div className="lifecycle-detail-panel">
<div className="lifecycle-stage-grid">
<div className="lifecycle-stage-card">
<div className="stage-title">1. Request preflight</div>
<div className="stage-meta">{formatTimestamp(item.created_at)}</div>
<div className="stage-status">{item.lifecycle?.preflight?.state || item.state}</div>
<div className="stage-body">
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code}</div>
</div>
</div>
<div className="lifecycle-stage-card">
<div className="stage-title">2. Solver quote</div>
<div className="stage-meta">{formatTimestamp(item.deadline_at)}</div>
<div className="stage-status">{`${item.solver_quote_count || 0} quote(s)`}</div>
<div className="stage-body">
<IdentifierLine label="Quote hash" value={item.quote_hash} />
<div className="status-subtle">{item.quoted_destination_amount ? `Quoted ${item.quoted_destination_amount} BTC` : 'No usable quote stored'}</div>
</div>
</div>
<div className="lifecycle-stage-card">
<div className="stage-title">3. Relay submission</div>
<div className="stage-meta">{formatTimestamp(item.submitted_at)}</div>
<div className="stage-status">{item.submission_status || 'Not submitted'}</div>
<div className="stage-body">
<IdentifierLine label="Submission" value={item.submission_id} />
<IdentifierLine label="Intent hash" value={item.intent_hash} />
<div className="status-subtle">Relay acceptance is not a completed trade.</div>
</div>
</div>
<div className="lifecycle-stage-card">
<div className="stage-title">4. Settlement truth</div>
<div className="stage-meta">{formatTimestamp(item.resolved_at)}</div>
<div className="stage-status">{item.outcome_status || item.state}</div>
<div className="stage-body">
<div>{item.settlement_summary?.text || 'No settled inventory delta is linked to this request.'}</div>
{item.settlement_summary?.caveat ? <div className="status-subtle">{item.settlement_summary.caveat}</div> : null}
</div>
</div>
</div>
<div className="trace-block">
<IdentifierLine label="Request" value={item.request_id} />
<IdentifierLine label="Idempotency" value={item.idempotency_key} />
<IdentifierLine label="Intent" value={item.intent_hash} />
<IdentifierLine label="Quote hash" value={item.quote_hash} />
</div>
<details className="raw-details">
<summary>Raw lifecycle evidence</summary>
<pre>{stringifyJson(item.lifecycle)}</pre>
</details>
</div>
);
}
function IntentRequestsTable({ items, executorArmed, onControl }) {
const [expanded, setExpanded] = useState(() => new Set());
if (!items?.length) return <EmptyState>No repo-created EURe-to-BTC requests are stored yet.</EmptyState>;
function toggle(rowKey) {
setExpanded((current) => {
const next = new Set(current);
if (next.has(rowKey)) next.delete(rowKey);
else next.add(rowKey);
return next;
});
}
return (
<TableFrame>
<table className="intent-requests-table">
<thead>
<tr>
<th>Time</th>
<th>Request id</th>
<th>Spend / min receive</th>
<th>Quote / intent</th>
<th>State</th>
<th>Reason</th>
<th>Settlement</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const rowKey = item.request_id || item.idempotency_key || String(index);
const isExpanded = expanded.has(rowKey);
const canSubmit = item.live_submit_capable && executorArmed === true;
return (
<Fragment key={rowKey}>
<tr>
<td>{formatTimestamp(item.resolved_at || item.submitted_at || item.created_at)}</td>
<td><IdentifierLine label="Request" value={item.request_id} /></td>
<td>
<div className="mono">{`${item.source_amount} ${item.source_symbol || 'EURe'}`}</div>
<div className="status-subtle">{`Min ${item.min_destination_amount} ${item.destination_symbol || 'BTC'}`}</div>
<div className="status-subtle">{`${item.slippage_bps ?? 'n/a'} bps slippage`}</div>
</td>
<td>
<IdentifierLine label="Quote" value={item.quote_hash} />
<IdentifierLine label="Intent" value={item.intent_hash} />
</td>
<td><Pill label={item.state_label || item.state} stateLabel={item.state} /></td>
<td>
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code}</div>
</td>
<td>
<div>{item.settlement_summary?.text || 'No settled inventory delta linked'}</div>
<div className="status-subtle">{item.has_settlement_evidence ? 'Settlement evidence present' : 'Not completed'}</div>
</td>
<td>
<div className="button-row compact">
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
</button>
{item.live_submit_capable ? (
<button
className="button"
disabled={!canSubmit}
onClick={() => onControl('trade-executor', 'intent-request-submit', { request_id: item.request_id })}
type="button"
>
Submit live
</button>
) : null}
</div>
{item.live_submit_capable && executorArmed !== true ? (
<div className="status-subtle">Executor must be armed before live submit.</div>
) : null}
</td>
</tr>
{isExpanded ? (
<tr className="lifecycle-expanded-row">
<td colSpan={8}><IntentRequestLifecycle item={item} /></td>
</tr>
) : null}
</Fragment>
);
})}
</tbody>
</table>
</TableFrame>
);
}
function LastControlResult({ result }) {
if (!result) return null;
@ -425,6 +664,26 @@ export default function FundsPage({
</div>
</section>
<section className="panel full-width-evidence-panel">
<div className="panel-head">
<div>
<div className="eyebrow">Own requests</div>
<h3>EURe to BTC request creation</h3>
<div className="panel-subtitle">Create a solver quote request first, then submit only a drafted request. Completed requires inventory movement, not relay acceptance.</div>
</div>
<div className="pills">
<Pill label={`Executor ${funds.intent_requests?.executor_armed ? 'Armed' : 'Disarmed'}`} stateLabel={funds.intent_requests?.executor_armed ? 'healthy' : 'warning'} />
</div>
</div>
<IntentRequestForm onControl={onControl} summary={funds.intent_requests} />
<div className="panel-subtitle">{funds.intent_requests?.caveat}</div>
<IntentRequestsTable
executorArmed={funds.intent_requests?.executor_armed}
items={funds.intent_requests?.items || []}
onControl={onControl}
/>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">

View file

@ -54,6 +54,60 @@ export function buildQuoteResponseSubmission({
};
}
export function buildIntentRequestSubmission({
request,
signerAccountId,
signer,
verifierContract,
currentSaltHex,
nonce = null,
now = Date.now(),
}) {
const selectedQuote = request.selected_quote || {};
const quoteHash = selectedQuote.quote_hash || request.quote_hash;
const destinationAmount = request.destination_amount_units
|| selectedQuote.amount_out
|| request.quoted_destination_amount_units;
if (!quoteHash) throw new Error('request quote_hash is required');
if (!request.source_asset_id || !request.destination_asset_id) throw new Error('request assets are required');
if (!request.source_amount_units || !destinationAmount) throw new Error('request amounts are required');
const deadline = request.deadline_at || new Date(now + Number(request.min_deadline_ms || 60_000)).toISOString();
const intentNonce = nonce || buildIntentNonce(currentSaltHex);
const payload = {
signer_id: signerAccountId,
verifying_contract: verifierContract,
deadline,
nonce: intentNonce,
intents: [
{
intent: 'token_diff',
diff: {
[request.source_asset_id]: `-${String(request.source_amount_units)}`,
[request.destination_asset_id]: String(destinationAmount),
},
},
],
};
const payloadString = JSON.stringify(payload);
const signed = signer.sign(Buffer.from(payloadString));
return {
quote_hashes: [String(quoteHash)],
signed_data: {
standard: 'raw_ed25519',
payload: payloadString,
public_key: signer.getPublicKey().toString(),
signature: encodeNearSignature(signed.signature),
},
signed_payload: payload,
destination_amount_units: String(destinationAmount),
nonce: intentNonce,
deadline,
};
}
function encodeNearSignature(signatureBytes) {
return `ed25519:${encodeBase58(signatureBytes)}`;
}

View file

@ -72,22 +72,45 @@ export function createVerifierClient({
};
}
export function createSolverRelayRpcClient({ rpcUrl }) {
export function createSolverRelayRpcClient({ rpcUrl, apiKey = '' }) {
let id = 1;
return {
async getStatus(intentHash) {
const response = await postJson(rpcUrl, {
async function request(method, params, { timeoutMs = 10_000 } = {}) {
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
const response = await postJson(
rpcUrl,
{
jsonrpc: '2.0',
id: id++,
method: 'get_status',
params: [{ intent_hash: intentHash }],
});
method,
params,
},
{
headers,
signal: AbortSignal.timeout(timeoutMs),
},
);
if (response.error) {
throw new Error(response.error.message || 'Solver Relay get_status failed');
}
if (response.error) {
throw new Error(response.error.message || `Solver Relay ${method} failed`);
}
return response.result;
return response.result;
}
return {
request,
async quote(params, options = {}) {
return request('quote', [params], options);
},
async publishIntent({ quoteHashes, signedData }, options = {}) {
return request('publish_intent', [{
quote_hashes: quoteHashes,
signed_data: signedData,
}], options);
},
async getStatus(intentHash, options = {}) {
return request('get_status', [{ intent_hash: intentHash }], options);
},
};
}

View file

@ -0,0 +1,161 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { deriveIntentRequestOutcomeRecords } from '../src/core/intent-request-outcomes.mjs';
const BTC = {
assetId: 'nep141:btc.omft.near',
symbol: 'BTC',
decimals: 8,
};
const EURE = {
assetId: 'nep141:eure.omft.near',
symbol: 'EURe',
decimals: 18,
};
function preflight(overrides = {}) {
return {
request_id: 'request-1',
idempotency_key: 'idem-1',
state: 'draft',
reason_code: 'quote_available',
source_asset_id: EURE.assetId,
destination_asset_id: BTC.assetId,
source_amount_units: '5000000000000000000',
min_destination_amount_units: '9800',
quoted_destination_amount_units: '10000',
selected_quote: {
quote_hash: 'quote-hash-1',
amount_out: '10000',
},
min_deadline_ms: 60000,
deadline_at: '2026-04-12T10:01:00.000Z',
created_at: '2026-04-12T10:00:00.000Z',
...overrides,
};
}
function submission(overrides = {}) {
return {
request_id: 'request-1',
idempotency_key: 'idem-1',
submission_id: 'submission-1',
status: 'accepted_by_relay',
result_code: 'publish_intent_accepted',
quote_hash: 'quote-hash-1',
intent_hash: 'intent-hash-1',
destination_amount_units: '10000',
submitted_at: '2026-04-12T10:00:10.000Z',
relay_status: 'PENDING',
...overrides,
};
}
function outcomes({ preflights = [preflight()], submissions = [], inventorySnapshots = [], now = '2026-04-12T10:00:30.000Z' } = {}) {
return deriveIntentRequestOutcomeRecords({
preflights,
submissions,
inventorySnapshots,
btcAsset: BTC,
eureAsset: EURE,
now: Date.parse(now),
});
}
test('request submitted or relay accepted does not become completed without inventory delta', () => {
const [record] = outcomes({
submissions: [submission({ relay_status: 'SETTLED' })],
inventorySnapshots: [
{
observed_at: '2026-04-12T10:00:00.000Z',
spendable: {
[EURE.assetId]: '5000000000000000000',
[BTC.assetId]: '0',
},
},
{
observed_at: '2026-04-12T10:00:20.000Z',
spendable: {
[EURE.assetId]: '5000000000000000000',
[BTC.assetId]: '0',
},
},
],
});
assert.equal(record.submission_status, 'accepted_by_relay');
assert.equal(record.relay_status, 'SETTLED');
assert.equal(record.outcome_status, 'awaiting_settlement');
assert.equal(record.outcome_reason, 'relay_settled_but_inventory_delta_missing');
assert.equal(record.attribution_status, 'unattributed');
assert.equal(record.attributed_inventory_delta, null);
assert.notEqual(record.outcome_status, 'completed');
});
test('completed request requires exact EURe decrease and BTC increase after submission', () => {
const [record] = outcomes({
submissions: [submission()],
inventorySnapshots: [
{
inventory_id: 'inventory-before',
observed_at: '2026-04-12T10:00:00.000Z',
spendable: {
[EURE.assetId]: '5000000000000000000',
[BTC.assetId]: '0',
},
},
{
inventory_id: 'inventory-after',
observed_at: '2026-04-12T10:00:15.000Z',
spendable: {
[EURE.assetId]: '0',
[BTC.assetId]: '10000',
},
},
],
});
assert.equal(record.outcome_status, 'completed');
assert.equal(record.outcome_source, 'intent_inventory_spendable_delta');
assert.equal(record.outcome_reason, 'matched_inventory_delta');
assert.equal(record.attribution_status, 'heuristic_match');
assert.equal(record.attributed_inventory_delta.inventory_id, 'inventory-after');
assert.deepEqual(record.attributed_inventory_delta.delta_units, {
[BTC.assetId]: '10000',
[EURE.assetId]: '-5000000000000000000',
});
});
test('accepted request becomes not filled only after deadline grace and fresh inventory evidence', () => {
const [record] = outcomes({
submissions: [submission()],
inventorySnapshots: [
{
observed_at: '2026-04-12T10:02:01.000Z',
spendable: {
[EURE.assetId]: '5000000000000000000',
[BTC.assetId]: '0',
},
},
],
now: '2026-04-12T10:02:01.000Z',
});
assert.equal(record.outcome_status, 'not_filled');
assert.equal(record.outcome_reason, 'deadline_elapsed_without_settlement');
assert.equal(record.attribution_status, 'unattributed');
assert.equal(record.attributed_inventory_delta, null);
});
test('blocked preflight remains blocked and is distinct from request rejection or completion', () => {
const [record] = outcomes({
preflights: [preflight({ state: 'blocked', reason_code: 'insufficient_spendable_eure' })],
});
assert.equal(record.outcome_status, 'blocked');
assert.equal(record.outcome_reason, 'insufficient_spendable_eure');
assert.equal(record.submission_status, null);
assert.notEqual(record.outcome_status, 'completed');
assert.notEqual(record.outcome_status, 'rejected');
});

View file

@ -0,0 +1,275 @@
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');
});

View file

@ -117,6 +117,14 @@ test('control routing only resolves the allowlisted safe dashboard actions', ()
service: 'trade-executor',
action: 'resume',
});
const requestPreflight = resolveDashboardControl({
service: 'trade-executor',
action: 'intent-request-preflight',
});
const requestSubmit = resolveDashboardControl({
service: 'trade-executor',
action: 'intent-request-submit',
});
const risky = resolveDashboardControl({
service: 'strategy-engine',
action: 'arm',
@ -128,6 +136,10 @@ test('control routing only resolves the allowlisted safe dashboard actions', ()
assert.equal(armExecutor?.label, 'Arm Executor');
assert.equal(resumeExecutor?.path, '/resume');
assert.equal(resumeExecutor?.label, 'Resume Executor Intake');
assert.equal(requestPreflight?.path, '/intent-request/preflight');
assert.equal(requestPreflight?.risk_class, 'safe');
assert.equal(requestSubmit?.path, '/intent-request/submit');
assert.equal(requestSubmit?.risk_class, 'live_funds');
assert.equal(risky, null);
});
@ -1190,3 +1202,81 @@ test('bootstrap lifecycle rows preserve quote terms, submitted terms, and gross
assert.equal(row.submitted_terms.asset_out_symbol, 'EURe');
assert.equal(row.gross_edge_value_eure, '1.5');
});
test('own request dashboard rows do not label relay accepted evidence as completed trade', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({
config,
auth: { authenticated: true, subject: 'local-operator', mode: 'stub', roles: ['operator'] },
portfolioMetric: null,
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [],
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
submissionSummary: { total: 0, last_submission_at: null },
fundingObservations: [],
recentDepositStatuses: [],
recentTradeDecisions: [],
recentExecuteTradeCommands: [],
recentExecutionResults: [],
recentQuoteOutcomes: [],
recentIntentRequests: [{
request_id: 'request-accepted-only',
idempotency_key: 'idem-accepted-only',
submission_id: 'submission-accepted-only',
intent_hash: 'intent-hash-accepted-only',
quote_hash: 'quote-hash-accepted-only',
created_at: '2026-04-12T10:00:00.000Z',
submitted_at: '2026-04-12T10:00:01.000Z',
state: 'awaiting_settlement',
state_label: 'Awaiting settlement',
reason_code: 'accepted_by_relay_without_settlement',
reason_text: 'Relay accepted the signed request. This is not settlement.',
source_asset_id: config.tradingEure.assetId,
source_symbol: 'EURe',
source_decimals: 18,
destination_asset_id: config.tradingBtc.assetId,
destination_symbol: 'BTC',
destination_decimals: 8,
source_amount_units: '5000000000000000000',
expected_destination_amount_units: '10000',
min_destination_amount_units: '9800',
quoted_destination_amount_units: '10000',
slippage_bps: 200,
submission_status: 'accepted_by_relay',
relay_status: 'PENDING',
outcome_status: 'awaiting_settlement',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
has_settlement_evidence: false,
live_submit_capable: false,
lifecycle: {
preflight: { state: 'draft' },
submission: { status: 'accepted_by_relay' },
outcome: { outcome_status: 'awaiting_settlement' },
},
}],
recentAlertTransitions: [],
serviceSnapshots: [{
service: 'trade-executor',
label: 'Trade Executor',
base_url: 'http://trade-executor',
reachable: true,
health: { ok: true },
state: { armed: true, paused: false },
}],
});
const row = bootstrap.funds.intent_requests.items[0];
assert.equal(row.state, 'awaiting_settlement');
assert.equal(row.state_label, 'Awaiting settlement');
assert.equal(row.submission_status, 'accepted_by_relay');
assert.equal(row.has_settlement_evidence, false);
assert.equal(row.settlement_summary.text, 'No settled inventory delta is linked to this request.');
assert.doesNotMatch(
[row.state_label, row.reason_text, row.settlement_summary.text].join(' '),
/successful trade|completed trade|asset delta/i,
);
});

View file

@ -3,7 +3,11 @@ import assert from 'node:assert/strict';
import { KeyPair } from 'near-api-js';
import { buildIntentNonce, buildQuoteResponseSubmission } from '../src/venues/near-intents/signing.mjs';
import {
buildIntentNonce,
buildIntentRequestSubmission,
buildQuoteResponseSubmission,
} from '../src/venues/near-intents/signing.mjs';
test('intent nonce uses verifier salt prefix and 32 byte base64 payload', () => {
const nonce = buildIntentNonce('252812b3');
@ -42,3 +46,49 @@ test('quote response signing builds token_diff payload for solver submission', (
assert.equal(payload.intents[0].diff['nep141:btc.omft.near'], '5000');
assert.equal(payload.intents[0].diff['nep141:eure.omft.near'], '-4900000000000000000');
});
test('request signing builds taker token_diff payload for EURe to BTC submission', () => {
const signer = {
getPublicKey() {
return { toString: () => 'ed25519:test-public-key' };
},
sign() {
return { signature: Uint8Array.from({ length: 64 }, () => 7) };
},
};
const submission = buildIntentRequestSubmission({
request: {
request_id: 'request-1',
source_asset_id: 'nep141:eure.omft.near',
destination_asset_id: 'nep141:btc.omft.near',
source_amount_units: '5000000000000000000',
selected_quote: {
quote_hash: 'quote-hash-1',
amount_out: '10000',
},
deadline_at: '2026-04-12T10:01:00.000Z',
},
signerAccountId: 'solver.near',
signer,
verifierContract: 'intents.near',
currentSaltHex: '252812b3',
nonce: 'fixed-nonce',
});
assert.deepEqual(submission.quote_hashes, ['quote-hash-1']);
assert.equal(submission.destination_amount_units, '10000');
assert.equal(submission.nonce, 'fixed-nonce');
assert.equal(submission.signed_data.standard, 'raw_ed25519');
assert.equal(submission.signed_data.public_key, 'ed25519:test-public-key');
assert.match(submission.signed_data.signature, /^ed25519:/);
const payload = JSON.parse(submission.signed_data.payload);
assert.equal(payload.signer_id, 'solver.near');
assert.equal(payload.verifying_contract, 'intents.near');
assert.equal(payload.deadline, '2026-04-12T10:01:00.000Z');
assert.equal(payload.nonce, 'fixed-nonce');
assert.equal(payload.intents[0].intent, 'token_diff');
assert.equal(payload.intents[0].diff['nep141:eure.omft.near'], '-5000000000000000000');
assert.equal(payload.intents[0].diff['nep141:btc.omft.near'], '10000');
});