From 69e5053dd346e54c2fb4124010ec8572edcaa704 Mon Sep 17 00:00:00 2001 From: Chin Kai Ze Date: Sun, 30 Aug 2026 14:11:30 +0800 Subject: [PATCH] fix: align OKX transaction normalization semantics --- lib/financial-context.ts | 8 ++- lib/types.ts | 2 +- package-lock.json | 4 +- package.json | 2 +- tests/okx-normalization.test.ts | 97 +++++++++++++++++++++++++++++++++ tests/upgrade.test.ts | 2 +- 6 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 tests/okx-normalization.test.ts diff --git a/lib/financial-context.ts b/lib/financial-context.ts index d7ee532..a0eaf52 100644 --- a/lib/financial-context.ts +++ b/lib/financial-context.ts @@ -176,7 +176,9 @@ export function normalizeTransactionState(options: { } const failedProviders = new Map(); - for (const transaction of relevantTransactions.filter((item) => item.status === "failed")) { + for (const transaction of relevantTransactions.filter( + (item) => item.status === "failed" && item.direction === "outgoing", + )) { const key = transaction.counterparty.trim().toLowerCase(); const existing = failedProviders.get(key); if (existing) { @@ -198,8 +200,8 @@ export function normalizeTransactionState(options: { periodDays: options.periodDays, inflows: successfulIncoming.reduce((sum, transaction) => sum + transaction.amount, 0), outflows: successfulOutgoing.reduce((sum, transaction) => sum + transaction.amount, 0), - incomingTransactions: relevantTransactions.filter((transaction) => transaction.direction === "incoming").length, - outgoingTransactions: relevantTransactions.filter((transaction) => transaction.direction === "outgoing").length, + incomingTransactions: successfulIncoming.length, + outgoingTransactions: successfulOutgoing.length, failedTransactions: relevantTransactions.filter((transaction) => transaction.status === "failed").length, firstHalfSpend, secondHalfSpend, diff --git a/lib/types.ts b/lib/types.ts index 1f938ab..c0fe7df 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,5 @@ export const APP_NAME = "AgentLedger" as const; -export const APP_VERSION = "0.3.3" as const; +export const APP_VERSION = "0.3.4" as const; export const SUPPORTED_ASSETS = ["USDT", "USDC", "USDG"] as const; diff --git a/package-lock.json b/package-lock.json index d7e7c75..f5e6f80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agentledger", - "version": "0.3.3", + "version": "0.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentledger", - "version": "0.3.3", + "version": "0.3.4", "dependencies": { "@okxweb3/x402-core": "^0.1.0", "@okxweb3/x402-evm": "^0.2.1", diff --git a/package.json b/package.json index 2a0db55..62d5022 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentledger", - "version": "0.3.3", + "version": "0.3.4", "private": true, "description": "The financial control plane for autonomous agents.", "scripts": { diff --git a/tests/okx-normalization.test.ts b/tests/okx-normalization.test.ts new file mode 100644 index 0000000..57871ad --- /dev/null +++ b/tests/okx-normalization.test.ts @@ -0,0 +1,97 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { normalizeFinancialState, normalizeTransactionState } from "../lib/financial-context"; +import type { NormalizedTransaction } from "../lib/types"; + +function transaction(options: { + index: number; + direction: "incoming" | "outgoing"; + status: "success" | "failed"; + amount: number; + counterparty?: string; + timestamp?: number; +}): NormalizedTransaction { + return { + txHash: `0xnormalization${options.index}`, + timestamp: options.timestamp ?? options.index * 10, + symbol: "USDC", + amount: options.amount, + direction: options.direction, + counterparty: options.counterparty ?? `provider-${options.index}`, + status: options.status, + }; +} + +function normalize(transactions: NormalizedTransaction[]) { + return normalizeTransactionState({ transactions, from: 0, to: 100, periodDays: 30 }); +} + +test("failed outgoing transactions are excluded from successful outgoing counts", () => { + const state = normalize([ + transaction({ index: 1, direction: "outgoing", status: "success", amount: 10 }), + transaction({ index: 2, direction: "outgoing", status: "success", amount: 20 }), + transaction({ index: 3, direction: "outgoing", status: "failed", amount: 30 }), + ]); + + assert.equal(state.outgoingTransactions, 2); + assert.equal(state.failedTransactions, 1); + assert.equal(state.totalTransactions, 3); + assert.equal(state.outflows, 30); +}); + +test("failed incoming transactions do not become failed providers", () => { + const state = normalize([ + transaction({ index: 1, direction: "incoming", status: "failed", amount: 50, counterparty: "client-a" }), + ]); + + assert.equal(state.incomingTransactions, 0); + assert.equal(state.outgoingTransactions, 0); + assert.equal(state.failedTransactions, 1); + assert.equal(state.totalTransactions, 1); + assert.deepEqual(state.failedProviders, []); + assert.equal(state.inflows, 0); +}); + +test("failed providers include only failed outgoing attempts", () => { + const state = normalize([ + transaction({ index: 1, direction: "outgoing", status: "failed", amount: 10, counterparty: "Provider-A" }), + transaction({ index: 2, direction: "outgoing", status: "failed", amount: 20, counterparty: "provider-a" }), + transaction({ index: 3, direction: "incoming", status: "failed", amount: 30, counterparty: "Provider-A" }), + ]); + + assert.deepEqual(state.failedProviders, [{ provider: "Provider-A", transactions: 2 }]); +}); + +test("mixed OKX transactions preserve the successful-count invariant", () => { + const state = normalize([ + transaction({ index: 1, direction: "incoming", status: "success", amount: 100 }), + transaction({ index: 2, direction: "outgoing", status: "success", amount: 40, counterparty: "provider-a" }), + transaction({ index: 3, direction: "incoming", status: "failed", amount: 200 }), + transaction({ index: 4, direction: "outgoing", status: "failed", amount: 80, counterparty: "provider-a" }), + ]); + + assert.equal(state.incomingTransactions + state.outgoingTransactions + state.failedTransactions, state.totalTransactions); + assert.equal(state.incomingTransactions, 1); + assert.equal(state.outgoingTransactions, 1); + assert.equal(state.failedTransactions, 2); + assert.equal(state.totalTransactions, 4); + assert.equal(state.inflows, 100); + assert.equal(state.outflows, 40); + assert.deepEqual(state.failedProviders, [{ provider: "provider-a", transactions: 1 }]); +}); + +test("provided-state normalization keeps successful and failed counts separate", () => { + const state = normalizeFinancialState({ + periodDays: 30, + cashFlow: { inflows: 100, outflows: 40, incomingTransactions: 1, outgoingTransactions: 1 }, + providers: [{ provider: "provider-a", spend: 40, transactions: 1, failedTransactions: 1 }], + failedTransactions: 1, + }); + + assert.equal(state.incomingTransactions, 1); + assert.equal(state.outgoingTransactions, 1); + assert.equal(state.failedTransactions, 1); + assert.equal(state.totalTransactions, 3); + assert.deepEqual(state.failedProviders, [{ provider: "provider-a", transactions: 1 }]); +}); diff --git a/tests/upgrade.test.ts b/tests/upgrade.test.ts index 6b733d1..8dd7b1d 100644 --- a/tests/upgrade.test.ts +++ b/tests/upgrade.test.ts @@ -421,7 +421,7 @@ test("recommendations report contains a concise summary and at most three action ], 100), new Date(NOW).toISOString(), ); - assert.equal(result.version, "0.3.3"); + assert.equal(result.version, "0.3.4"); assert.ok(result.executiveSummary.length > 0); assert.ok(result.recommendedActions.length <= 3); });