unrip/src/apps/trade-executor.mjs
philipp 41b9ec680b
Some checks failed
deploy / deploy (push) Failing after 1m35s
Implement funded NEAR Intents trade loop
Proof: first non-mocked tradeable loop for one pair using funded NEAR Intents inventory, Kafka, and PostgreSQL.

Assumptions: solver-side execution is performed by signed token_diff quote responses over the Solver Relay; EURe is treated as 1:1 with EUR; k3s runtime uses unrip-dev.near as the named signer account.

Still fake: signer key is not yet registered on intents.near, strategy and executor remain disarmed by default, and no live mainnet quote response has been submitted from this repo yet.
2026-04-02 10:01:15 +02:00

277 lines
7.9 KiB
JavaScript

import process from 'node:process';
import { createConsumer } from '../bus/kafka/consumer.mjs';
import { createProducer } from '../bus/kafka/producer.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 { createLogger, serializeError } from '../core/log.mjs';
import { assertExecuteTradeCommand, assertTradeResult } from '../core/schemas.mjs';
import { loadConfig } from '../lib/config.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';
const config = loadConfig();
const logger = createLogger({
service: 'trade-executor',
component: 'executor',
namespace: config.projectNamespace,
venue: 'near-intents',
});
if (!config.nearIntentsApiKey || !config.nearIntentsAccountId || !config.nearIntentsSignerPrivateKey) {
logger.error('missing_executor_config', {
details: {
required: [
'NEAR_INTENTS_API_KEY',
'NEAR_INTENTS_ACCOUNT_ID',
'NEAR_INTENTS_SIGNER_PRIVATE_KEY',
],
},
});
process.exit(1);
}
const consumer = await createConsumer({
groupId: config.kafkaConsumerGroupExecutor,
brokers: config.kafkaBrokers,
clientId: config.kafkaClientId,
logger,
});
const producer = await createProducer({
brokers: config.kafkaBrokers,
clientId: config.kafkaClientId,
logger,
});
const verifierClient = createVerifierClient({
nearRpcUrl: config.nearRpcUrl,
verifierContract: config.nearVerifierContract,
signerPrivateKey: config.nearIntentsSignerPrivateKey,
});
const signer = verifierClient.getSigner();
const relayClient = await startSolverRelayWs({
apiKey: config.nearIntentsApiKey,
wsUrl: config.nearIntentsWsUrl,
logger: logger.child({ component: 'solver-relay' }),
subscriptions: ['quote_status'],
onEvent(payload) {
state.last_quote_status = payload?.params || payload?.result || payload;
},
});
const stateStore = createExecutorStateStore({ stateDir: config.executorStateDir });
const state = {
armed: config.executorInitialArmed,
paused: false,
draining: false,
last_command: null,
last_request: null,
last_venue_response: null,
last_quote_status: null,
last_error: null,
in_flight_count: 0,
completed_count: 0,
};
await consumer.subscribe({ topic: config.kafkaTopicCmdExecuteTrade, fromBeginning: false });
await consumer.run({
eachMessage: async ({ message }) => {
if (!message.value) return;
try {
const event = parseEventMessage(message.value.toString());
assertExecuteTradeCommand(event);
await handleCommand(event);
} catch (error) {
state.last_error = serializeError(error);
logger.error('executor_message_failed', {
topic: config.kafkaTopicCmdExecuteTrade,
details: {
error: serializeError(error),
},
});
}
},
});
async function handleCommand(event) {
const payload = event.payload;
state.last_command = payload;
const existing = stateStore.get(payload.command_id);
if (existing?.status === 'completed') {
logger.warn('duplicate_command_skipped', {
topic: config.kafkaTopicCmdExecuteTrade,
pair: payload.pair,
details: {
command_id: payload.command_id,
},
});
return;
}
if (state.paused) return;
if (!state.armed) {
await publishResult(payload, {
status: 'rejected',
result_code: 'executor_disarmed',
note: 'executor is disarmed',
});
return;
}
stateStore.markProcessing(payload.command_id, {
quote_id: payload.quote_id,
idempotency_key: payload.idempotency_key,
execution_key: payload.execution_key,
});
state.in_flight_count += 1;
try {
const currentSaltHex = await verifierClient.currentSalt();
const submission = buildQuoteResponseSubmission({
command: payload,
signerAccountId: config.nearIntentsAccountId,
signer,
verifierContract: config.nearVerifierContract,
currentSaltHex,
});
state.last_request = submission;
const response = await relayClient.request('quote_response', [submission], {
timeoutMs: config.executorResponseTimeoutMs,
});
state.last_venue_response = response;
state.last_error = null;
await publishResult(payload, {
status: 'submitted',
result_code: response === 'OK' ? 'quote_response_ok' : 'quote_response_ack',
venue_response: response,
});
stateStore.markCompleted(payload.command_id, {
quote_id: payload.quote_id,
result: response,
});
state.completed_count += 1;
} catch (error) {
state.last_error = serializeError(error);
stateStore.markFailed(payload.command_id, {
quote_id: payload.quote_id,
error: serializeError(error),
});
await publishResult(payload, {
status: 'failed',
result_code: 'submission_failed',
error: serializeError(error),
});
} finally {
state.in_flight_count = Math.max(0, state.in_flight_count - 1);
if (state.draining && state.in_flight_count === 0) {
setTimeout(() => shutdown(), 0);
}
}
}
async function publishResult(command, extraPayload) {
const event = buildEventEnvelope({
source: 'trade-executor',
venue: 'near-intents',
eventType: 'trade_result',
payload: {
command_id: command.command_id,
decision_id: command.decision_id,
idempotency_key: command.idempotency_key,
execution_key: command.execution_key,
quote_id: command.quote_id,
pair: command.pair,
...extraPayload,
},
});
assertTradeResult(event);
await producer.sendJson(config.kafkaTopicExecTradeResult, event, { key: command.execution_key });
}
const controlApi = startControlApi({
host: config.tradeExecutorControlHost,
port: config.tradeExecutorControlPort,
logger: logger.child({ component: 'control-api' }),
service: 'trade-executor',
namespace: config.projectNamespace,
stateProvider: {
async getState() {
const signerRegistered = await verifierClient.isPublicKeyRegistered({
accountId: config.nearIntentsAccountId,
}).catch(() => null);
return {
account_id: config.nearIntentsAccountId,
signer_public_key: signer.getPublicKey().toString(),
signer_registered: signerRegistered,
...state,
durable_state: stateStore.getState(),
};
},
},
routes: [
{
method: 'POST',
path: '/arm',
handler: () => {
state.armed = true;
return { ok: true, armed: true };
},
},
{
method: 'POST',
path: '/disarm',
handler: () => {
state.armed = false;
return { ok: true, armed: false };
},
},
{
method: 'POST',
path: '/pause',
handler: () => {
state.paused = true;
consumer.pause([{ topic: config.kafkaTopicCmdExecuteTrade }]);
return { ok: true, paused: true };
},
},
{
method: 'POST',
path: '/resume',
handler: () => {
state.paused = false;
consumer.resume([{ topic: config.kafkaTopicCmdExecuteTrade }]);
return { ok: true, paused: false };
},
},
{
method: 'POST',
path: '/drain',
handler: () => {
state.draining = true;
state.paused = true;
consumer.pause([{ topic: config.kafkaTopicCmdExecuteTrade }]);
if (state.in_flight_count === 0) {
setTimeout(() => shutdown(), 0);
}
return { ok: true, draining: true };
},
},
],
});
async function shutdown() {
await controlApi.close().catch(() => {});
relayClient.close();
await consumer.disconnect();
await producer.disconnect();
process.exit(0);
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);