Proof: npm test (138 passing); npm run operator-dashboard:build; git diff --cached --check. Assumptions: Bridge deposits expose near_token_id/intents_token_id for credited asset attribution; nBTC is the solver trading reserve while btc.omft.near remains a tracked legacy BTC wrapper. Still fake: No live asset migration was submitted; existing btc.omft.near balance is only tracked and withdrawable until a separately approved conversion or withdrawal is executed.
This commit is contained in:
parent
8507403b0f
commit
ab078d976a
18 changed files with 597 additions and 73 deletions
|
|
@ -10,12 +10,14 @@ data:
|
||||||
NEAR_INTENTS_RPC_URL: https://solver-relay-v2.chaindefuser.com/rpc
|
NEAR_INTENTS_RPC_URL: https://solver-relay-v2.chaindefuser.com/rpc
|
||||||
NEAR_INTENTS_BRIDGE_RPC_URL: https://bridge.chaindefuser.com/rpc
|
NEAR_INTENTS_BRIDGE_RPC_URL: https://bridge.chaindefuser.com/rpc
|
||||||
NEAR_INTENTS_VERIFIER_CONTRACT: intents.near
|
NEAR_INTENTS_VERIFIER_CONTRACT: intents.near
|
||||||
NEAR_RPC_URL: https://rpc.fastnear.com
|
NEAR_RPC_URL: https://near.lava.build
|
||||||
NEAR_INTENTS_PAIR_FILTER: nep141:btc.omft.near->nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near
|
NEAR_INTENTS_PAIR_FILTER: nep141:nbtc.bridge.near->nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near
|
||||||
NEAR_INTENTS_STATUS_POLL_MS: "60000"
|
NEAR_INTENTS_STATUS_POLL_MS: "60000"
|
||||||
NEAR_INTENTS_ACCOUNT_ID: unrip-dev.near
|
NEAR_INTENTS_ACCOUNT_ID: unrip-dev.near
|
||||||
TRADING_BTC_ASSET_ID: nep141:btc.omft.near
|
TRADING_BTC_ASSET_ID: nep141:nbtc.bridge.near
|
||||||
|
TRADING_BTC_TRACKED_ASSET_IDS: nep141:nbtc.bridge.near,nep141:btc.omft.near
|
||||||
TRADING_BTC_SYMBOL: BTC
|
TRADING_BTC_SYMBOL: BTC
|
||||||
|
TRADING_BTC_LABEL: BTC / nBTC reserve
|
||||||
TRADING_BTC_DECIMALS: "8"
|
TRADING_BTC_DECIMALS: "8"
|
||||||
TRADING_BTC_CHAIN: btc:mainnet
|
TRADING_BTC_CHAIN: btc:mainnet
|
||||||
TRADING_BTC_WITHDRAW_ADDRESS: ""
|
TRADING_BTC_WITHDRAW_ADDRESS: ""
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,7 @@ const controlApi = startControlApi({
|
||||||
async function refreshPortfolioMetrics() {
|
async function refreshPortfolioMetrics() {
|
||||||
const inputs = await loadPortfolioMetricInputs(pool, {
|
const inputs = await loadPortfolioMetricInputs(pool, {
|
||||||
btcAsset: config.tradingBtc,
|
btcAsset: config.tradingBtc,
|
||||||
|
btcAssets: config.tradingBtcAssets,
|
||||||
eureAsset: config.tradingEure,
|
eureAsset: config.tradingEure,
|
||||||
});
|
});
|
||||||
const payload = computePortfolioMetric({
|
const payload = computePortfolioMetric({
|
||||||
|
|
@ -351,6 +352,7 @@ async function refreshPortfolioMetrics() {
|
||||||
currentPrice: inputs.currentPrice?.payload,
|
currentPrice: inputs.currentPrice?.payload,
|
||||||
externalFlows: inputs.externalFlows || [],
|
externalFlows: inputs.externalFlows || [],
|
||||||
btcAsset: config.tradingBtc,
|
btcAsset: config.tradingBtc,
|
||||||
|
btcAssets: config.tradingBtcAssets,
|
||||||
eureAsset: config.tradingEure,
|
eureAsset: config.tradingEure,
|
||||||
commandCount: inputs.commandCount,
|
commandCount: inputs.commandCount,
|
||||||
resultCount: inputs.resultCount,
|
resultCount: inputs.resultCount,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import process from 'node:process';
|
||||||
|
|
||||||
import { createConsumer } from '../bus/kafka/consumer.mjs';
|
import { createConsumer } from '../bus/kafka/consumer.mjs';
|
||||||
import { createProducer } from '../bus/kafka/producer.mjs';
|
import { createProducer } from '../bus/kafka/producer.mjs';
|
||||||
|
import {
|
||||||
|
bridgeDepositAssetId,
|
||||||
|
uniqueChainsForAssets,
|
||||||
|
} from '../core/bridge-assets.mjs';
|
||||||
import { startControlApi } from '../core/control-api.mjs';
|
import { startControlApi } from '../core/control-api.mjs';
|
||||||
import { buildEventEnvelope, parseEventMessage } from '../core/event-envelope.mjs';
|
import { buildEventEnvelope, parseEventMessage } from '../core/event-envelope.mjs';
|
||||||
import { buildFundingVisibility } from '../core/funding-observations.mjs';
|
import { buildFundingVisibility } from '../core/funding-observations.mjs';
|
||||||
|
|
@ -43,6 +47,12 @@ const producer = await createProducer({
|
||||||
clientId: config.kafkaClientId,
|
clientId: config.kafkaClientId,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const chains = uniqueChainsForAssets(config.trackedAssets);
|
||||||
|
const fallbackAssetByChain = new Map([
|
||||||
|
[config.tradingBtc.chain, config.tradingBtc.assetId],
|
||||||
|
[config.tradingEure.chain, config.tradingEure.assetId],
|
||||||
|
]);
|
||||||
const consumer = await createConsumer({
|
const consumer = await createConsumer({
|
||||||
groupId: config.kafkaConsumerGroupInventory,
|
groupId: config.kafkaConsumerGroupInventory,
|
||||||
brokers: config.kafkaBrokers,
|
brokers: config.kafkaBrokers,
|
||||||
|
|
@ -117,23 +127,30 @@ async function refresh() {
|
||||||
try {
|
try {
|
||||||
const balances = await verifierClient.mtBatchBalanceOf({
|
const balances = await verifierClient.mtBatchBalanceOf({
|
||||||
accountId: config.nearIntentsAccountId,
|
accountId: config.nearIntentsAccountId,
|
||||||
tokenIds: config.activeAssetIds,
|
tokenIds: config.trackedAssetIds,
|
||||||
});
|
});
|
||||||
const recentDeposits = [];
|
const recentDeposits = [];
|
||||||
for (const chain of [config.tradingBtc.chain, config.tradingEure.chain]) {
|
for (const chain of chains) {
|
||||||
const response = await bridgeClient.recentDeposits({
|
const response = await bridgeClient.recentDeposits({
|
||||||
accountId: config.nearIntentsAccountId,
|
accountId: config.nearIntentsAccountId,
|
||||||
chain,
|
chain,
|
||||||
});
|
});
|
||||||
for (const deposit of response?.deposits || []) {
|
for (const deposit of response?.deposits || []) {
|
||||||
|
const assetId = bridgeDepositAssetId(deposit, {
|
||||||
|
assetRegistry: config.assetRegistry,
|
||||||
|
fallbackAssetId: fallbackAssetByChain.get(chain),
|
||||||
|
});
|
||||||
recentDeposits.push({
|
recentDeposits.push({
|
||||||
tx_hash: deposit.tx_hash || null,
|
tx_hash: deposit.tx_hash || null,
|
||||||
chain,
|
chain,
|
||||||
asset_id: chain === config.tradingBtc.chain ? config.tradingBtc.assetId : config.tradingEure.assetId,
|
asset_id: assetId,
|
||||||
amount: String(deposit.amount || '0'),
|
amount: String(deposit.amount || '0'),
|
||||||
address: deposit.address,
|
address: deposit.address,
|
||||||
status: deposit.status,
|
status: deposit.status,
|
||||||
decimals: deposit.decimals,
|
decimals: deposit.decimals,
|
||||||
|
near_token_id: deposit.near_token_id || null,
|
||||||
|
intents_token_id: deposit.intents_token_id || null,
|
||||||
|
defuse_asset_identifier: deposit.defuse_asset_identifier || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
|
||||||
import { createProducer } from '../bus/kafka/producer.mjs';
|
import { createProducer } from '../bus/kafka/producer.mjs';
|
||||||
|
import {
|
||||||
|
assetsByChain as groupAssetsByChain,
|
||||||
|
bridgeDepositAssetId,
|
||||||
|
uniqueChainsForAssets,
|
||||||
|
} from '../core/bridge-assets.mjs';
|
||||||
import { startControlApi } from '../core/control-api.mjs';
|
import { startControlApi } from '../core/control-api.mjs';
|
||||||
import { buildEventEnvelope } from '../core/event-envelope.mjs';
|
import { buildEventEnvelope } from '../core/event-envelope.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -83,8 +88,9 @@ const store = createJsonStateStore({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const chains = [config.tradingBtc.chain, config.tradingEure.chain];
|
const chains = uniqueChainsForAssets(config.trackedAssets);
|
||||||
const assetsByChain = new Map([
|
const trackedAssetsByChain = groupAssetsByChain(config.trackedAssets);
|
||||||
|
const fallbackAssetByChain = new Map([
|
||||||
[config.tradingBtc.chain, config.tradingBtc.assetId],
|
[config.tradingBtc.chain, config.tradingBtc.assetId],
|
||||||
[config.tradingEure.chain, config.tradingEure.assetId],
|
[config.tradingEure.chain, config.tradingEure.assetId],
|
||||||
]);
|
]);
|
||||||
|
|
@ -145,8 +151,11 @@ async function refreshChain(chain, state) {
|
||||||
action_type: 'deposit_address_refreshed',
|
action_type: 'deposit_address_refreshed',
|
||||||
status: 'READY',
|
status: 'READY',
|
||||||
chain,
|
chain,
|
||||||
asset_id: assetsByChain.get(chain),
|
asset_id: chainAssetIds(chain).length === 1 ? chainAssetIds(chain)[0] : null,
|
||||||
details: depositAddress,
|
details: {
|
||||||
|
...depositAddress,
|
||||||
|
asset_ids: chainAssetIds(chain),
|
||||||
|
},
|
||||||
}, state);
|
}, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,8 +165,12 @@ async function refreshChain(chain, state) {
|
||||||
});
|
});
|
||||||
const bridgeDeposits = deposits?.deposits || [];
|
const bridgeDeposits = deposits?.deposits || [];
|
||||||
for (const deposit of bridgeDeposits) {
|
for (const deposit of bridgeDeposits) {
|
||||||
const key = `${chain}:${deposit.tx_hash || deposit.address}:${deposit.defuse_asset_identifier}`;
|
const assetId = mapDepositAssetId(deposit, chain);
|
||||||
const assetId = mapDepositAssetId(deposit.defuse_asset_identifier, chain);
|
const key = [
|
||||||
|
chain,
|
||||||
|
deposit.tx_hash || deposit.address,
|
||||||
|
deposit.intents_token_id || deposit.near_token_id || deposit.defuse_asset_identifier,
|
||||||
|
].join(':');
|
||||||
const normalized = {
|
const normalized = {
|
||||||
tx_hash: deposit.tx_hash || null,
|
tx_hash: deposit.tx_hash || null,
|
||||||
chain,
|
chain,
|
||||||
|
|
@ -167,6 +180,9 @@ async function refreshChain(chain, state) {
|
||||||
address: deposit.address,
|
address: deposit.address,
|
||||||
status: deposit.status,
|
status: deposit.status,
|
||||||
decimals: deposit.decimals,
|
decimals: deposit.decimals,
|
||||||
|
near_token_id: deposit.near_token_id || null,
|
||||||
|
intents_token_id: deposit.intents_token_id || null,
|
||||||
|
defuse_asset_identifier: deposit.defuse_asset_identifier || null,
|
||||||
};
|
};
|
||||||
const previous = state.deposits[key];
|
const previous = state.deposits[key];
|
||||||
state.deposits[key] = normalized;
|
state.deposits[key] = normalized;
|
||||||
|
|
@ -240,6 +256,11 @@ async function refreshFundingObservations({ chain, state, fundingHandle, bridgeD
|
||||||
try {
|
try {
|
||||||
const observed = await observer.listTransactions({ address: fundingHandle });
|
const observed = await observer.listTransactions({ address: fundingHandle });
|
||||||
for (const tx of observed.transactions) {
|
for (const tx of observed.transactions) {
|
||||||
|
const bridgeDeposit = matchBridgeDeposit({
|
||||||
|
txHash: tx.tx_hash,
|
||||||
|
fundingHandle,
|
||||||
|
bridgeDeposits,
|
||||||
|
});
|
||||||
const key = buildFundingObservationKey({
|
const key = buildFundingObservationKey({
|
||||||
chain,
|
chain,
|
||||||
fundingHandle,
|
fundingHandle,
|
||||||
|
|
@ -249,7 +270,9 @@ async function refreshFundingObservations({ chain, state, fundingHandle, bridgeD
|
||||||
const next = correlateFundingObservation({
|
const next = correlateFundingObservation({
|
||||||
existing: previous,
|
existing: previous,
|
||||||
accountId: config.nearIntentsAccountId,
|
accountId: config.nearIntentsAccountId,
|
||||||
assetId: assetsByChain.get(chain),
|
assetId: bridgeDeposit
|
||||||
|
? mapDepositAssetId(bridgeDeposit, chain)
|
||||||
|
: fallbackAssetByChain.get(chain),
|
||||||
chain,
|
chain,
|
||||||
fundingHandle,
|
fundingHandle,
|
||||||
source: tx.source || observed.source,
|
source: tx.source || observed.source,
|
||||||
|
|
@ -257,11 +280,7 @@ async function refreshFundingObservations({ chain, state, fundingHandle, bridgeD
|
||||||
amount: tx.amount,
|
amount: tx.amount,
|
||||||
confirmations: tx.confirmations,
|
confirmations: tx.confirmations,
|
||||||
observedAt: tx.observed_at || observed.observed_at,
|
observedAt: tx.observed_at || observed.observed_at,
|
||||||
bridgeDeposit: matchBridgeDeposit({
|
bridgeDeposit,
|
||||||
txHash: tx.tx_hash,
|
|
||||||
fundingHandle,
|
|
||||||
bridgeDeposits,
|
|
||||||
}),
|
|
||||||
stuckAfterMs: config.fundingObservationStuckMs,
|
stuckAfterMs: config.fundingObservationStuckMs,
|
||||||
});
|
});
|
||||||
state.funding_observations[key] = next;
|
state.funding_observations[key] = next;
|
||||||
|
|
@ -273,10 +292,15 @@ async function refreshFundingObservations({ chain, state, fundingHandle, bridgeD
|
||||||
for (const [key, previous] of Object.entries(state.funding_observations)) {
|
for (const [key, previous] of Object.entries(state.funding_observations)) {
|
||||||
if (previous.chain !== chain || previous.funding_handle !== fundingHandle) continue;
|
if (previous.chain !== chain || previous.funding_handle !== fundingHandle) continue;
|
||||||
|
|
||||||
|
const bridgeDeposit = matchBridgeDeposit({
|
||||||
|
txHash: previous.tx_hash,
|
||||||
|
fundingHandle,
|
||||||
|
bridgeDeposits,
|
||||||
|
});
|
||||||
const next = correlateFundingObservation({
|
const next = correlateFundingObservation({
|
||||||
existing: previous,
|
existing: previous,
|
||||||
accountId: previous.account_id,
|
accountId: previous.account_id,
|
||||||
assetId: previous.asset_id,
|
assetId: bridgeDeposit ? mapDepositAssetId(bridgeDeposit, chain) : previous.asset_id,
|
||||||
chain: previous.chain,
|
chain: previous.chain,
|
||||||
fundingHandle: previous.funding_handle,
|
fundingHandle: previous.funding_handle,
|
||||||
source: previous.source,
|
source: previous.source,
|
||||||
|
|
@ -284,11 +308,7 @@ async function refreshFundingObservations({ chain, state, fundingHandle, bridgeD
|
||||||
amount: previous.amount,
|
amount: previous.amount,
|
||||||
confirmations: previous.confirmations,
|
confirmations: previous.confirmations,
|
||||||
observedAt: refreshedAt,
|
observedAt: refreshedAt,
|
||||||
bridgeDeposit: matchBridgeDeposit({
|
bridgeDeposit,
|
||||||
txHash: previous.tx_hash,
|
|
||||||
fundingHandle,
|
|
||||||
bridgeDeposits,
|
|
||||||
}),
|
|
||||||
stuckAfterMs: config.fundingObservationStuckMs,
|
stuckAfterMs: config.fundingObservationStuckMs,
|
||||||
});
|
});
|
||||||
state.funding_observations[key] = next;
|
state.funding_observations[key] = next;
|
||||||
|
|
@ -768,10 +788,15 @@ function mapSupportedTokens(tokens) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDepositAssetId(defuseAssetIdentifier, chain) {
|
function mapDepositAssetId(deposit, chain) {
|
||||||
if (chain === config.tradingBtc.chain) return config.tradingBtc.assetId;
|
return bridgeDepositAssetId(deposit, {
|
||||||
if (chain === config.tradingEure.chain) return config.tradingEure.assetId;
|
assetRegistry: config.assetRegistry,
|
||||||
return defuseAssetIdentifier;
|
fallbackAssetId: fallbackAssetByChain.get(chain),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function chainAssetIds(chain) {
|
||||||
|
return (trackedAssetsByChain.get(chain) || []).map((asset) => asset.assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferWithdrawStatusCode(error) {
|
function inferWithdrawStatusCode(error) {
|
||||||
|
|
@ -796,10 +821,10 @@ function buildPublicState() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account_id: config.nearIntentsAccountId,
|
account_id: config.nearIntentsAccountId,
|
||||||
withdrawal_defaults: {
|
tracked_assets: config.trackedAssets,
|
||||||
[config.tradingBtc.assetId]: config.tradingBtc.withdrawAddress || null,
|
withdrawal_defaults: Object.fromEntries(
|
||||||
[config.tradingEure.assetId]: config.tradingEure.withdrawAddress || null,
|
config.trackedAssets.map((asset) => [asset.assetId, asset.withdrawAddress || null]),
|
||||||
},
|
),
|
||||||
...state,
|
...state,
|
||||||
observer_health: buildObserverHealth(state.observer_health, {
|
observer_health: buildObserverHealth(state.observer_health, {
|
||||||
now,
|
now,
|
||||||
|
|
|
||||||
43
src/core/bridge-assets.mjs
Normal file
43
src/core/bridge-assets.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
export function intentsAssetIdFromNearTokenId(nearTokenId) {
|
||||||
|
const normalized = String(nearTokenId || '').trim();
|
||||||
|
if (!normalized) return null;
|
||||||
|
if (normalized.startsWith('nep141:')) return normalized;
|
||||||
|
return `nep141:${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bridgeDepositAssetId(deposit, {
|
||||||
|
assetRegistry = new Map(),
|
||||||
|
fallbackAssetId = null,
|
||||||
|
} = {}) {
|
||||||
|
const candidates = [
|
||||||
|
deposit?.intents_token_id,
|
||||||
|
intentsAssetIdFromNearTokenId(deposit?.near_token_id),
|
||||||
|
deposit?.defuse_asset_identifier,
|
||||||
|
fallbackAssetId,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!assetRegistry?.size || assetRegistry.has(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackAssetId || candidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uniqueChainsForAssets(assets = []) {
|
||||||
|
return [...new Set(
|
||||||
|
assets
|
||||||
|
.map((asset) => asset?.chain)
|
||||||
|
.filter(Boolean),
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assetsByChain(assets = []) {
|
||||||
|
const byChain = new Map();
|
||||||
|
for (const asset of assets) {
|
||||||
|
if (!asset?.chain) continue;
|
||||||
|
const chainAssets = byChain.get(asset.chain) || [];
|
||||||
|
chainAssets.push(asset);
|
||||||
|
byChain.set(asset.chain, chainAssets);
|
||||||
|
}
|
||||||
|
return byChain;
|
||||||
|
}
|
||||||
|
|
@ -175,7 +175,8 @@ export function hasFundingObservationChanged(previous, next) {
|
||||||
if (!previous) return true;
|
if (!previous) return true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
previous.status !== next.status
|
previous.asset_id !== next.asset_id
|
||||||
|
|| previous.status !== next.status
|
||||||
|| previous.confirmations !== next.confirmations
|
|| previous.confirmations !== next.confirmations
|
||||||
|| previous.bridge_deposit_tx_hash !== next.bridge_deposit_tx_hash
|
|| previous.bridge_deposit_tx_hash !== next.bridge_deposit_tx_hash
|
||||||
|| previous.bridge_status !== next.bridge_status
|
|| previous.bridge_status !== next.bridge_status
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,7 @@ export function createDashboardLiveState({
|
||||||
config,
|
config,
|
||||||
active_pair: config.activePair,
|
active_pair: config.activePair,
|
||||||
btc_asset: config.tradingBtc,
|
btc_asset: config.tradingBtc,
|
||||||
|
btc_assets: config.tradingBtcAssets || [config.tradingBtc],
|
||||||
eure_asset: config.tradingEure,
|
eure_asset: config.tradingEure,
|
||||||
quote_limit: config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT,
|
quote_limit: config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT,
|
||||||
lifecycle_limit: config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT,
|
lifecycle_limit: config.operatorDashboardLifecycleLimit || DASHBOARD_LIVE_LIFECYCLE_LIMIT,
|
||||||
|
|
@ -670,6 +671,7 @@ export function buildLiveStatusBar(state) {
|
||||||
inventory: state.latest_inventory,
|
inventory: state.latest_inventory,
|
||||||
marketPrice: state.latest_market_price,
|
marketPrice: state.latest_market_price,
|
||||||
btcAsset: state.btc_asset,
|
btcAsset: state.btc_asset,
|
||||||
|
btcAssets: state.btc_assets,
|
||||||
eureAsset: state.eure_asset,
|
eureAsset: state.eure_asset,
|
||||||
}),
|
}),
|
||||||
active_alert_count: 0,
|
active_alert_count: 0,
|
||||||
|
|
@ -728,6 +730,8 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) {
|
||||||
return {
|
return {
|
||||||
asset_id: asset.assetId,
|
asset_id: asset.assetId,
|
||||||
symbol: asset.symbol,
|
symbol: asset.symbol,
|
||||||
|
label: asset.label || asset.symbol,
|
||||||
|
role: asset.role || null,
|
||||||
chain: asset.chain,
|
chain: asset.chain,
|
||||||
spendable_units: spendableUnits,
|
spendable_units: spendableUnits,
|
||||||
spendable: formatUnits(spendableUnits, asset.decimals),
|
spendable: formatUnits(spendableUnits, asset.decimals),
|
||||||
|
|
@ -772,14 +776,24 @@ function buildFundingSummary({ config, fundingObservations, recentDepositStatuse
|
||||||
withdrawals_frozen: liquidityState?.withdrawals_frozen ?? null,
|
withdrawals_frozen: liquidityState?.withdrawals_frozen ?? null,
|
||||||
withdrawal_defaults: liquidityState?.withdrawal_defaults || {},
|
withdrawal_defaults: liquidityState?.withdrawal_defaults || {},
|
||||||
},
|
},
|
||||||
handles: Object.entries(liquidityState?.deposit_addresses || {}).map(([chain, details]) => ({
|
handles: Object.entries(liquidityState?.deposit_addresses || {}).map(([chain, details]) => {
|
||||||
chain,
|
const chainAssets = (config.trackedAssets || [...config.assetRegistry.values()])
|
||||||
asset_id: config.tradingBtc.chain === chain ? config.tradingBtc.assetId : config.tradingEure.assetId,
|
.filter((asset) => asset.chain === chain);
|
||||||
symbol: config.tradingBtc.chain === chain ? config.tradingBtc.symbol : config.tradingEure.symbol,
|
return {
|
||||||
address: details?.address || null,
|
chain,
|
||||||
memo: details?.memo || null,
|
asset_id: chainAssets.length === 1 ? chainAssets[0].assetId : null,
|
||||||
refreshed_at: details?.refreshed_at || null,
|
asset_ids: chainAssets.map((asset) => asset.assetId),
|
||||||
})),
|
symbol: chainAssets.length === 1
|
||||||
|
? chainAssets[0].symbol
|
||||||
|
: chainAssets.map((asset) => asset.label || asset.symbol).join(', '),
|
||||||
|
label: chainAssets.length === 1
|
||||||
|
? (chainAssets[0].label || chainAssets[0].symbol)
|
||||||
|
: `${chain} funding handle`,
|
||||||
|
address: details?.address || null,
|
||||||
|
memo: details?.memo || null,
|
||||||
|
refreshed_at: details?.refreshed_at || null,
|
||||||
|
};
|
||||||
|
}),
|
||||||
credited_deposits: recentFundingActivity
|
credited_deposits: recentFundingActivity
|
||||||
.filter((observation) => CREDITED_FUNDING_STATUSES.has(String(observation?.status || '').toUpperCase()))
|
.filter((observation) => CREDITED_FUNDING_STATUSES.has(String(observation?.status || '').toUpperCase()))
|
||||||
.sort((left, right) => sortTimestamps(
|
.sort((left, right) => sortTimestamps(
|
||||||
|
|
@ -793,6 +807,7 @@ function buildFundingSummary({ config, fundingObservations, recentDepositStatuse
|
||||||
return {
|
return {
|
||||||
asset_id: entry.asset_id,
|
asset_id: entry.asset_id,
|
||||||
symbol: asset?.symbol || entry.asset_id,
|
symbol: asset?.symbol || entry.asset_id,
|
||||||
|
asset_symbol: asset?.label || asset?.symbol || entry.asset_id,
|
||||||
pre_credit_total_units: entry.pre_credit_total || '0',
|
pre_credit_total_units: entry.pre_credit_total || '0',
|
||||||
pre_credit_total: formatUnits(entry.pre_credit_total || '0', asset?.decimals || 0),
|
pre_credit_total: formatUnits(entry.pre_credit_total || '0', asset?.decimals || 0),
|
||||||
latest_status: entry.latest_status,
|
latest_status: entry.latest_status,
|
||||||
|
|
@ -806,6 +821,7 @@ function buildFundingSummary({ config, fundingObservations, recentDepositStatuse
|
||||||
chain: entry.chain,
|
chain: entry.chain,
|
||||||
asset_id: entry.asset_id,
|
asset_id: entry.asset_id,
|
||||||
symbol: asset?.symbol || entry.asset_id,
|
symbol: asset?.symbol || entry.asset_id,
|
||||||
|
asset_symbol: asset?.label || asset?.symbol || entry.asset_id,
|
||||||
pre_credit_total_units: entry.pre_credit_total || '0',
|
pre_credit_total_units: entry.pre_credit_total || '0',
|
||||||
pre_credit_total: formatUnits(entry.pre_credit_total || '0', asset?.decimals || 0),
|
pre_credit_total: formatUnits(entry.pre_credit_total || '0', asset?.decimals || 0),
|
||||||
latest_status: entry.latest_status,
|
latest_status: entry.latest_status,
|
||||||
|
|
@ -882,11 +898,16 @@ function buildRecentWithdrawals({ config, liquidityState }) {
|
||||||
withdrawal_hash: withdrawal.withdrawal_hash,
|
withdrawal_hash: withdrawal.withdrawal_hash,
|
||||||
asset_id: withdrawal.asset_id,
|
asset_id: withdrawal.asset_id,
|
||||||
symbol: asset?.symbol || withdrawal.asset_id,
|
symbol: asset?.symbol || withdrawal.asset_id,
|
||||||
|
asset_symbol: asset?.label || asset?.symbol || withdrawal.asset_id,
|
||||||
chain: withdrawal.chain || null,
|
chain: withdrawal.chain || null,
|
||||||
amount_units: String(withdrawal.amount || '0'),
|
amount_units: String(withdrawal.amount || '0'),
|
||||||
|
amount_display: formatUnits(withdrawal.amount || '0', asset?.decimals || 0),
|
||||||
amount: formatUnits(withdrawal.amount || '0', asset?.decimals || 0),
|
amount: formatUnits(withdrawal.amount || '0', asset?.decimals || 0),
|
||||||
status: withdrawal.status || null,
|
status: withdrawal.status || null,
|
||||||
address: withdrawal.address || null,
|
address: withdrawal.address || null,
|
||||||
|
destination_address: withdrawal.address || null,
|
||||||
|
requested_at: withdrawal.submitted_at || withdrawal.noted_at || null,
|
||||||
|
completed_at: withdrawal.status === 'COMPLETED' ? withdrawal.last_checked_at : null,
|
||||||
submitted_at: withdrawal.submitted_at || null,
|
submitted_at: withdrawal.submitted_at || null,
|
||||||
last_checked_at: withdrawal.last_checked_at || null,
|
last_checked_at: withdrawal.last_checked_at || null,
|
||||||
noted_at: withdrawal.noted_at || null,
|
noted_at: withdrawal.noted_at || null,
|
||||||
|
|
@ -1849,11 +1870,13 @@ function normalizeFundingObservationForUi({ config, observation }) {
|
||||||
funding_observation_id: observation.funding_observation_id,
|
funding_observation_id: observation.funding_observation_id,
|
||||||
asset_id: observation.asset_id,
|
asset_id: observation.asset_id,
|
||||||
symbol: asset?.symbol || observation.asset_id,
|
symbol: asset?.symbol || observation.asset_id,
|
||||||
|
asset_symbol: asset?.label || asset?.symbol || observation.asset_id,
|
||||||
chain: observation.chain,
|
chain: observation.chain,
|
||||||
funding_handle: observation.funding_handle,
|
funding_handle: observation.funding_handle,
|
||||||
tx_hash: observation.tx_hash,
|
tx_hash: observation.tx_hash,
|
||||||
status: observation.status,
|
status: observation.status,
|
||||||
amount_units: observation.amount,
|
amount_units: observation.amount,
|
||||||
|
amount_display: formatUnits(observation.amount || '0', asset?.decimals || 0),
|
||||||
amount: formatUnits(observation.amount || '0', asset?.decimals || 0),
|
amount: formatUnits(observation.amount || '0', asset?.decimals || 0),
|
||||||
confirmations: observation.confirmations,
|
confirmations: observation.confirmations,
|
||||||
first_seen_at: observation.first_seen_at,
|
first_seen_at: observation.first_seen_at,
|
||||||
|
|
@ -1888,11 +1911,13 @@ function normalizeLiquidityDepositForUi({ config, deposit, observedAt }) {
|
||||||
funding_observation_id: null,
|
funding_observation_id: null,
|
||||||
asset_id: deposit?.asset_id || null,
|
asset_id: deposit?.asset_id || null,
|
||||||
symbol: asset?.symbol || deposit?.asset_id || null,
|
symbol: asset?.symbol || deposit?.asset_id || null,
|
||||||
|
asset_symbol: asset?.label || asset?.symbol || deposit?.asset_id || null,
|
||||||
chain: deposit?.chain || null,
|
chain: deposit?.chain || null,
|
||||||
funding_handle: deposit?.address || null,
|
funding_handle: deposit?.address || null,
|
||||||
tx_hash: deposit?.tx_hash || null,
|
tx_hash: deposit?.tx_hash || null,
|
||||||
status,
|
status,
|
||||||
amount_units: String(deposit?.amount || '0'),
|
amount_units: String(deposit?.amount || '0'),
|
||||||
|
amount_display: formatUnits(deposit?.amount || '0', asset?.decimals || 0),
|
||||||
amount: formatUnits(deposit?.amount || '0', asset?.decimals || 0),
|
amount: formatUnits(deposit?.amount || '0', asset?.decimals || 0),
|
||||||
confirmations: null,
|
confirmations: null,
|
||||||
first_seen_at: timestamp,
|
first_seen_at: timestamp,
|
||||||
|
|
@ -2117,12 +2142,21 @@ function highestAlertSeverity(alerts) {
|
||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeCurrentPortfolioValue({ inventory, marketPrice, btcAsset, eureAsset }) {
|
function computeCurrentPortfolioValue({
|
||||||
|
inventory,
|
||||||
|
marketPrice,
|
||||||
|
btcAsset,
|
||||||
|
btcAssets = null,
|
||||||
|
eureAsset,
|
||||||
|
}) {
|
||||||
if (!inventory || !marketPrice || !btcAsset || !eureAsset) return null;
|
if (!inventory || !marketPrice || !btcAsset || !eureAsset) return null;
|
||||||
|
|
||||||
const btcUnits = String(inventory.spendable?.[btcAsset.assetId] || '0');
|
const effectiveBtcAssets = btcAssets?.length ? btcAssets : [btcAsset];
|
||||||
|
const btcScaled = effectiveBtcAssets.reduce((total, asset) => {
|
||||||
|
const units = String(inventory.spendable?.[asset.assetId] || '0');
|
||||||
|
return total + unitsToScaledDecimal(units, asset.decimals);
|
||||||
|
}, 0n);
|
||||||
const eureUnits = String(inventory.spendable?.[eureAsset.assetId] || '0');
|
const eureUnits = String(inventory.spendable?.[eureAsset.assetId] || '0');
|
||||||
const btcScaled = unitsToScaledDecimal(btcUnits, btcAsset.decimals);
|
|
||||||
const eureScaled = unitsToScaledDecimal(eureUnits, eureAsset.decimals);
|
const eureScaled = unitsToScaledDecimal(eureUnits, eureAsset.decimals);
|
||||||
const priceScaled = parseScaledDecimal(marketPrice.eure_per_btc || marketPrice.eur_per_btc || '0');
|
const priceScaled = parseScaledDecimal(marketPrice.eure_per_btc || marketPrice.eur_per_btc || '0');
|
||||||
const total = eureScaled + multiplyScaled(btcScaled, priceScaled);
|
const total = eureScaled + multiplyScaled(btcScaled, priceScaled);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|
||||||
export const DEFAULT_NEAR_INTENTS_PAIR_FILTER =
|
export const DEFAULT_NEAR_INTENTS_PAIR_FILTER =
|
||||||
'nep141:btc.omft.near->nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near';
|
'nep141:nbtc.bridge.near->nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near';
|
||||||
|
|
||||||
export function parsePairFilter(argv) {
|
export function parsePairFilter(argv) {
|
||||||
const idx = argv.indexOf('--pair');
|
const idx = argv.indexOf('--pair');
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,20 @@ export function computePortfolioMetric({
|
||||||
currentPrice,
|
currentPrice,
|
||||||
externalFlows = [],
|
externalFlows = [],
|
||||||
btcAsset,
|
btcAsset,
|
||||||
|
btcAssets = null,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
commandCount = 0,
|
commandCount = 0,
|
||||||
resultCount = 0,
|
resultCount = 0,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (!currentInventory || !currentPrice || !btcAsset?.assetId || !eureAsset?.assetId) {
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
||||||
|
const displayBtcAsset = effectiveBtcAssets[0];
|
||||||
|
if (!currentInventory || !currentPrice || !displayBtcAsset?.assetId || !eureAsset?.assetId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBtcUnits = String(currentInventory.spendable?.[btcAsset.assetId] || '0');
|
const currentBtcUnits = sumAssetUnits(currentInventory, effectiveBtcAssets).toString();
|
||||||
const currentEureUnits = String(currentInventory.spendable?.[eureAsset.assetId] || '0');
|
const currentEureUnits = String(currentInventory.spendable?.[eureAsset.assetId] || '0');
|
||||||
const currentBtc = unitsToScaledDecimal(currentBtcUnits, btcAsset.decimals);
|
const currentBtc = sumAssetScaled(currentInventory, effectiveBtcAssets);
|
||||||
const currentEure = unitsToScaledDecimal(currentEureUnits, eureAsset.decimals);
|
const currentEure = unitsToScaledDecimal(currentEureUnits, eureAsset.decimals);
|
||||||
const currentPriceScaled = parseScaledDecimal(currentPrice.eure_per_btc);
|
const currentPriceScaled = parseScaledDecimal(currentPrice.eure_per_btc);
|
||||||
const currentBtcMarkValue = multiplyScaled(currentBtc, currentPriceScaled);
|
const currentBtcMarkValue = multiplyScaled(currentBtc, currentPriceScaled);
|
||||||
|
|
@ -35,7 +38,8 @@ export function computePortfolioMetric({
|
||||||
},
|
},
|
||||||
current_inventory: buildInventoryView({
|
current_inventory: buildInventoryView({
|
||||||
inventory: currentInventory,
|
inventory: currentInventory,
|
||||||
btcAsset,
|
btcAsset: displayBtcAsset,
|
||||||
|
btcAssets: effectiveBtcAssets,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
}),
|
}),
|
||||||
current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue),
|
current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue),
|
||||||
|
|
@ -70,9 +74,9 @@ export function computePortfolioMetric({
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baselineBtcUnits = String(baseline.inventory.spendable?.[btcAsset.assetId] || '0');
|
const baselineBtcUnits = sumAssetUnits(baseline.inventory, effectiveBtcAssets).toString();
|
||||||
const baselineEureUnits = String(baseline.inventory.spendable?.[eureAsset.assetId] || '0');
|
const baselineEureUnits = String(baseline.inventory.spendable?.[eureAsset.assetId] || '0');
|
||||||
const baselineBtc = unitsToScaledDecimal(baselineBtcUnits, btcAsset.decimals);
|
const baselineBtc = sumAssetScaled(baseline.inventory, effectiveBtcAssets);
|
||||||
const baselineEure = unitsToScaledDecimal(baselineEureUnits, eureAsset.decimals);
|
const baselineEure = unitsToScaledDecimal(baselineEureUnits, eureAsset.decimals);
|
||||||
const baselinePriceScaled = parseScaledDecimal(baseline.price.eure_per_btc);
|
const baselinePriceScaled = parseScaledDecimal(baseline.price.eure_per_btc);
|
||||||
const baselinePortfolioAtBaselinePrice = baselineEure + multiplyScaled(baselineBtc, baselinePriceScaled);
|
const baselinePortfolioAtBaselinePrice = baselineEure + multiplyScaled(baselineBtc, baselinePriceScaled);
|
||||||
|
|
@ -81,7 +85,8 @@ export function computePortfolioMetric({
|
||||||
const externalFlowSummary = summarizeExternalFlows({
|
const externalFlowSummary = summarizeExternalFlows({
|
||||||
externalFlows,
|
externalFlows,
|
||||||
currentPriceScaled,
|
currentPriceScaled,
|
||||||
btcAsset,
|
btcAsset: displayBtcAsset,
|
||||||
|
btcAssets: effectiveBtcAssets,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
});
|
});
|
||||||
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
|
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
|
||||||
|
|
@ -119,7 +124,7 @@ export function computePortfolioMetric({
|
||||||
net_btc_units: externalFlowSummary.netBtcUnits.toString(),
|
net_btc_units: externalFlowSummary.netBtcUnits.toString(),
|
||||||
net_btc: formatScaledDecimal(unitsToScaledDecimal(
|
net_btc: formatScaledDecimal(unitsToScaledDecimal(
|
||||||
externalFlowSummary.netBtcUnits.toString(),
|
externalFlowSummary.netBtcUnits.toString(),
|
||||||
btcAsset.decimals,
|
displayBtcAsset.decimals,
|
||||||
)),
|
)),
|
||||||
net_eure_units: externalFlowSummary.netEureUnits.toString(),
|
net_eure_units: externalFlowSummary.netEureUnits.toString(),
|
||||||
net_eure: formatScaledDecimal(unitsToScaledDecimal(
|
net_eure: formatScaledDecimal(unitsToScaledDecimal(
|
||||||
|
|
@ -145,7 +150,8 @@ export function computePortfolioMetric({
|
||||||
},
|
},
|
||||||
inventory: buildInventoryView({
|
inventory: buildInventoryView({
|
||||||
inventory: baseline.inventory,
|
inventory: baseline.inventory,
|
||||||
btcAsset,
|
btcAsset: displayBtcAsset,
|
||||||
|
btcAssets: effectiveBtcAssets,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
@ -162,9 +168,10 @@ export function buildPortfolioMetricId({ baselineInventoryId, currentInventoryId
|
||||||
].join(':');
|
].join(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInventoryView({ inventory, btcAsset, eureAsset }) {
|
function buildInventoryView({ inventory, btcAsset, btcAssets = null, eureAsset }) {
|
||||||
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
||||||
const spendable = inventory?.spendable || {};
|
const spendable = inventory?.spendable || {};
|
||||||
const btcUnits = String(spendable[btcAsset.assetId] || '0');
|
const btcUnits = sumAssetUnits(inventory, effectiveBtcAssets).toString();
|
||||||
const eureUnits = String(spendable[eureAsset.assetId] || '0');
|
const eureUnits = String(spendable[eureAsset.assetId] || '0');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -172,6 +179,15 @@ function buildInventoryView({ inventory, btcAsset, eureAsset }) {
|
||||||
synced_at: inventory?.synced_at || null,
|
synced_at: inventory?.synced_at || null,
|
||||||
btc_units: btcUnits,
|
btc_units: btcUnits,
|
||||||
btc: formatAssetUnits(btcUnits, btcAsset.decimals),
|
btc: formatAssetUnits(btcUnits, btcAsset.decimals),
|
||||||
|
btc_assets: effectiveBtcAssets.map((asset) => {
|
||||||
|
const units = String(spendable[asset.assetId] || '0');
|
||||||
|
return {
|
||||||
|
asset_id: asset.assetId,
|
||||||
|
label: asset.label || asset.symbol,
|
||||||
|
units,
|
||||||
|
amount: formatAssetUnits(units, asset.decimals),
|
||||||
|
};
|
||||||
|
}),
|
||||||
eure_units: eureUnits,
|
eure_units: eureUnits,
|
||||||
eure: formatAssetUnits(eureUnits, eureAsset.decimals),
|
eure: formatAssetUnits(eureUnits, eureAsset.decimals),
|
||||||
};
|
};
|
||||||
|
|
@ -181,8 +197,10 @@ function summarizeExternalFlows({
|
||||||
externalFlows,
|
externalFlows,
|
||||||
currentPriceScaled,
|
currentPriceScaled,
|
||||||
btcAsset,
|
btcAsset,
|
||||||
|
btcAssets = null,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
}) {
|
}) {
|
||||||
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
||||||
let flowCount = 0;
|
let flowCount = 0;
|
||||||
let depositCount = 0;
|
let depositCount = 0;
|
||||||
let withdrawalCount = 0;
|
let withdrawalCount = 0;
|
||||||
|
|
@ -205,9 +223,10 @@ function summarizeExternalFlows({
|
||||||
latestEffectiveAt = flow.effective_at || null;
|
latestEffectiveAt = flow.effective_at || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flow.asset_id === btcAsset.assetId) {
|
const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === flow.asset_id);
|
||||||
|
if (flowBtcAsset) {
|
||||||
netBtcUnits += signedUnits;
|
netBtcUnits += signedUnits;
|
||||||
const btcAmount = unitsToScaledDecimal(signedUnits.toString(), btcAsset.decimals);
|
const btcAmount = unitsToScaledDecimal(signedUnits.toString(), flowBtcAsset.decimals);
|
||||||
const flowPriceScaled = parseScaledDecimal(
|
const flowPriceScaled = parseScaledDecimal(
|
||||||
flow.reference_price_eure_per_btc_at_flow_time || '0',
|
flow.reference_price_eure_per_btc_at_flow_time || '0',
|
||||||
);
|
);
|
||||||
|
|
@ -233,6 +252,31 @@ function summarizeExternalFlows({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBtcAssets({ btcAsset, btcAssets = null }) {
|
||||||
|
const assets = btcAssets?.length ? btcAssets : [btcAsset];
|
||||||
|
const byId = new Map();
|
||||||
|
for (const asset of assets) {
|
||||||
|
if (!asset?.assetId) continue;
|
||||||
|
byId.set(asset.assetId, asset);
|
||||||
|
}
|
||||||
|
return [...byId.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumAssetUnits(inventory, assets) {
|
||||||
|
const spendable = inventory?.spendable || {};
|
||||||
|
return assets.reduce(
|
||||||
|
(total, asset) => total + BigInt(spendable[asset.assetId] || '0'),
|
||||||
|
0n,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumAssetScaled(inventory, assets) {
|
||||||
|
const spendable = inventory?.spendable || {};
|
||||||
|
return assets.reduce((total, asset) => (
|
||||||
|
total + unitsToScaledDecimal(String(spendable[asset.assetId] || '0'), asset.decimals)
|
||||||
|
), 0n);
|
||||||
|
}
|
||||||
|
|
||||||
function unitsToScaledDecimal(units, decimals) {
|
function unitsToScaledDecimal(units, decimals) {
|
||||||
return BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals);
|
return BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const DEFAULTS = {
|
||||||
nearIntentsWsUrl: 'wss://solver-relay-v2.chaindefuser.com/ws',
|
nearIntentsWsUrl: 'wss://solver-relay-v2.chaindefuser.com/ws',
|
||||||
nearIntentsRpcUrl: 'https://solver-relay-v2.chaindefuser.com/rpc',
|
nearIntentsRpcUrl: 'https://solver-relay-v2.chaindefuser.com/rpc',
|
||||||
nearBridgeRpcUrl: 'https://bridge.chaindefuser.com/rpc',
|
nearBridgeRpcUrl: 'https://bridge.chaindefuser.com/rpc',
|
||||||
nearRpcUrl: 'https://rpc.fastnear.com',
|
nearRpcUrl: 'https://near.lava.build',
|
||||||
nearVerifierContract: 'intents.near',
|
nearVerifierContract: 'intents.near',
|
||||||
nearIntentsPairFilter: DEFAULT_NEAR_INTENTS_PAIR_FILTER,
|
nearIntentsPairFilter: DEFAULT_NEAR_INTENTS_PAIR_FILTER,
|
||||||
nearIntentsPairFilterReloadMs: 5_000,
|
nearIntentsPairFilterReloadMs: 5_000,
|
||||||
|
|
@ -45,8 +45,14 @@ const DEFAULTS = {
|
||||||
postgresUrl: 'postgresql://unrip:unrip@127.0.0.1:5432/unrip',
|
postgresUrl: 'postgresql://unrip:unrip@127.0.0.1:5432/unrip',
|
||||||
projectName: 'unrip',
|
projectName: 'unrip',
|
||||||
projectNamespace: 'unrip',
|
projectNamespace: 'unrip',
|
||||||
tradingBtcAssetId: 'nep141:btc.omft.near',
|
tradingBtcAssetId: 'nep141:nbtc.bridge.near',
|
||||||
|
tradingBtcTrackedAssetIds: [
|
||||||
|
'nep141:nbtc.bridge.near',
|
||||||
|
'nep141:btc.omft.near',
|
||||||
|
],
|
||||||
tradingBtcSymbol: 'BTC',
|
tradingBtcSymbol: 'BTC',
|
||||||
|
tradingBtcLabel: 'BTC / nBTC reserve',
|
||||||
|
tradingBtcLegacyLabel: 'BTC / legacy OMFT',
|
||||||
tradingBtcDecimals: 8,
|
tradingBtcDecimals: 8,
|
||||||
tradingBtcChain: 'btc:mainnet',
|
tradingBtcChain: 'btc:mainnet',
|
||||||
tradingBtcWithdrawAddress: '',
|
tradingBtcWithdrawAddress: '',
|
||||||
|
|
@ -138,16 +144,36 @@ function parseBoolean(value, fallback) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAsset({ assetId, symbol, decimals, chain, withdrawAddress = '' }) {
|
function unique(values) {
|
||||||
|
return [...new Set(values.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAsset({
|
||||||
|
assetId,
|
||||||
|
symbol,
|
||||||
|
decimals,
|
||||||
|
chain,
|
||||||
|
withdrawAddress = '',
|
||||||
|
label = symbol,
|
||||||
|
role = 'tracked',
|
||||||
|
}) {
|
||||||
return {
|
return {
|
||||||
assetId,
|
assetId,
|
||||||
symbol,
|
symbol,
|
||||||
|
label,
|
||||||
decimals,
|
decimals,
|
||||||
chain,
|
chain,
|
||||||
withdrawAddress,
|
withdrawAddress,
|
||||||
|
role,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildBtcAssetLabel(assetId, tradingAssetId) {
|
||||||
|
if (assetId === tradingAssetId) return DEFAULTS.tradingBtcLabel;
|
||||||
|
if (assetId === 'nep141:btc.omft.near') return DEFAULTS.tradingBtcLegacyLabel;
|
||||||
|
return `${DEFAULTS.tradingBtcSymbol} / ${assetId.replace(/^nep141:/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function defaultControlBaseUrl({ serviceName, port, namespace }) {
|
function defaultControlBaseUrl({ serviceName, port, namespace }) {
|
||||||
if (process.env.KUBERNETES_SERVICE_HOST) {
|
if (process.env.KUBERNETES_SERVICE_HOST) {
|
||||||
return `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
|
return `http://${serviceName}.${namespace}.svc.cluster.local:${port}`;
|
||||||
|
|
@ -161,19 +187,46 @@ export function loadConfig({ envPath = '.env' } = {}) {
|
||||||
const tradingBtc = buildAsset({
|
const tradingBtc = buildAsset({
|
||||||
assetId: process.env.TRADING_BTC_ASSET_ID || DEFAULTS.tradingBtcAssetId,
|
assetId: process.env.TRADING_BTC_ASSET_ID || DEFAULTS.tradingBtcAssetId,
|
||||||
symbol: process.env.TRADING_BTC_SYMBOL || DEFAULTS.tradingBtcSymbol,
|
symbol: process.env.TRADING_BTC_SYMBOL || DEFAULTS.tradingBtcSymbol,
|
||||||
|
label: process.env.TRADING_BTC_LABEL || DEFAULTS.tradingBtcLabel,
|
||||||
decimals: parseNumber(process.env.TRADING_BTC_DECIMALS, DEFAULTS.tradingBtcDecimals),
|
decimals: parseNumber(process.env.TRADING_BTC_DECIMALS, DEFAULTS.tradingBtcDecimals),
|
||||||
chain: process.env.TRADING_BTC_CHAIN || DEFAULTS.tradingBtcChain,
|
chain: process.env.TRADING_BTC_CHAIN || DEFAULTS.tradingBtcChain,
|
||||||
withdrawAddress:
|
withdrawAddress:
|
||||||
process.env.TRADING_BTC_WITHDRAW_ADDRESS || DEFAULTS.tradingBtcWithdrawAddress,
|
process.env.TRADING_BTC_WITHDRAW_ADDRESS || DEFAULTS.tradingBtcWithdrawAddress,
|
||||||
|
role: 'trading',
|
||||||
|
});
|
||||||
|
const configuredTrackedBtcAssetIds = splitCsv(process.env.TRADING_BTC_TRACKED_ASSET_IDS);
|
||||||
|
const trackedBtcAssetIds = unique([
|
||||||
|
tradingBtc.assetId,
|
||||||
|
...(configuredTrackedBtcAssetIds.length
|
||||||
|
? configuredTrackedBtcAssetIds
|
||||||
|
: DEFAULTS.tradingBtcTrackedAssetIds),
|
||||||
|
]);
|
||||||
|
const tradingBtcAssets = trackedBtcAssetIds.map((assetId) => {
|
||||||
|
if (assetId === tradingBtc.assetId) return tradingBtc;
|
||||||
|
return buildAsset({
|
||||||
|
assetId,
|
||||||
|
symbol: tradingBtc.symbol,
|
||||||
|
label: buildBtcAssetLabel(assetId, tradingBtc.assetId),
|
||||||
|
decimals: tradingBtc.decimals,
|
||||||
|
chain: tradingBtc.chain,
|
||||||
|
withdrawAddress: tradingBtc.withdrawAddress,
|
||||||
|
role: 'legacy',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const tradingEure = buildAsset({
|
const tradingEure = buildAsset({
|
||||||
assetId: process.env.TRADING_EURE_ASSET_ID || DEFAULTS.tradingEureAssetId,
|
assetId: process.env.TRADING_EURE_ASSET_ID || DEFAULTS.tradingEureAssetId,
|
||||||
symbol: process.env.TRADING_EURE_SYMBOL || DEFAULTS.tradingEureSymbol,
|
symbol: process.env.TRADING_EURE_SYMBOL || DEFAULTS.tradingEureSymbol,
|
||||||
|
label: process.env.TRADING_EURE_LABEL || DEFAULTS.tradingEureSymbol,
|
||||||
decimals: parseNumber(process.env.TRADING_EURE_DECIMALS, DEFAULTS.tradingEureDecimals),
|
decimals: parseNumber(process.env.TRADING_EURE_DECIMALS, DEFAULTS.tradingEureDecimals),
|
||||||
chain: process.env.TRADING_EURE_CHAIN || DEFAULTS.tradingEureChain,
|
chain: process.env.TRADING_EURE_CHAIN || DEFAULTS.tradingEureChain,
|
||||||
withdrawAddress:
|
withdrawAddress:
|
||||||
process.env.TRADING_EURE_WITHDRAW_ADDRESS || DEFAULTS.tradingEureWithdrawAddress,
|
process.env.TRADING_EURE_WITHDRAW_ADDRESS || DEFAULTS.tradingEureWithdrawAddress,
|
||||||
|
role: 'trading',
|
||||||
});
|
});
|
||||||
|
const trackedAssets = [
|
||||||
|
...tradingBtcAssets,
|
||||||
|
tradingEure,
|
||||||
|
];
|
||||||
|
|
||||||
const projectName = process.env.PROJECT_NAME || DEFAULTS.projectName;
|
const projectName = process.env.PROJECT_NAME || DEFAULTS.projectName;
|
||||||
const projectNamespace =
|
const projectNamespace =
|
||||||
|
|
@ -380,13 +433,13 @@ export function loadConfig({ envPath = '.env' } = {}) {
|
||||||
projectName,
|
projectName,
|
||||||
projectNamespace,
|
projectNamespace,
|
||||||
tradingBtc,
|
tradingBtc,
|
||||||
|
tradingBtcAssets,
|
||||||
tradingEure,
|
tradingEure,
|
||||||
activePair: `${tradingBtc.assetId}->${tradingEure.assetId}`,
|
activePair: `${tradingBtc.assetId}->${tradingEure.assetId}`,
|
||||||
activeAssetIds: [tradingBtc.assetId, tradingEure.assetId],
|
activeAssetIds: [tradingBtc.assetId, tradingEure.assetId],
|
||||||
assetRegistry: new Map([
|
trackedAssets,
|
||||||
[tradingBtc.assetId, tradingBtc],
|
trackedAssetIds: trackedAssets.map((asset) => asset.assetId),
|
||||||
[tradingEure.assetId, tradingEure],
|
assetRegistry: new Map(trackedAssets.map((asset) => [asset.assetId, asset])),
|
||||||
]),
|
|
||||||
marketReferenceRefreshMs: parseNumber(
|
marketReferenceRefreshMs: parseNumber(
|
||||||
process.env.MARKET_REFERENCE_REFRESH_MS,
|
process.env.MARKET_REFERENCE_REFRESH_MS,
|
||||||
DEFAULTS.marketReferenceRefreshMs,
|
DEFAULTS.marketReferenceRefreshMs,
|
||||||
|
|
|
||||||
|
|
@ -335,8 +335,10 @@ export async function insertEnvironmentStatusChange(pool, { topic, event, record
|
||||||
|
|
||||||
export async function loadPortfolioMetricInputs(pool, {
|
export async function loadPortfolioMetricInputs(pool, {
|
||||||
btcAsset = null,
|
btcAsset = null,
|
||||||
|
btcAssets = null,
|
||||||
eureAsset = null,
|
eureAsset = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
||||||
const [currentInventory, currentPrice, commandAggregate, resultAggregate] = await Promise.all([
|
const [currentInventory, currentPrice, commandAggregate, resultAggregate] = await Promise.all([
|
||||||
loadLatestEventPayload(pool, 'intent_inventory_snapshots'),
|
loadLatestEventPayload(pool, 'intent_inventory_snapshots'),
|
||||||
loadLatestEventPayload(pool, 'market_price_events'),
|
loadLatestEventPayload(pool, 'market_price_events'),
|
||||||
|
|
@ -376,12 +378,13 @@ export async function loadPortfolioMetricInputs(pool, {
|
||||||
const externalFlows = (
|
const externalFlows = (
|
||||||
baselineInventory
|
baselineInventory
|
||||||
&& baselinePrice
|
&& baselinePrice
|
||||||
&& btcAsset?.assetId
|
&& effectiveBtcAssets.length
|
||||||
&& eureAsset?.assetId
|
&& eureAsset?.assetId
|
||||||
)
|
)
|
||||||
? await loadExternalAssetFlowsSince(pool, {
|
? await loadExternalAssetFlowsSince(pool, {
|
||||||
since: firstCommandAt,
|
since: firstCommandAt,
|
||||||
btcAsset,
|
btcAsset: effectiveBtcAssets[0],
|
||||||
|
btcAssets: effectiveBtcAssets,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
@ -1258,8 +1261,10 @@ async function loadNearestPricePayload(pool, anchorAt) {
|
||||||
async function loadExternalAssetFlowsSince(pool, {
|
async function loadExternalAssetFlowsSince(pool, {
|
||||||
since,
|
since,
|
||||||
btcAsset,
|
btcAsset,
|
||||||
|
btcAssets = null,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
||||||
const [depositRows, withdrawalRows] = await Promise.all([
|
const [depositRows, withdrawalRows] = await Promise.all([
|
||||||
loadCreditedDepositRowsSince(pool, since),
|
loadCreditedDepositRowsSince(pool, since),
|
||||||
loadCompletedWithdrawalRowsSince(pool, since),
|
loadCompletedWithdrawalRowsSince(pool, since),
|
||||||
|
|
@ -1272,7 +1277,8 @@ async function loadExternalAssetFlowsSince(pool, {
|
||||||
row,
|
row,
|
||||||
kind: 'deposit',
|
kind: 'deposit',
|
||||||
sign: 1n,
|
sign: 1n,
|
||||||
btcAsset,
|
btcAsset: effectiveBtcAssets[0],
|
||||||
|
btcAssets: effectiveBtcAssets,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -1282,7 +1288,8 @@ async function loadExternalAssetFlowsSince(pool, {
|
||||||
row,
|
row,
|
||||||
kind: 'withdrawal',
|
kind: 'withdrawal',
|
||||||
sign: -1n,
|
sign: -1n,
|
||||||
btcAsset,
|
btcAsset: effectiveBtcAssets[0],
|
||||||
|
btcAssets: effectiveBtcAssets,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -1353,6 +1360,7 @@ async function normalizeExternalFlowRow(pool, {
|
||||||
kind,
|
kind,
|
||||||
sign,
|
sign,
|
||||||
btcAsset,
|
btcAsset,
|
||||||
|
btcAssets = null,
|
||||||
eureAsset,
|
eureAsset,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const payload = row?.payload || {};
|
const payload = row?.payload || {};
|
||||||
|
|
@ -1364,8 +1372,10 @@ async function normalizeExternalFlowRow(pool, {
|
||||||
const effectiveAt = toIsoTimestamp(row.observed_at || row.ingested_at);
|
const effectiveAt = toIsoTimestamp(row.observed_at || row.ingested_at);
|
||||||
const signedUnits = (sign * BigInt(amount)).toString();
|
const signedUnits = (sign * BigInt(amount)).toString();
|
||||||
let referencePriceAtFlowTime = null;
|
let referencePriceAtFlowTime = null;
|
||||||
|
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
|
||||||
|
const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === assetId);
|
||||||
|
|
||||||
if (assetId === btcAsset?.assetId) {
|
if (flowBtcAsset) {
|
||||||
const nearestPrice = await loadNearestPricePayload(pool, effectiveAt);
|
const nearestPrice = await loadNearestPricePayload(pool, effectiveAt);
|
||||||
referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null;
|
referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null;
|
||||||
} else if (assetId !== eureAsset?.assetId) {
|
} else if (assetId !== eureAsset?.assetId) {
|
||||||
|
|
@ -1387,6 +1397,15 @@ async function normalizeExternalFlowRow(pool, {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBtcAssets({ btcAsset = null, btcAssets = null } = {}) {
|
||||||
|
const assets = btcAssets?.length ? btcAssets : [btcAsset];
|
||||||
|
return [...new Map(
|
||||||
|
assets
|
||||||
|
.filter((asset) => asset?.assetId)
|
||||||
|
.map((asset) => [asset.assetId, asset]),
|
||||||
|
).values()];
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePortfolioMetricRow(row) {
|
function normalizePortfolioMetricRow(row) {
|
||||||
return {
|
return {
|
||||||
metric_id: row.metric_id,
|
metric_id: row.metric_id,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ function BalancesTable({ items }) {
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<tr key={item.asset_id}>
|
<tr key={item.asset_id}>
|
||||||
<td>
|
<td>
|
||||||
<strong>{item.symbol}</strong>
|
<strong>{item.label || item.symbol}</strong>
|
||||||
<div className="muted mono">{item.asset_id}</div>
|
<div className="muted mono">{item.asset_id}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="mono">{item.spendable}</td>
|
<td className="mono">{item.spendable}</td>
|
||||||
|
|
@ -61,11 +61,14 @@ function HandlesList({ handles }) {
|
||||||
{handles.map((handle) => (
|
{handles.map((handle) => (
|
||||||
<div className="service-card" key={`${handle.chain}:${handle.address || handle.asset_id}`}>
|
<div className="service-card" key={`${handle.chain}:${handle.address || handle.asset_id}`}>
|
||||||
<div className="service-head">
|
<div className="service-head">
|
||||||
<strong>{handle.symbol}</strong>
|
<strong>{handle.label || handle.symbol}</strong>
|
||||||
<Pill label={handle.chain} stateLabel="healthy" />
|
<Pill label={handle.chain} stateLabel="healthy" />
|
||||||
</div>
|
</div>
|
||||||
<div className="service-detail">
|
<div className="service-detail">
|
||||||
<div className="mono">{handle.address || 'Unavailable'}</div>
|
<div className="mono">{handle.address || 'Unavailable'}</div>
|
||||||
|
{handle.asset_ids?.length ? (
|
||||||
|
<div className="mono">{handle.asset_ids.join(', ')}</div>
|
||||||
|
) : null}
|
||||||
{handle.memo ? <div className="mono">{`Memo ${handle.memo}`}</div> : null}
|
{handle.memo ? <div className="mono">{`Memo ${handle.memo}`}</div> : null}
|
||||||
<div>{`Refreshed ${formatTimestamp(handle.refreshed_at)}`}</div>
|
<div>{`Refreshed ${formatTimestamp(handle.refreshed_at)}`}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -209,7 +212,7 @@ function WithdrawalEstimateForm({ balances, withdrawalDefaults, onControl }) {
|
||||||
>
|
>
|
||||||
{(balances || []).map((item) => (
|
{(balances || []).map((item) => (
|
||||||
<option key={item.asset_id} value={item.asset_id}>
|
<option key={item.asset_id} value={item.asset_id}>
|
||||||
{item.symbol}
|
{item.label || item.symbol}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
39
test/bridge-assets.test.mjs
Normal file
39
test/bridge-assets.test.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
bridgeDepositAssetId,
|
||||||
|
intentsAssetIdFromNearTokenId,
|
||||||
|
} from '../src/core/bridge-assets.mjs';
|
||||||
|
|
||||||
|
const NBTC = 'nep141:nbtc.bridge.near';
|
||||||
|
const LEGACY_BTC = 'nep141:btc.omft.near';
|
||||||
|
|
||||||
|
const assetRegistry = new Map([
|
||||||
|
[NBTC, { assetId: NBTC }],
|
||||||
|
[LEGACY_BTC, { assetId: LEGACY_BTC }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('intentsAssetIdFromNearTokenId normalizes NEAR token ids to verifier asset ids', () => {
|
||||||
|
assert.equal(intentsAssetIdFromNearTokenId('nbtc.bridge.near'), NBTC);
|
||||||
|
assert.equal(intentsAssetIdFromNearTokenId(NBTC), NBTC);
|
||||||
|
assert.equal(intentsAssetIdFromNearTokenId(''), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bridgeDepositAssetId uses credited bridge near_token_id instead of chain-only fallback', () => {
|
||||||
|
assert.equal(bridgeDepositAssetId({
|
||||||
|
near_token_id: 'btc.omft.near',
|
||||||
|
defuse_asset_identifier: 'btc:mainnet:native',
|
||||||
|
}, {
|
||||||
|
assetRegistry,
|
||||||
|
fallbackAssetId: NBTC,
|
||||||
|
}), LEGACY_BTC);
|
||||||
|
|
||||||
|
assert.equal(bridgeDepositAssetId({
|
||||||
|
near_token_id: 'nbtc.bridge.near',
|
||||||
|
defuse_asset_identifier: 'btc:mainnet:native',
|
||||||
|
}, {
|
||||||
|
assetRegistry,
|
||||||
|
fallbackAssetId: LEGACY_BTC,
|
||||||
|
}), NBTC);
|
||||||
|
});
|
||||||
56
test/config-assets.test.mjs
Normal file
56
test/config-assets.test.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { loadConfig } from '../src/lib/config.mjs';
|
||||||
|
|
||||||
|
const ENV_KEYS = [
|
||||||
|
'NEAR_RPC_URL',
|
||||||
|
'NEAR_INTENTS_PAIR_FILTER',
|
||||||
|
'TRADING_BTC_ASSET_ID',
|
||||||
|
'TRADING_BTC_TRACKED_ASSET_IDS',
|
||||||
|
'TRADING_BTC_LABEL',
|
||||||
|
'TRADING_EURE_ASSET_ID',
|
||||||
|
];
|
||||||
|
|
||||||
|
const NBTC = 'nep141:nbtc.bridge.near';
|
||||||
|
const LEGACY_BTC = 'nep141:btc.omft.near';
|
||||||
|
const EURE = 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near';
|
||||||
|
|
||||||
|
function withCleanEnv(fn) {
|
||||||
|
const previous = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
|
||||||
|
for (const key of ENV_KEYS) delete process.env[key];
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
for (const [key, value] of Object.entries(previous)) {
|
||||||
|
if (value == null) delete process.env[key];
|
||||||
|
else process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('default config trades nBTC while still tracking legacy BTC', () => withCleanEnv(() => {
|
||||||
|
const config = loadConfig({ envPath: '/tmp/unrip-no-such-env-file' });
|
||||||
|
|
||||||
|
assert.equal(config.nearRpcUrl, 'https://near.lava.build');
|
||||||
|
assert.equal(
|
||||||
|
config.nearIntentsPairFilter,
|
||||||
|
`${NBTC}->${EURE}`,
|
||||||
|
);
|
||||||
|
assert.equal(config.tradingBtc.assetId, NBTC);
|
||||||
|
assert.deepEqual(config.activeAssetIds, [NBTC, EURE]);
|
||||||
|
assert.deepEqual(config.trackedAssetIds, [NBTC, LEGACY_BTC, EURE]);
|
||||||
|
assert.equal(config.assetRegistry.get(NBTC).label, 'BTC / nBTC reserve');
|
||||||
|
assert.equal(config.assetRegistry.get(LEGACY_BTC).label, 'BTC / legacy OMFT');
|
||||||
|
assert.equal(config.assetRegistry.get(LEGACY_BTC).role, 'legacy');
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('tracked BTC ids always include the configured trading BTC reserve', () => withCleanEnv(() => {
|
||||||
|
process.env.TRADING_BTC_ASSET_ID = NBTC;
|
||||||
|
process.env.TRADING_BTC_TRACKED_ASSET_IDS = LEGACY_BTC;
|
||||||
|
|
||||||
|
const config = loadConfig({ envPath: '/tmp/unrip-no-such-env-file' });
|
||||||
|
|
||||||
|
assert.deepEqual(config.tradingBtcAssets.map((asset) => asset.assetId), [NBTC, LEGACY_BTC]);
|
||||||
|
assert.deepEqual(config.activeAssetIds, [NBTC, EURE]);
|
||||||
|
}));
|
||||||
|
|
@ -4,6 +4,7 @@ import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
buildFundingVisibility,
|
buildFundingVisibility,
|
||||||
correlateFundingObservation,
|
correlateFundingObservation,
|
||||||
|
hasFundingObservationChanged,
|
||||||
} from '../src/core/funding-observations.mjs';
|
} from '../src/core/funding-observations.mjs';
|
||||||
import { buildInventorySnapshot } from '../src/core/inventory.mjs';
|
import { buildInventorySnapshot } from '../src/core/inventory.mjs';
|
||||||
import { routeHistoryRecord } from '../src/core/history-records.mjs';
|
import { routeHistoryRecord } from '../src/core/history-records.mjs';
|
||||||
|
|
@ -79,6 +80,26 @@ test('funding observation correlates to later credit without losing tx hash', ()
|
||||||
assert.equal(credited.credited_at, '2026-04-03T08:10:00.000Z');
|
assert.equal(credited.credited_at, '2026-04-03T08:10:00.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('funding observation republishes when bridge metadata changes credited BTC wrapper', () => {
|
||||||
|
const previous = correlateFundingObservation({
|
||||||
|
accountId: 'solver.near',
|
||||||
|
assetId: 'nep141:nbtc.bridge.near',
|
||||||
|
chain: 'btc:mainnet',
|
||||||
|
fundingHandle: 'bc1qexample',
|
||||||
|
source: 'btc_mempool_space',
|
||||||
|
txHash: 'btc-tx-1',
|
||||||
|
amount: '1500',
|
||||||
|
confirmations: 1,
|
||||||
|
observedAt: '2026-05-07T13:00:00.000Z',
|
||||||
|
});
|
||||||
|
const next = {
|
||||||
|
...previous,
|
||||||
|
asset_id: 'nep141:btc.omft.near',
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(hasFundingObservationChanged(previous, next), true);
|
||||||
|
});
|
||||||
|
|
||||||
test('history writer routes funding observations into the funding table family', () => {
|
test('history writer routes funding observations into the funding table family', () => {
|
||||||
const routed = routeHistoryRecord({
|
const routed = routeHistoryRecord({
|
||||||
topic: 'ops.funding_observation',
|
topic: 'ops.funding_observation',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ import { buildBridgeWithdrawalPlan } from '../src/core/liquidity-withdrawals.mjs
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
assetRegistry: new Map([
|
assetRegistry: new Map([
|
||||||
|
['nep141:nbtc.bridge.near', {
|
||||||
|
assetId: 'nep141:nbtc.bridge.near',
|
||||||
|
chain: 'btc:mainnet',
|
||||||
|
withdrawAddress: '',
|
||||||
|
}],
|
||||||
['nep141:btc.omft.near', {
|
['nep141:btc.omft.near', {
|
||||||
assetId: 'nep141:btc.omft.near',
|
assetId: 'nep141:btc.omft.near',
|
||||||
chain: 'btc:mainnet',
|
chain: 'btc:mainnet',
|
||||||
|
|
@ -48,6 +53,33 @@ test('buildBridgeWithdrawalPlan creates an external-chain EURe withdrawal plan',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildBridgeWithdrawalPlan distinguishes nBTC and legacy BTC bridge tokens', () => {
|
||||||
|
const plan = buildBridgeWithdrawalPlan({
|
||||||
|
assetId: 'nep141:nbtc.bridge.near',
|
||||||
|
amount: '10000',
|
||||||
|
destinationAddress: 'bc1qexample',
|
||||||
|
supportedTokens: {
|
||||||
|
legacyBtc: {
|
||||||
|
near_token_id: 'btc.omft.near',
|
||||||
|
defuse_asset_identifier: 'btc:mainnet:native',
|
||||||
|
min_withdrawal_amount: '700',
|
||||||
|
withdrawal_fee: '1500',
|
||||||
|
},
|
||||||
|
nbtc: {
|
||||||
|
near_token_id: 'nbtc.bridge.near',
|
||||||
|
defuse_asset_identifier: 'btc:mainnet:native',
|
||||||
|
min_withdrawal_amount: '700',
|
||||||
|
withdrawal_fee: '1500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(plan.asset_id, 'nep141:nbtc.bridge.near');
|
||||||
|
assert.equal(plan.near_token_id, 'nbtc.bridge.near');
|
||||||
|
assert.equal(plan.defuse_asset_identifier, 'btc:mainnet:native');
|
||||||
|
});
|
||||||
|
|
||||||
test('buildBridgeWithdrawalPlan rejects amounts below the bridge minimum', () => {
|
test('buildBridgeWithdrawalPlan rejects amounts below the bridge minimum', () => {
|
||||||
assert.throws(() => buildBridgeWithdrawalPlan({
|
assert.throws(() => buildBridgeWithdrawalPlan({
|
||||||
assetId: 'nep141:btc.omft.near',
|
assetId: 'nep141:btc.omft.near',
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,43 @@ function buildConfig() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDualBtcConfig() {
|
||||||
|
const tradingBtc = {
|
||||||
|
assetId: 'nep141:nbtc.bridge.near',
|
||||||
|
symbol: 'BTC',
|
||||||
|
label: 'BTC / nBTC reserve',
|
||||||
|
decimals: 8,
|
||||||
|
chain: 'btc:mainnet',
|
||||||
|
};
|
||||||
|
const legacyBtc = {
|
||||||
|
assetId: 'nep141:btc.omft.near',
|
||||||
|
symbol: 'BTC',
|
||||||
|
label: 'BTC / legacy OMFT',
|
||||||
|
decimals: 8,
|
||||||
|
chain: 'btc:mainnet',
|
||||||
|
};
|
||||||
|
const tradingEure = {
|
||||||
|
assetId: 'nep141:eure.omft.near',
|
||||||
|
symbol: 'EURe',
|
||||||
|
decimals: 18,
|
||||||
|
chain: 'eth:100',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...buildConfig(),
|
||||||
|
activePair: `${tradingBtc.assetId}->${tradingEure.assetId}`,
|
||||||
|
tradingBtc,
|
||||||
|
tradingBtcAssets: [tradingBtc, legacyBtc],
|
||||||
|
tradingEure,
|
||||||
|
trackedAssets: [tradingBtc, legacyBtc, tradingEure],
|
||||||
|
assetRegistry: new Map([
|
||||||
|
[tradingBtc.assetId, tradingBtc],
|
||||||
|
[legacyBtc.assetId, legacyBtc],
|
||||||
|
[tradingEure.assetId, tradingEure],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test('profitability summary separates baseline, hold, market move, and portfolio comparison', () => {
|
test('profitability summary separates baseline, hold, market move, and portfolio comparison', () => {
|
||||||
const summary = buildProfitabilitySummary({
|
const summary = buildProfitabilitySummary({
|
||||||
metric: {
|
metric: {
|
||||||
|
|
@ -752,6 +789,64 @@ test('bootstrap aggregation keeps Funds as default and carries live control stat
|
||||||
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].reason_code, 'strategy_disarmed');
|
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].reason_code, 'strategy_disarmed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('bootstrap balances and funding handles distinguish nBTC reserve from legacy BTC', () => {
|
||||||
|
const config = buildDualBtcConfig();
|
||||||
|
const bootstrap = buildDashboardBootstrap({
|
||||||
|
config,
|
||||||
|
inventorySnapshot: {
|
||||||
|
ingested_at: '2026-05-07T13:45:00.000Z',
|
||||||
|
payload: {
|
||||||
|
synced_at: '2026-05-07T13:45:00.000Z',
|
||||||
|
reconciliation_status: 'ok',
|
||||||
|
spendable: {
|
||||||
|
[config.tradingBtc.assetId]: '100000',
|
||||||
|
'nep141:btc.omft.near': '201051',
|
||||||
|
[config.tradingEure.assetId]: '0',
|
||||||
|
},
|
||||||
|
pending_inbound: {},
|
||||||
|
pending_outbound: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marketPrice: {
|
||||||
|
payload: {
|
||||||
|
observed_at: '2026-05-07T13:45:00.000Z',
|
||||||
|
eure_per_btc: '50000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recentQuotes: [],
|
||||||
|
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
|
||||||
|
submissionSummary: { total: 0 },
|
||||||
|
fundingObservations: [],
|
||||||
|
recentTradeDecisions: [],
|
||||||
|
recentExecuteTradeCommands: [],
|
||||||
|
recentExecutionResults: [],
|
||||||
|
recentAlertTransitions: [],
|
||||||
|
serviceSnapshots: [{
|
||||||
|
service: 'liquidity-manager',
|
||||||
|
state: {
|
||||||
|
withdrawals_frozen: true,
|
||||||
|
deposit_addresses: {
|
||||||
|
'btc:mainnet': {
|
||||||
|
address: 'bc1qdeposit',
|
||||||
|
refreshed_at: '2026-05-07T13:44:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(bootstrap.funds.balances.items.map((item) => item.label), [
|
||||||
|
'BTC / nBTC reserve',
|
||||||
|
'BTC / legacy OMFT',
|
||||||
|
'EURe',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(bootstrap.funds.funding.handles[0].asset_ids, [
|
||||||
|
'nep141:nbtc.bridge.near',
|
||||||
|
'nep141:btc.omft.near',
|
||||||
|
]);
|
||||||
|
assert.equal(bootstrap.funds.funding.handles[0].label, 'btc:mainnet funding handle');
|
||||||
|
});
|
||||||
|
|
||||||
test('bootstrap normalizes actionable decision vocabulary before exposing it to the dashboard', () => {
|
test('bootstrap normalizes actionable decision vocabulary before exposing it to the dashboard', () => {
|
||||||
const config = buildConfig();
|
const config = buildConfig();
|
||||||
const bootstrap = buildDashboardBootstrap({
|
const bootstrap = buildDashboardBootstrap({
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ const btcAsset = {
|
||||||
decimals: 8,
|
decimals: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nbtcAsset = {
|
||||||
|
assetId: 'nep141:nbtc.bridge.near',
|
||||||
|
symbol: 'BTC',
|
||||||
|
label: 'BTC / nBTC reserve',
|
||||||
|
decimals: 8,
|
||||||
|
};
|
||||||
|
|
||||||
const eureAsset = {
|
const eureAsset = {
|
||||||
assetId: 'nep141:eure.omft.near',
|
assetId: 'nep141:eure.omft.near',
|
||||||
symbol: 'EURe',
|
symbol: 'EURe',
|
||||||
|
|
@ -97,6 +104,37 @@ test('portfolio metrics stay available before the first live execution', () => {
|
||||||
assert.equal(metric.price_move_pnl_eure, null);
|
assert.equal(metric.price_move_pnl_eure, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('portfolio metrics value both tracked BTC wrappers as BTC-equivalent inventory', () => {
|
||||||
|
const metric = computePortfolioMetric({
|
||||||
|
baseline: null,
|
||||||
|
currentInventory: {
|
||||||
|
inventory_id: 'current-dual-btc',
|
||||||
|
synced_at: '2026-05-07T13:45:00.000Z',
|
||||||
|
spendable: {
|
||||||
|
'nep141:nbtc.bridge.near': '100000',
|
||||||
|
'nep141:btc.omft.near': '201051',
|
||||||
|
'nep141:eure.omft.near': '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentPrice: {
|
||||||
|
price_id: 'price-dual-btc',
|
||||||
|
observed_at: '2026-05-07T13:45:00.000Z',
|
||||||
|
eure_per_btc: '50000',
|
||||||
|
},
|
||||||
|
btcAsset: nbtcAsset,
|
||||||
|
btcAssets: [nbtcAsset, btcAsset],
|
||||||
|
eureAsset,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(metric.current_inventory.btc_units, '301051');
|
||||||
|
assert.equal(metric.current_inventory.btc, '0.00301051');
|
||||||
|
assert.equal(metric.current_portfolio_value_eure, '150.5255');
|
||||||
|
assert.deepEqual(metric.current_inventory.btc_assets.map((asset) => asset.asset_id), [
|
||||||
|
'nep141:nbtc.bridge.near',
|
||||||
|
'nep141:btc.omft.near',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => {
|
test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => {
|
||||||
const metric = computePortfolioMetric({
|
const metric = computePortfolioMetric({
|
||||||
baseline: {
|
baseline: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue