All checks were successful
deploy / deploy (push) Successful in 56s
Proof: Targeted notification and outcome refresh tests pass; full npm test passes with 284 tests; operator dashboard bundle builds. Assumptions: A completed settlement can be computed after its inventory movement timestamp, and notification_deliveries remains the canonical once-only de-dup guard for ntfy delivery. Still fake: Venue-native terminal fill ids and fee-complete realized PnL remain unavailable; ntfy is still an external utility service outside the unrip app deployment ownership.
241 lines
10 KiB
JavaScript
241 lines
10 KiB
JavaScript
import { formatUnitsDecimal } from './intent-requests.mjs';
|
|
|
|
const CREDITED_DEPOSIT_STATUSES = new Set(['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED']);
|
|
const COMPLETED_WITHDRAWAL_STATUSES = new Set(['COMPLETED', 'FINALIZED', 'SETTLED']);
|
|
const SETTLED_ATTRIBUTION_STATUSES = new Set(['heuristic_match', 'linked_settlement']);
|
|
|
|
export function buildLiquidityActionNotification({ event, config } = {}) {
|
|
const payload = event?.payload || event || {};
|
|
const details = payload.details || {};
|
|
const status = normalizeStatus(payload.status || details.status);
|
|
|
|
if (payload.action_type === 'deposit_status_observed' && CREDITED_DEPOSIT_STATUSES.has(status)) {
|
|
if (!details.tx_hash) return null;
|
|
const asset = assetFor(config, payload.asset_id || details.asset_id);
|
|
const amount = formatAssetUnits(details.amount || '0', asset);
|
|
const sourceId = `${payload.chain || details.chain || 'unknown'}:${payload.asset_id || details.asset_id || 'unknown'}:${details.tx_hash}:${details.amount || '0'}`;
|
|
return {
|
|
notification_key: `deposit_credited:${sourceId}`,
|
|
notification_type: 'deposit_credited',
|
|
source_kind: 'liquidity_action',
|
|
source_id: details.tx_hash,
|
|
title: 'Deposit credited',
|
|
message: [
|
|
`Deposit credited: ${amount}`,
|
|
`chain=${payload.chain || details.chain || 'unknown'}`,
|
|
`tx=${details.tx_hash}`,
|
|
].join('\n'),
|
|
priority: 'default',
|
|
tags: ['unrip', 'deposit'],
|
|
data: {
|
|
action_type: payload.action_type,
|
|
status,
|
|
asset_id: payload.asset_id || details.asset_id || null,
|
|
amount_units: String(details.amount || '0'),
|
|
tx_hash: details.tx_hash,
|
|
chain: payload.chain || details.chain || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (payload.action_type === 'withdrawal_status_changed' && COMPLETED_WITHDRAWAL_STATUSES.has(status)) {
|
|
if (!details.withdrawal_hash) return null;
|
|
const asset = assetFor(config, payload.asset_id || details.asset_id);
|
|
const amount = formatAssetUnits(details.amount || '0', asset);
|
|
const sourceId = `${payload.chain || details.chain || 'unknown'}:${payload.asset_id || details.asset_id || 'unknown'}:${details.withdrawal_hash}`;
|
|
return {
|
|
notification_key: `withdrawal_completed:${sourceId}`,
|
|
notification_type: 'withdrawal_completed',
|
|
source_kind: 'liquidity_action',
|
|
source_id: details.withdrawal_hash,
|
|
title: 'Withdrawal completed',
|
|
message: [
|
|
`Withdrawal completed: ${amount}`,
|
|
`chain=${payload.chain || details.chain || 'unknown'}`,
|
|
`withdrawal=${details.withdrawal_hash}`,
|
|
details.transfer_tx_hash ? `transfer=${details.transfer_tx_hash}` : null,
|
|
].filter(Boolean).join('\n'),
|
|
priority: 'default',
|
|
tags: ['unrip', 'withdrawal'],
|
|
data: {
|
|
action_type: payload.action_type,
|
|
status,
|
|
asset_id: payload.asset_id || details.asset_id || null,
|
|
amount_units: String(details.amount || '0'),
|
|
withdrawal_hash: details.withdrawal_hash,
|
|
transfer_tx_hash: details.transfer_tx_hash || null,
|
|
chain: payload.chain || details.chain || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function buildQuoteOutcomeNotification({ record, config } = {}) {
|
|
if (!hasProvenSettlement(record)) return null;
|
|
const quoteId = record.quote_id || record.payload?.quote_id || null;
|
|
if (!quoteId) return null;
|
|
const delta = record.attributed_inventory_delta || record.payload?.attributed_inventory_delta || {};
|
|
const inventoryId = delta.inventory_id || 'unknown-inventory';
|
|
const deltaSummary = formatDeltaSummary(delta.delta_units || {}, config);
|
|
return {
|
|
notification_key: `trade_completed:quote:${quoteId}:${inventoryId}`,
|
|
notification_type: 'trade_completed',
|
|
source_kind: 'quote_response',
|
|
source_id: quoteId,
|
|
title: 'Trade completed',
|
|
message: [
|
|
`Quote trade completed: ${quoteId}`,
|
|
`asset movement: ${deltaSummary}`,
|
|
record.gross_edge_pct || record.payload?.gross_edge_pct ? `edge=${record.gross_edge_pct || record.payload?.gross_edge_pct}%` : null,
|
|
`inventory=${inventoryId}`,
|
|
].filter(Boolean).join('\n'),
|
|
priority: 'default',
|
|
tags: ['unrip', 'trade'],
|
|
data: {
|
|
trade_kind: 'quote_response',
|
|
quote_id: quoteId,
|
|
decision_id: record.decision_id || record.payload?.decision_id || null,
|
|
command_id: record.command_id || record.payload?.command_id || null,
|
|
outcome_status: record.outcome_status || record.payload?.outcome_status || null,
|
|
attribution_status: record.attribution_status || record.payload?.attribution_status || null,
|
|
inventory_id: inventoryId,
|
|
delta_units: delta.delta_units || null,
|
|
gross_edge_pct: record.gross_edge_pct || record.payload?.gross_edge_pct || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildIntentRequestOutcomeNotification({ record, config } = {}) {
|
|
if (!hasProvenSettlement(record)) return null;
|
|
const requestId = record.request_id || record.payload?.request_id || null;
|
|
if (!requestId) return null;
|
|
const delta = record.attributed_inventory_delta || record.payload?.attributed_inventory_delta || {};
|
|
const inventoryId = delta.inventory_id || 'unknown-inventory';
|
|
const deltaSummary = formatDeltaSummary(delta.delta_units || {}, config);
|
|
return {
|
|
notification_key: `trade_completed:intent_request:${requestId}:${inventoryId}`,
|
|
notification_type: 'trade_completed',
|
|
source_kind: 'intent_request',
|
|
source_id: requestId,
|
|
title: 'Trade completed',
|
|
message: [
|
|
`Own request completed: ${requestId}`,
|
|
`asset movement: ${deltaSummary}`,
|
|
record.intent_hash || record.payload?.intent_hash ? `intent=${record.intent_hash || record.payload?.intent_hash}` : null,
|
|
`inventory=${inventoryId}`,
|
|
].filter(Boolean).join('\n'),
|
|
priority: 'default',
|
|
tags: ['unrip', 'trade'],
|
|
data: {
|
|
trade_kind: 'intent_request',
|
|
request_id: requestId,
|
|
idempotency_key: record.idempotency_key || record.payload?.idempotency_key || null,
|
|
submission_id: record.submission_id || record.payload?.submission_id || null,
|
|
intent_hash: record.intent_hash || record.payload?.intent_hash || null,
|
|
outcome_status: record.outcome_status || record.payload?.outcome_status || null,
|
|
attribution_status: record.attribution_status || record.payload?.attribution_status || null,
|
|
inventory_id: inventoryId,
|
|
delta_units: delta.delta_units || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function observedAtOrAfter(value, minObservedAt) {
|
|
if (!minObservedAt) return true;
|
|
const valueTs = Date.parse(value || '');
|
|
const minTs = Date.parse(minObservedAt || '');
|
|
return Number.isFinite(valueTs) && Number.isFinite(minTs) && valueTs >= minTs;
|
|
}
|
|
|
|
export function outcomeNotificationObservedAtOrAfter(record = {}, minObservedAt) {
|
|
return observedAtOrAfter(record.outcome_observed_at || record.payload?.outcome_observed_at, minObservedAt)
|
|
|| observedAtOrAfter(record.computed_at || record.payload?.computed_at, minObservedAt);
|
|
}
|
|
|
|
export function hasProvenSettlement(record = {}) {
|
|
const payload = record.payload || {};
|
|
const status = record.outcome_status || payload.outcome_status;
|
|
const attributionStatus = record.attribution_status || payload.attribution_status;
|
|
const delta = record.attributed_inventory_delta || payload.attributed_inventory_delta;
|
|
return status === 'completed'
|
|
&& SETTLED_ATTRIBUTION_STATUSES.has(attributionStatus)
|
|
&& Boolean(delta?.delta_units);
|
|
}
|
|
|
|
export function createNotificationDispatcher({ client, store, logger = null } = {}) {
|
|
return {
|
|
async publishOnce(notification) {
|
|
if (!notification) return { ok: true, skipped: true, reason: 'no_notification' };
|
|
if (!client?.isConfigured?.()) {
|
|
return { ok: true, skipped: true, reason: 'notification_client_not_configured' };
|
|
}
|
|
|
|
const claimed = await store.claim(notification);
|
|
if (!claimed) return { ok: true, skipped: true, reason: 'notification_already_delivered' };
|
|
|
|
const result = await client.publish({
|
|
title: notification.title,
|
|
message: notification.message,
|
|
priority: notification.priority,
|
|
tags: notification.tags,
|
|
click: notification.click,
|
|
});
|
|
|
|
await store.finish(notification, result);
|
|
if (!result.ok) {
|
|
logger?.warn?.('notification_delivery_failed', {
|
|
details: {
|
|
notification_key: notification.notification_key,
|
|
notification_type: notification.notification_type,
|
|
result,
|
|
},
|
|
});
|
|
}
|
|
return result.ok
|
|
? { ...result, delivered: true }
|
|
: { ...result, delivered: false };
|
|
},
|
|
};
|
|
}
|
|
|
|
function hasAssetRegistry(config) {
|
|
return config?.assetRegistry && typeof config.assetRegistry.get === 'function';
|
|
}
|
|
|
|
function assetFor(config, assetId) {
|
|
if (hasAssetRegistry(config) && config.assetRegistry.get(assetId)) return config.assetRegistry.get(assetId);
|
|
if (config?.tradingBtc?.assetId === assetId) return config.tradingBtc;
|
|
if (config?.tradingEure?.assetId === assetId) return config.tradingEure;
|
|
return { assetId, symbol: shortAssetId(assetId), decimals: 0 };
|
|
}
|
|
|
|
function formatAssetUnits(units, asset) {
|
|
return `${formatUnitsDecimal(units || '0', Number(asset?.decimals || 0))} ${asset?.symbol || shortAssetId(asset?.assetId)}`;
|
|
}
|
|
|
|
function formatSignedAssetUnits(units, asset) {
|
|
const value = BigInt(String(units || '0'));
|
|
const sign = value > 0n ? '+' : value < 0n ? '-' : '';
|
|
const absolute = value < 0n ? -value : value;
|
|
return `${sign}${formatAssetUnits(absolute.toString(), asset)}`;
|
|
}
|
|
|
|
function formatDeltaSummary(deltaUnits, config) {
|
|
const parts = Object.entries(deltaUnits || {})
|
|
.filter(([, units]) => BigInt(String(units || '0')) !== 0n)
|
|
.map(([assetId, units]) => formatSignedAssetUnits(units, assetFor(config, assetId)));
|
|
return parts.length ? parts.join(', ') : 'no non-zero delta';
|
|
}
|
|
|
|
function normalizeStatus(status) {
|
|
return String(status || '').trim().toUpperCase();
|
|
}
|
|
|
|
function shortAssetId(assetId) {
|
|
if (!assetId) return 'asset';
|
|
const raw = String(assetId);
|
|
if (raw.length <= 18) return raw;
|
|
return `${raw.slice(0, 8)}...${raw.slice(-6)}`;
|
|
}
|