Proof: npm test (217/217), npm run operator-dashboard:build, and focused intent/trading-config tests cover DB-null request amount and slippage limits. Assumptions: removing request amount and slippage caps is explicitly operator-approved for the current live-funds workflow; preflight remains side-effect-free and submit remains separate. Still fake: request settlement truth still depends on inventory-delta attribution instead of venue-native terminal fill events.
This commit is contained in:
parent
a73ea1c83f
commit
b45d12d37c
9 changed files with 186 additions and 49 deletions
|
|
@ -50,8 +50,10 @@ export function createIntentRequestController({
|
|||
);
|
||||
const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200);
|
||||
const minDeadlineMs = Number(body.min_deadline_ms || requestPair.minDeadlineMs || config.intentRequestMinDeadlineMs || 60_000);
|
||||
const maxAmountUnits = parseDecimalToUnits(
|
||||
String(requestPair.requestMaxNotional || config.intentRequestMaxAmountEure || 5),
|
||||
const maxAmountUnits = requestPair.requestMaxNotional == null
|
||||
? null
|
||||
: parseDecimalToUnits(
|
||||
String(requestPair.requestMaxNotional),
|
||||
sourceAsset.decimals,
|
||||
{ field: 'intent_request_max_notional' },
|
||||
);
|
||||
|
|
@ -80,22 +82,25 @@ export function createIntentRequestController({
|
|||
);
|
||||
}
|
||||
sourceAmountUnits = parseDecimalToUnits(amountEure, sourceAsset.decimals, { field: 'amount_eure' });
|
||||
if (BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
|
||||
if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
|
||||
blockedBeforeQuote = true;
|
||||
throw codedError(
|
||||
'amount_exceeds_request_limit',
|
||||
`Requested ${amountEure} ${sourceAsset.symbol} exceeds configured live request limit ${requestPair.requestMaxNotional || config.intentRequestMaxAmountEure || 5} ${sourceAsset.symbol}.`,
|
||||
`Requested ${amountEure} ${sourceAsset.symbol} exceeds configured live request limit ${requestPair.requestMaxNotional} ${sourceAsset.symbol}.`,
|
||||
);
|
||||
}
|
||||
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(requestPair.requestMaxSlippageBps ?? config.intentRequestMaxSlippageBps ?? 200)) {
|
||||
if (
|
||||
requestPair.requestMaxSlippageBps != null
|
||||
&& slippageBps > Number(requestPair.requestMaxSlippageBps)
|
||||
) {
|
||||
blockedBeforeQuote = true;
|
||||
throw codedError(
|
||||
'slippage_exceeds_request_limit',
|
||||
`Slippage ${slippageBps} bps exceeds configured limit ${requestPair.requestMaxSlippageBps ?? config.intentRequestMaxSlippageBps ?? 200} bps.`,
|
||||
`Slippage ${slippageBps} bps exceeds configured limit ${requestPair.requestMaxSlippageBps} bps.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -560,11 +565,9 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
|
|||
priceRoute: pair.priceRoute,
|
||||
requestDefaultNotional:
|
||||
strategyConfig.requestDefaultNotional || config.intentRequestDefaultAmountEure,
|
||||
requestMaxNotional:
|
||||
strategyConfig.requestMaxNotional || config.intentRequestMaxAmountEure,
|
||||
requestMaxNotional: strategyConfig.requestMaxNotional ?? null,
|
||||
slippageBps: strategyConfig.slippageBps ?? config.intentRequestDefaultSlippageBps,
|
||||
requestMaxSlippageBps:
|
||||
strategyConfig.requestMaxSlippageBps ?? strategyConfig.slippageBps ?? config.intentRequestMaxSlippageBps,
|
||||
requestMaxSlippageBps: strategyConfig.requestMaxSlippageBps ?? null,
|
||||
minDeadlineMs: strategyConfig.minDeadlineMs ?? config.intentRequestMinDeadlineMs,
|
||||
priceMaxAgeMs: strategyConfig.priceMaxAgeMs ?? config.intentRequestPriceMaxAgeMs,
|
||||
inventoryMaxAgeMs: strategyConfig.inventoryMaxAgeMs ?? config.intentRequestInventoryMaxAgeMs,
|
||||
|
|
|
|||
|
|
@ -900,14 +900,23 @@ function buildFundingSummary({ config, fundingObservations, recentDepositStatuse
|
|||
}
|
||||
|
||||
function buildIntentRequestSummary({ config, intentRequests = [], executorState = {} } = {}) {
|
||||
const defaultPair = config.defaultTakerPair || null;
|
||||
const strategyConfig = defaultPair?.strategyConfig || null;
|
||||
const usingDbPairConfig = Boolean(config.tradingConfigLoaded && strategyConfig);
|
||||
const sourceAsset = defaultPair?.assetIn || config.tradingEure;
|
||||
const destinationAsset = defaultPair?.assetOut || config.tradingBtc;
|
||||
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),
|
||||
source_symbol: sourceAsset?.symbol || 'Source',
|
||||
destination_symbol: destinationAsset?.symbol || 'Destination',
|
||||
amount_eure: String(strategyConfig?.requestDefaultNotional ?? config.intentRequestDefaultAmountEure ?? 5),
|
||||
max_amount_eure: usingDbPairConfig
|
||||
? strategyConfig.requestMaxNotional ?? null
|
||||
: String(config.intentRequestMaxAmountEure || 5),
|
||||
slippage_bps: Number(strategyConfig?.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200),
|
||||
max_slippage_bps: usingDbPairConfig
|
||||
? strategyConfig.requestMaxSlippageBps ?? null
|
||||
: Number(config.intentRequestMaxSlippageBps ?? 200),
|
||||
},
|
||||
executor_armed: executorState.armed ?? null,
|
||||
executor_paused: executorState.paused ?? null,
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ export const CURRENT_REVERSE_PAIR_KEY = pairKey(CURRENT_EURE_ASSET_ID, CURRENT_N
|
|||
export const CURRENT_EDGE_BPS = 49;
|
||||
export const CURRENT_STRATEGY_MAX_NOTIONAL = '150';
|
||||
export const CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE = '5';
|
||||
export const CURRENT_REQUEST_MAX_NOTIONAL_EURE = '5';
|
||||
export const CURRENT_SLIPPAGE_BPS = 200;
|
||||
export const CURRENT_REQUEST_MAX_NOTIONAL_EURE = null;
|
||||
export const CURRENT_REQUEST_MAX_SLIPPAGE_BPS = null;
|
||||
export const CURRENT_MIN_DEADLINE_MS = 60_000;
|
||||
export const CURRENT_PRICE_MAX_AGE_MS = 30_000;
|
||||
export const CURRENT_INVENTORY_MAX_AGE_MS = 30_000;
|
||||
|
|
@ -188,7 +189,7 @@ export function buildSeedStrategyConfig(pairId, {
|
|||
inventoryMaxAgeMs: CURRENT_INVENTORY_MAX_AGE_MS,
|
||||
requestDefaultNotional: CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE,
|
||||
requestMaxNotional: CURRENT_REQUEST_MAX_NOTIONAL_EURE,
|
||||
requestMaxSlippageBps: CURRENT_SLIPPAGE_BPS,
|
||||
requestMaxSlippageBps: CURRENT_REQUEST_MAX_SLIPPAGE_BPS,
|
||||
createdBy,
|
||||
reason,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -719,9 +719,9 @@ export async function createPairStrategyConfigVersion(pool, {
|
|||
minDeadlineMs = null,
|
||||
priceMaxAgeMs = null,
|
||||
inventoryMaxAgeMs = null,
|
||||
requestDefaultNotional = null,
|
||||
requestMaxNotional = null,
|
||||
requestMaxSlippageBps = null,
|
||||
requestDefaultNotional = undefined,
|
||||
requestMaxNotional = undefined,
|
||||
requestMaxSlippageBps = undefined,
|
||||
changedBy = 'operator',
|
||||
reason = 'operator config update',
|
||||
} = {}) {
|
||||
|
|
@ -763,17 +763,17 @@ export async function createPairStrategyConfigVersion(pool, {
|
|||
inventoryMaxAgeMs:
|
||||
inventoryMaxAgeMs == null ? Number(active.inventory_max_age_ms) : Number(inventoryMaxAgeMs),
|
||||
requestDefaultNotional:
|
||||
requestDefaultNotional == null
|
||||
requestDefaultNotional === undefined
|
||||
? active.request_default_notional == null ? null : String(active.request_default_notional)
|
||||
: String(requestDefaultNotional),
|
||||
: nullablePositiveNumberString(requestDefaultNotional, 'request_default_notional'),
|
||||
requestMaxNotional:
|
||||
requestMaxNotional == null
|
||||
requestMaxNotional === undefined
|
||||
? active.request_max_notional == null ? null : String(active.request_max_notional)
|
||||
: String(requestMaxNotional),
|
||||
: nullablePositiveNumberString(requestMaxNotional, 'request_max_notional'),
|
||||
requestMaxSlippageBps:
|
||||
requestMaxSlippageBps == null
|
||||
requestMaxSlippageBps === undefined
|
||||
? active.request_max_slippage_bps == null ? null : Number(active.request_max_slippage_bps)
|
||||
: Number(requestMaxSlippageBps),
|
||||
: nullableNonNegativeInteger(requestMaxSlippageBps, 'request_max_slippage_bps'),
|
||||
createdBy: changedBy,
|
||||
reason,
|
||||
};
|
||||
|
|
@ -874,9 +874,9 @@ export async function setTradingPairMode(pool, {
|
|||
minDeadlineMs = null,
|
||||
priceMaxAgeMs = null,
|
||||
inventoryMaxAgeMs = null,
|
||||
requestDefaultNotional = null,
|
||||
requestMaxNotional = null,
|
||||
requestMaxSlippageBps = null,
|
||||
requestDefaultNotional = undefined,
|
||||
requestMaxNotional = undefined,
|
||||
requestMaxSlippageBps = undefined,
|
||||
changedBy = 'operator',
|
||||
reason = 'operator pair mode update',
|
||||
} = {}) {
|
||||
|
|
@ -1336,9 +1336,9 @@ function buildInitialPairStrategyConfig(pairId, {
|
|||
minDeadlineMs = null,
|
||||
priceMaxAgeMs = null,
|
||||
inventoryMaxAgeMs = null,
|
||||
requestDefaultNotional = null,
|
||||
requestMaxNotional = null,
|
||||
requestMaxSlippageBps = null,
|
||||
requestDefaultNotional = undefined,
|
||||
requestMaxNotional = undefined,
|
||||
requestMaxSlippageBps = undefined,
|
||||
changedBy = 'operator',
|
||||
reason = 'operator pair strategy config initialization',
|
||||
} = {}) {
|
||||
|
|
@ -1396,11 +1396,6 @@ function nonNegativeIntegerOrDefault(value, fallback, field) {
|
|||
return next;
|
||||
}
|
||||
|
||||
function nullableNonNegativeIntegerOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return fallback == null ? null : nonNegativeIntegerOrDefault(fallback, 0, field);
|
||||
return nonNegativeIntegerOrDefault(value, 0, field);
|
||||
}
|
||||
|
||||
function positiveNumberStringOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return String(fallback);
|
||||
const next = String(value).trim();
|
||||
|
|
@ -1416,10 +1411,25 @@ function nonNegativeNumberStringOrDefault(value, fallback, field) {
|
|||
}
|
||||
|
||||
function nullablePositiveNumberStringOrDefault(value, fallback, field) {
|
||||
if (!hasConfigOverride(value)) return fallback == null ? null : positiveNumberStringOrDefault(fallback, '1', field);
|
||||
if (value === undefined) return fallback == null ? null : positiveNumberStringOrDefault(fallback, '1', field);
|
||||
return nullablePositiveNumberString(value, field);
|
||||
}
|
||||
|
||||
function nullablePositiveNumberString(value, field) {
|
||||
if (!hasConfigOverride(value)) return null;
|
||||
return positiveNumberStringOrDefault(value, '1', field);
|
||||
}
|
||||
|
||||
function nullableNonNegativeIntegerOrDefault(value, fallback, field) {
|
||||
if (value === undefined) return fallback == null ? null : nonNegativeIntegerOrDefault(fallback, 0, field);
|
||||
return nullableNonNegativeInteger(value, field);
|
||||
}
|
||||
|
||||
function nullableNonNegativeInteger(value, field) {
|
||||
if (!hasConfigOverride(value)) return null;
|
||||
return nonNegativeIntegerOrDefault(value, 0, field);
|
||||
}
|
||||
|
||||
function normalizeStrategyConfigRow(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -279,6 +279,10 @@ function IdentifierLine({ label, value }) {
|
|||
|
||||
function IntentRequestForm({ summary, onControl }) {
|
||||
const defaults = summary?.defaults || {};
|
||||
const sourceSymbol = defaults.source_symbol || 'EURe';
|
||||
const destinationSymbol = defaults.destination_symbol || 'BTC';
|
||||
const hasAmountCap = defaults.max_amount_eure != null && defaults.max_amount_eure !== '';
|
||||
const hasSlippageCap = defaults.max_slippage_bps != null && defaults.max_slippage_bps !== '';
|
||||
const [form, setForm] = useState({
|
||||
amount_eure: defaults.amount_eure || '5',
|
||||
slippage_bps: String(defaults.slippage_bps ?? 200),
|
||||
|
|
@ -303,36 +307,40 @@ function IntentRequestForm({ summary, onControl }) {
|
|||
<form onSubmit={handlePreflight}>
|
||||
<div className="form-grid">
|
||||
<div className="field">
|
||||
<label htmlFor="intent-request-amount">Spend EURe</label>
|
||||
<label htmlFor="intent-request-amount">{`Spend ${sourceSymbol}`}</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}
|
||||
{...(hasAmountCap ? { max: defaults.max_amount_eure } : {})}
|
||||
/>
|
||||
<div className="status-subtle">{`Max ${defaults.max_amount_eure || '5'} EURe per live test request`}</div>
|
||||
<div className="status-subtle">
|
||||
{hasAmountCap ? `Max ${defaults.max_amount_eure} ${sourceSymbol}` : 'No request amount cap'}
|
||||
</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}
|
||||
{...(hasSlippageCap ? { max: defaults.max_slippage_bps } : {})}
|
||||
/>
|
||||
<div className="status-subtle">{`Max ${defaults.max_slippage_bps ?? 200} bps / 2%`}</div>
|
||||
<div className="status-subtle">
|
||||
{hasSlippageCap ? `Max ${defaults.max_slippage_bps} bps` : 'No slippage cap'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="button-row">
|
||||
<button className="button" type="submit">Preflight EURe to BTC</button>
|
||||
<button className="button" type="submit">{`Preflight ${sourceSymbol} to ${destinationSymbol}`}</button>
|
||||
<button
|
||||
className="button secondary"
|
||||
onClick={() => onControl('trade-executor', 'intent-request-refresh-outcomes')}
|
||||
|
|
|
|||
|
|
@ -630,6 +630,16 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
|||
<td>{strategyConfig.edge_bps ?? 'Unavailable'} bps</td>
|
||||
<td>
|
||||
<div>{strategyConfig.max_notional || 'Unavailable'} max</div>
|
||||
<div className="status-subtle">
|
||||
{strategyConfig.request_max_notional == null
|
||||
? 'No request cap'
|
||||
: `${strategyConfig.request_max_notional} request max`}
|
||||
</div>
|
||||
<div className="status-subtle">
|
||||
{strategyConfig.request_max_slippage_bps == null
|
||||
? 'No slippage cap'
|
||||
: `${strategyConfig.request_max_slippage_bps} bps slippage max`}
|
||||
</div>
|
||||
<div className="status-subtle">{strategyConfig.price_max_age_ms || 'Unavailable'} ms price max age</div>
|
||||
</td>
|
||||
<td>{route.source || 'Unavailable'}</td>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,14 @@ function buildRelay() {
|
|||
};
|
||||
}
|
||||
|
||||
function buildController({ store = buildStore(), relay = buildRelay(), armed = true, verifierRegistered = true, withMakerSuppressed = async (operation) => operation() } = {}) {
|
||||
function buildController({
|
||||
store = buildStore(),
|
||||
relay = buildRelay(),
|
||||
armed = true,
|
||||
verifierRegistered = true,
|
||||
withMakerSuppressed = async (operation) => operation(),
|
||||
getTradingConfig = null,
|
||||
} = {}) {
|
||||
return {
|
||||
store,
|
||||
relay,
|
||||
|
|
@ -137,6 +144,7 @@ function buildController({ store = buildStore(), relay = buildRelay(), armed = t
|
|||
signer: KeyPair.fromRandom('ed25519'),
|
||||
isArmed: () => armed,
|
||||
isPaused: () => false,
|
||||
getTradingConfig,
|
||||
withMakerSuppressed,
|
||||
now: () => Date.parse('2026-04-12T10:00:00.000Z'),
|
||||
uuid: (() => {
|
||||
|
|
@ -196,6 +204,60 @@ test('preflight is side-effect-free and does not publish a live intent', async (
|
|||
assert.equal(relay.publishCalls, 0);
|
||||
});
|
||||
|
||||
test('DB null request limits allow operator-chosen amount and slippage', async () => {
|
||||
const store = buildStore({ inventoryUnits: '6000000000000000000' });
|
||||
const relay = buildRelay();
|
||||
relay.quote = async function quote() {
|
||||
this.quoteCalls += 1;
|
||||
return [{
|
||||
quote_hash: 'uncapped-quote-hash',
|
||||
amount_out: '12000',
|
||||
expiration_time: '2026-04-12T10:01:00.000Z',
|
||||
}];
|
||||
};
|
||||
const pairId = `${EURE.assetId}->${BTC.assetId}`;
|
||||
const pair = {
|
||||
key: pairId,
|
||||
pairId,
|
||||
takerEnabled: true,
|
||||
canTrade: true,
|
||||
assetIn: EURE,
|
||||
assetOut: BTC,
|
||||
strategyConfig: {
|
||||
requestDefaultNotional: '5',
|
||||
requestMaxNotional: null,
|
||||
slippageBps: 200,
|
||||
requestMaxSlippageBps: null,
|
||||
minDeadlineMs: 60_000,
|
||||
priceMaxAgeMs: 30_000,
|
||||
inventoryMaxAgeMs: 30_000,
|
||||
},
|
||||
priceRoute: {
|
||||
source: 'btc_eur_reference',
|
||||
},
|
||||
};
|
||||
const { controller } = buildController({
|
||||
store,
|
||||
relay,
|
||||
getTradingConfig: async () => ({
|
||||
ok: true,
|
||||
tradingEure: EURE,
|
||||
tradingBtc: BTC,
|
||||
pairByKey: new Map([[pairId, pair]]),
|
||||
defaultTakerPair: pair,
|
||||
}),
|
||||
});
|
||||
|
||||
const preflight = await controller.preflight({ amount_eure: '6', slippage_bps: 250 });
|
||||
|
||||
assert.equal(preflight.state, 'draft');
|
||||
assert.equal(preflight.reason_code, 'quote_available');
|
||||
assert.equal(preflight.request_max_notional, null);
|
||||
assert.equal(preflight.slippage_bps, 250);
|
||||
assert.equal(preflight.live_submit_capable, true);
|
||||
assert.equal(relay.quoteCalls, 1);
|
||||
});
|
||||
|
||||
test('insufficient spendable EURe blocks before solver quote or signing', async () => {
|
||||
const store = buildStore({ inventoryUnits: '0' });
|
||||
const relay = buildRelay();
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ test('operator dashboard exposes DB-backed pair activation and pause controls',
|
|||
assert.match(source, /control\.action === 'pause-pair'/);
|
||||
assert.match(source, /edgeBps: body\.edge_bps/);
|
||||
assert.match(source, /maxNotional: body\.max_notional/);
|
||||
assert.match(source, /requestMaxNotional: bodyField\(body, 'request_max_notional', 'requestMaxNotional'\)/);
|
||||
assert.match(source, /requestMaxSlippageBps: bodyField\(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'\)/);
|
||||
});
|
||||
|
||||
test('operator dashboard API auth failures are JSON for frontend fetches', () => {
|
||||
|
|
|
|||
|
|
@ -213,6 +213,38 @@ test('edge update creates a new active strategy version', async () => {
|
|||
assert.equal(versions.find((row) => row.version === 1).active, false);
|
||||
});
|
||||
|
||||
test('strategy config update can clear request amount and slippage caps', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await seedTradingConfig(pool);
|
||||
const pairId = `${CURRENT_EURE_ASSET_ID}->${CURRENT_NBTC_ASSET_ID}`;
|
||||
|
||||
await createPairStrategyConfigVersion(pool, {
|
||||
pairId,
|
||||
edgeBps: 49,
|
||||
maxNotional: '200',
|
||||
requestMaxNotional: '5',
|
||||
requestMaxSlippageBps: 200,
|
||||
changedBy: 'test',
|
||||
reason: 'operator capped request limits',
|
||||
});
|
||||
const next = await createPairStrategyConfigVersion(pool, {
|
||||
pairId,
|
||||
edgeBps: 49,
|
||||
maxNotional: '200',
|
||||
requestMaxNotional: null,
|
||||
requestMaxSlippageBps: null,
|
||||
changedBy: 'test',
|
||||
reason: 'operator uncapped request limits',
|
||||
});
|
||||
const snapshot = await loadTradingConfig(pool);
|
||||
const pair = snapshot.pairByKey.get(pairId);
|
||||
|
||||
assert.equal(next.requestMaxNotional, null);
|
||||
assert.equal(next.requestMaxSlippageBps, null);
|
||||
assert.equal(pair.strategyConfig.requestMaxNotional, null);
|
||||
assert.equal(pair.strategyConfig.requestMaxSlippageBps, null);
|
||||
});
|
||||
|
||||
test('strategy config update rejects invalid max notional', async () => {
|
||||
const pool = createMemoryPool();
|
||||
await seedTradingConfig(pool);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue