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)}`; }