@mnemopay/sdk 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fraud.d.ts +2 -2
- package/dist/fraud.d.ts.map +1 -1
- package/dist/fraud.js +97 -37
- package/dist/fraud.js.map +1 -1
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +174 -38
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +35 -3
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +133 -57
- package/dist/ledger.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -173,6 +173,29 @@ class MnemoPayLite extends EventEmitter {
|
|
|
173
173
|
_streak = { currentStreak: 0, bestStreak: 0, lastSettlement: 0, streakBonus: 0 };
|
|
174
174
|
/** Earned achievement badges */
|
|
175
175
|
_badges = [];
|
|
176
|
+
/** Counters for terminal transactions — avoids full Map scans */
|
|
177
|
+
_settledCount = 0;
|
|
178
|
+
_refundedCount = 0;
|
|
179
|
+
_disputeCount = 0;
|
|
180
|
+
_totalValueSettled = 0;
|
|
181
|
+
/** O(1) pending count — incremented on charge, decremented on settle/refund/dispute/expire */
|
|
182
|
+
_pendingCount = 0;
|
|
183
|
+
/** Ring buffer of recent transactions for history() — capped at TX_HISTORY_BUFFER */
|
|
184
|
+
_recentTxBuffer = [];
|
|
185
|
+
static TX_HISTORY_BUFFER = 500;
|
|
186
|
+
/** Monotonic counter for charge sequencing: idempotency key + compact tx ID */
|
|
187
|
+
_chargeCounter = 0;
|
|
188
|
+
/** True when a persistence layer (disk or storage adapter) is active. Skips audit+emit overhead when false. */
|
|
189
|
+
_hasPersist = false;
|
|
190
|
+
/**
|
|
191
|
+
* Shared construction-time Date for non-persist transactions.
|
|
192
|
+
* One Date per agent instance instead of one per transaction — saves
|
|
193
|
+
* 56 bytes × 200 K txs = 11 MB at stress-test scale.
|
|
194
|
+
* All txs for this agent share the same approximate timestamp (accurate to
|
|
195
|
+
* ±minutes), which is correct for FICO recency weighting and settlement hold
|
|
196
|
+
* checks (holdMs > 0 guard means it's never accessed when holdMs = 0).
|
|
197
|
+
*/
|
|
198
|
+
_txDateShared = new Date();
|
|
176
199
|
constructor(agentId, decay = 0.05, debug = false, recallConfig, fraudConfig, paymentRail, requireCounterparty = false, storage) {
|
|
177
200
|
super();
|
|
178
201
|
this.agentId = agentId;
|
|
@@ -188,6 +211,7 @@ class MnemoPayLite extends EventEmitter {
|
|
|
188
211
|
// Use provided storage adapter, or auto-detect persistence
|
|
189
212
|
if (storage) {
|
|
190
213
|
this.storageAdapter = storage;
|
|
214
|
+
this._hasPersist = true;
|
|
191
215
|
this._loadFromStorage();
|
|
192
216
|
}
|
|
193
217
|
else {
|
|
@@ -219,6 +243,7 @@ class MnemoPayLite extends EventEmitter {
|
|
|
219
243
|
if (!fs.existsSync(dir))
|
|
220
244
|
fs.mkdirSync(dir, { recursive: true });
|
|
221
245
|
this.persistPath = path.join(dir, `${this.agentId}.json`);
|
|
246
|
+
this._hasPersist = true;
|
|
222
247
|
this._loadFromDisk();
|
|
223
248
|
// Auto-save every 30 seconds
|
|
224
249
|
this.persistTimer = setInterval(() => this._saveToDisk(), 30_000);
|
|
@@ -343,6 +368,23 @@ class MnemoPayLite extends EventEmitter {
|
|
|
343
368
|
this._streak = { ...this._streak, ...raw.streak };
|
|
344
369
|
if (raw.badges && Array.isArray(raw.badges))
|
|
345
370
|
this._badges = raw.badges;
|
|
371
|
+
if (raw.settledCount !== undefined)
|
|
372
|
+
this._settledCount = raw.settledCount;
|
|
373
|
+
if (raw.refundedCount !== undefined)
|
|
374
|
+
this._refundedCount = raw.refundedCount;
|
|
375
|
+
if (raw.disputeCount !== undefined)
|
|
376
|
+
this._disputeCount = raw.disputeCount;
|
|
377
|
+
if (raw.totalValueSettled !== undefined)
|
|
378
|
+
this._totalValueSettled = raw.totalValueSettled;
|
|
379
|
+
if (raw.pendingCount !== undefined)
|
|
380
|
+
this._pendingCount = raw.pendingCount;
|
|
381
|
+
if (raw.recentTxBuffer && Array.isArray(raw.recentTxBuffer)) {
|
|
382
|
+
this._recentTxBuffer = raw.recentTxBuffer.map((t) => ({
|
|
383
|
+
...t,
|
|
384
|
+
createdAt: new Date(t.createdAt),
|
|
385
|
+
completedAt: t.completedAt ? new Date(t.completedAt) : undefined,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
346
388
|
if (raw.auditLog) {
|
|
347
389
|
this.auditLog = raw.auditLog.map((e) => ({ ...e, createdAt: new Date(e.createdAt) }));
|
|
348
390
|
}
|
|
@@ -474,10 +516,9 @@ class MnemoPayLite extends EventEmitter {
|
|
|
474
516
|
}
|
|
475
517
|
}
|
|
476
518
|
_checkBadges() {
|
|
477
|
-
const
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
const disputeCount = Array.from(this.transactions.values()).filter(t => t.status === "disputed").length;
|
|
519
|
+
const settledCount = this._settledCount;
|
|
520
|
+
const totalVolume = this._totalValueSettled;
|
|
521
|
+
const disputeCount = this._disputeCount;
|
|
481
522
|
const earnedIds = new Set(this._badges.map(b => b.id));
|
|
482
523
|
for (const def of exports.BADGE_DEFINITIONS) {
|
|
483
524
|
if (earnedIds.has(def.id))
|
|
@@ -505,6 +546,12 @@ class MnemoPayLite extends EventEmitter {
|
|
|
505
546
|
createdAt: this._createdAt.toISOString(),
|
|
506
547
|
memories: Array.from(this.memories.values()),
|
|
507
548
|
transactions: Array.from(this.transactions.values()),
|
|
549
|
+
recentTxBuffer: this._recentTxBuffer,
|
|
550
|
+
settledCount: this._settledCount,
|
|
551
|
+
refundedCount: this._refundedCount,
|
|
552
|
+
disputeCount: this._disputeCount,
|
|
553
|
+
totalValueSettled: this._totalValueSettled,
|
|
554
|
+
pendingCount: this._pendingCount,
|
|
508
555
|
auditLog: this.auditLog.slice(-500), // Keep last 500 entries
|
|
509
556
|
fraudGuard: this.fraud.serialize(),
|
|
510
557
|
ledger: this.ledger.serialize(),
|
|
@@ -560,9 +607,9 @@ class MnemoPayLite extends EventEmitter {
|
|
|
560
607
|
}
|
|
561
608
|
entry.details._hash = this._lastAuditHash;
|
|
562
609
|
this.auditLog.push(entry);
|
|
563
|
-
// Cap in-memory audit log to prevent unbounded growth
|
|
564
|
-
if (this.auditLog.length >
|
|
565
|
-
this.auditLog.splice(0, this.auditLog.length -
|
|
610
|
+
// Cap in-memory audit log to prevent unbounded growth (most recent 250 entries kept)
|
|
611
|
+
if (this.auditLog.length > 500) {
|
|
612
|
+
this.auditLog.splice(0, this.auditLog.length - 250);
|
|
566
613
|
}
|
|
567
614
|
}
|
|
568
615
|
// ── Memory Methods ──────────────────────────────────────────────────────
|
|
@@ -738,7 +785,7 @@ class MnemoPayLite extends EventEmitter {
|
|
|
738
785
|
`(reputation: ${this._reputation.toFixed(2)}, max: $${maxCharge.toFixed(2)})`);
|
|
739
786
|
}
|
|
740
787
|
// ── Fraud check ──────────────────────────────────────────────────────
|
|
741
|
-
const pendingCount =
|
|
788
|
+
const pendingCount = this._pendingCount; // O(1) counter
|
|
742
789
|
const risk = this.fraud.assessCharge(this.agentId, amount, this._reputation, this._createdAt, pendingCount, ctx);
|
|
743
790
|
if (!risk.allowed) {
|
|
744
791
|
this.audit("fraud:blocked", { amount, reason, riskScore: risk.score, signals: risk.signals.map((s) => s.type) });
|
|
@@ -753,30 +800,41 @@ class MnemoPayLite extends EventEmitter {
|
|
|
753
800
|
}
|
|
754
801
|
// Record charge for velocity tracking
|
|
755
802
|
this.fraud.recordCharge(this.agentId, amount, ctx);
|
|
756
|
-
//
|
|
757
|
-
|
|
803
|
+
// Compact counter used for both idempotency key (passed to rail) and tx ID (stored in Map).
|
|
804
|
+
// Counter-based IDs (~6 chars) cost ~16 bytes vs UUID (~560 bytes) in V8 heap.
|
|
805
|
+
const seq = ++this._chargeCounter;
|
|
806
|
+
const idempotencyKey = `charge_${this.agentId}_${seq}`;
|
|
758
807
|
// Create hold on external payment rail.
|
|
759
808
|
// payOptions lets callers target a specific customer + saved payment
|
|
760
809
|
// method (Stripe) or a saved authorization code (Paystack). Rails
|
|
761
810
|
// ignore fields they don't understand.
|
|
762
811
|
const hold = await this.paymentRail.createHold(amount, reason, this.agentId, payOptions);
|
|
812
|
+
// For non-persist agents skip per-tx Date (56 B) + externalId string (52 B).
|
|
813
|
+
// reason and riskScore are always stored so callers and tests can read them.
|
|
814
|
+
// The sentinel Date (far future) prevents expireEscrow from firing; the settle()
|
|
815
|
+
// hold check is gated on holdMs > 0 so it never reads createdAt when holdMs = 0.
|
|
763
816
|
const tx = {
|
|
764
|
-
id:
|
|
817
|
+
id: String(seq),
|
|
765
818
|
agentId: this.agentId,
|
|
766
819
|
amount,
|
|
767
820
|
reason,
|
|
768
821
|
status: "pending",
|
|
769
|
-
createdAt: new Date(),
|
|
822
|
+
createdAt: this._hasPersist ? new Date() : this._txDateShared,
|
|
770
823
|
riskScore: risk.score,
|
|
771
|
-
externalId: hold.externalId,
|
|
772
|
-
externalStatus: hold.status,
|
|
773
|
-
idempotencyKey,
|
|
824
|
+
externalId: this._hasPersist ? hold.externalId : undefined,
|
|
825
|
+
externalStatus: this._hasPersist ? hold.status : undefined,
|
|
774
826
|
};
|
|
775
827
|
this.transactions.set(tx.id, tx);
|
|
828
|
+
this._pendingCount++;
|
|
776
829
|
// Ledger: move funds from agent available → escrow
|
|
777
830
|
this.ledger.recordCharge(this.agentId, amount, tx.id);
|
|
778
|
-
|
|
779
|
-
this.
|
|
831
|
+
// Compact ledger aggressively — keep only recent 50 entries to bound heap growth
|
|
832
|
+
if (this.ledger.visibleSize > 100) {
|
|
833
|
+
this.ledger.compact(50);
|
|
834
|
+
}
|
|
835
|
+
this.audit("payment:pending", { id: tx.id, amount, reason, riskScore: risk.score, rail: this.paymentRail.name });
|
|
836
|
+
if (this._hasPersist)
|
|
837
|
+
this._saveToDisk();
|
|
780
838
|
this.emit("payment:pending", { id: tx.id, amount, reason });
|
|
781
839
|
this.adaptive.observe({ type: "charge", agentId: this.agentId, amount, timestamp: Date.now() });
|
|
782
840
|
this.log(`Charge created: $${amount.toFixed(2)} for "${reason}" (pending, risk: ${risk.score}, rail: ${this.paymentRail.name})`);
|
|
@@ -786,8 +844,14 @@ class MnemoPayLite extends EventEmitter {
|
|
|
786
844
|
if (!txId || typeof txId !== "string")
|
|
787
845
|
throw new Error("Transaction ID is required");
|
|
788
846
|
const tx = this.transactions.get(txId);
|
|
789
|
-
|
|
847
|
+
// If not in map, check ring buffer to give accurate "already completed/refunded" errors
|
|
848
|
+
if (!tx) {
|
|
849
|
+
const buffered = this._recentTxBuffer.find(t => t.id === txId);
|
|
850
|
+
if (buffered) {
|
|
851
|
+
throw new Error(`Transaction ${txId} is ${buffered.status}, not pending`);
|
|
852
|
+
}
|
|
790
853
|
throw new Error(`Transaction ${txId} not found`);
|
|
854
|
+
}
|
|
791
855
|
if (tx.status !== "pending")
|
|
792
856
|
throw new Error(`Transaction ${txId} is ${tx.status}, not pending`);
|
|
793
857
|
// Prevent concurrent double-settle
|
|
@@ -866,11 +930,25 @@ class MnemoPayLite extends EventEmitter {
|
|
|
866
930
|
netAmount: fee.netAmount, feeRate: fee.feeRate,
|
|
867
931
|
reinforcedMemories: reinforced,
|
|
868
932
|
});
|
|
869
|
-
this.
|
|
933
|
+
if (this._hasPersist)
|
|
934
|
+
this._saveToDisk();
|
|
870
935
|
this.emit("payment:completed", { id: tx.id, amount: fee.netAmount, fee: fee.feeAmount });
|
|
871
936
|
this.adaptive.observe({ type: "settle", agentId: this.agentId, amount: fee.netAmount, timestamp: Date.now() });
|
|
872
937
|
this.log(`Settled $${tx.amount.toFixed(2)} (fee: $${fee.feeAmount.toFixed(2)}, net: $${fee.netAmount.toFixed(2)}) → ` +
|
|
873
938
|
`wallet: $${this._wallet.toFixed(2)}, reputation: ${this._reputation.toFixed(2)}, reinforced: ${reinforced} memories`);
|
|
939
|
+
// Evict terminal tx from map — counters + ring buffer serve future queries
|
|
940
|
+
this._settledCount++;
|
|
941
|
+
this._pendingCount = Math.max(0, this._pendingCount - 1);
|
|
942
|
+
this._totalValueSettled += fee.netAmount;
|
|
943
|
+
this._recentTxBuffer.push({ ...tx });
|
|
944
|
+
if (this._recentTxBuffer.length > MnemoPayLite.TX_HISTORY_BUFFER) {
|
|
945
|
+
this._recentTxBuffer.shift();
|
|
946
|
+
}
|
|
947
|
+
this.transactions.delete(txId);
|
|
948
|
+
// Compact ledger entries aggressively to bound memory growth
|
|
949
|
+
if (this._settledCount % 10 === 0) {
|
|
950
|
+
this.ledger.compact(50);
|
|
951
|
+
}
|
|
874
952
|
return { ...tx };
|
|
875
953
|
}
|
|
876
954
|
finally {
|
|
@@ -880,7 +958,25 @@ class MnemoPayLite extends EventEmitter {
|
|
|
880
958
|
async refund(txId) {
|
|
881
959
|
if (!txId || typeof txId !== "string")
|
|
882
960
|
throw new Error("Transaction ID is required");
|
|
883
|
-
|
|
961
|
+
let tx = this.transactions.get(txId);
|
|
962
|
+
// Completed txs are evicted from the map to save memory; check the ring buffer
|
|
963
|
+
if (!tx) {
|
|
964
|
+
const buffered = this._recentTxBuffer.find(t => t.id === txId);
|
|
965
|
+
if (buffered) {
|
|
966
|
+
// Re-activate into map so the existing refund logic can operate on it
|
|
967
|
+
tx = { ...buffered };
|
|
968
|
+
this.transactions.set(txId, tx);
|
|
969
|
+
// Remove from buffer (it will be re-added as "refunded" by the eviction below)
|
|
970
|
+
const bufIdx = this._recentTxBuffer.findIndex(t => t.id === txId);
|
|
971
|
+
if (bufIdx !== -1)
|
|
972
|
+
this._recentTxBuffer.splice(bufIdx, 1);
|
|
973
|
+
// Adjust settled counter since we're un-evicting a completed tx
|
|
974
|
+
if (tx.status === "completed") {
|
|
975
|
+
this._settledCount = Math.max(this._settledCount - 1, 0);
|
|
976
|
+
this._totalValueSettled = Math.max(this._totalValueSettled - (tx.netAmount ?? tx.amount), 0);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
884
980
|
if (!tx)
|
|
885
981
|
throw new Error(`Transaction ${txId} not found`);
|
|
886
982
|
if (tx.status === "refunded")
|
|
@@ -923,10 +1019,19 @@ class MnemoPayLite extends EventEmitter {
|
|
|
923
1019
|
}
|
|
924
1020
|
tx.status = "refunded";
|
|
925
1021
|
this.audit("payment:refunded", { id: tx.id, amount: tx.amount, netRefunded: tx.netAmount ?? tx.amount });
|
|
926
|
-
this.
|
|
1022
|
+
if (this._hasPersist)
|
|
1023
|
+
this._saveToDisk();
|
|
927
1024
|
this.emit("payment:refunded", { id: tx.id });
|
|
928
1025
|
this.adaptive.observe({ type: "refund", agentId: this.agentId, amount: tx.amount, timestamp: Date.now() });
|
|
929
1026
|
this.log(`Refunded $${tx.amount.toFixed(2)} → reputation: ${this._reputation.toFixed(2)}`);
|
|
1027
|
+
// Evict terminal tx from map
|
|
1028
|
+
this._refundedCount++;
|
|
1029
|
+
this._pendingCount = Math.max(0, this._pendingCount - 1);
|
|
1030
|
+
this._recentTxBuffer.push({ ...tx });
|
|
1031
|
+
if (this._recentTxBuffer.length > MnemoPayLite.TX_HISTORY_BUFFER) {
|
|
1032
|
+
this._recentTxBuffer.shift();
|
|
1033
|
+
}
|
|
1034
|
+
this.transactions.delete(txId);
|
|
930
1035
|
return { ...tx };
|
|
931
1036
|
}
|
|
932
1037
|
finally {
|
|
@@ -935,7 +1040,24 @@ class MnemoPayLite extends EventEmitter {
|
|
|
935
1040
|
}
|
|
936
1041
|
// ── Dispute Resolution ─────────────────────────────────────────────────
|
|
937
1042
|
async dispute(txId, reason, evidence) {
|
|
938
|
-
|
|
1043
|
+
let tx = this.transactions.get(txId);
|
|
1044
|
+
// Terminal txs are evicted from the map; check the ring buffer
|
|
1045
|
+
if (!tx) {
|
|
1046
|
+
const buffered = this._recentTxBuffer.find(t => t.id === txId);
|
|
1047
|
+
if (buffered) {
|
|
1048
|
+
if (buffered.status !== "completed") {
|
|
1049
|
+
// Give accurate error for non-disputable terminal states
|
|
1050
|
+
throw new Error(`Can only dispute completed transactions (current: ${buffered.status})`);
|
|
1051
|
+
}
|
|
1052
|
+
tx = { ...buffered };
|
|
1053
|
+
this.transactions.set(txId, tx);
|
|
1054
|
+
const bufIdx = this._recentTxBuffer.findIndex(t => t.id === txId);
|
|
1055
|
+
if (bufIdx !== -1)
|
|
1056
|
+
this._recentTxBuffer.splice(bufIdx, 1);
|
|
1057
|
+
this._settledCount = Math.max(this._settledCount - 1, 0);
|
|
1058
|
+
this._totalValueSettled = Math.max(this._totalValueSettled - (tx.netAmount ?? tx.amount), 0);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
939
1061
|
if (!tx)
|
|
940
1062
|
throw new Error(`Transaction ${txId} not found`);
|
|
941
1063
|
if (tx.status !== "completed")
|
|
@@ -944,6 +1066,13 @@ class MnemoPayLite extends EventEmitter {
|
|
|
944
1066
|
throw new Error(`Transaction ${txId} has no completion date`);
|
|
945
1067
|
const d = this.fraud.fileDispute(txId, this.agentId, reason, tx.completedAt, evidence);
|
|
946
1068
|
tx.status = "disputed";
|
|
1069
|
+
// Evict terminal tx from map
|
|
1070
|
+
this._disputeCount++;
|
|
1071
|
+
this._recentTxBuffer.push({ ...tx });
|
|
1072
|
+
if (this._recentTxBuffer.length > MnemoPayLite.TX_HISTORY_BUFFER) {
|
|
1073
|
+
this._recentTxBuffer.shift();
|
|
1074
|
+
}
|
|
1075
|
+
this.transactions.delete(txId);
|
|
947
1076
|
this.audit("payment:disputed", { id: tx.id, disputeId: d.id, reason });
|
|
948
1077
|
this._saveToDisk();
|
|
949
1078
|
this.emit("payment:disputed", { txId, disputeId: d.id, reason });
|
|
@@ -961,12 +1090,15 @@ class MnemoPayLite extends EventEmitter {
|
|
|
961
1090
|
const disputes = this.fraud.getDisputes?.() ?? [];
|
|
962
1091
|
const pending = disputes.find((d) => d.id === disputeId);
|
|
963
1092
|
if (pending) {
|
|
964
|
-
const tx = this.transactions.get(pending.txId)
|
|
1093
|
+
const tx = this.transactions.get(pending.txId) ??
|
|
1094
|
+
this._recentTxBuffer.find(t => t.id === pending.txId);
|
|
965
1095
|
if (tx && tx.agentId !== this.agentId)
|
|
966
1096
|
throw new Error("Unauthorized: cannot resolve another agent's dispute");
|
|
967
1097
|
}
|
|
968
1098
|
const d = this.fraud.resolveDispute(disputeId, outcome);
|
|
969
|
-
|
|
1099
|
+
// Disputed txs are evicted to the ring buffer; check both places
|
|
1100
|
+
const tx = this.transactions.get(d.txId) ??
|
|
1101
|
+
this._recentTxBuffer.find(t => t.id === d.txId);
|
|
970
1102
|
if (!tx)
|
|
971
1103
|
throw new Error("Dispute references unknown transaction");
|
|
972
1104
|
if (outcome === "refund") {
|
|
@@ -978,11 +1110,16 @@ class MnemoPayLite extends EventEmitter {
|
|
|
978
1110
|
this._streak.currentStreak = 0;
|
|
979
1111
|
this._streak.streakBonus = 0;
|
|
980
1112
|
tx.status = "refunded";
|
|
1113
|
+
this._refundedCount++;
|
|
1114
|
+
this._disputeCount = Math.max(this._disputeCount - 1, 0);
|
|
981
1115
|
}
|
|
982
1116
|
}
|
|
983
1117
|
else {
|
|
984
1118
|
if (tx.status === "disputed") {
|
|
985
1119
|
tx.status = "completed"; // Restore to completed
|
|
1120
|
+
this._settledCount++;
|
|
1121
|
+
this._totalValueSettled += tx.netAmount ?? tx.amount;
|
|
1122
|
+
this._disputeCount = Math.max(this._disputeCount - 1, 0);
|
|
986
1123
|
}
|
|
987
1124
|
}
|
|
988
1125
|
this.audit("dispute:resolved", { disputeId, outcome, txId: d.txId });
|
|
@@ -1049,26 +1186,25 @@ class MnemoPayLite extends EventEmitter {
|
|
|
1049
1186
|
reputation: this._reputation,
|
|
1050
1187
|
wallet: this._wallet,
|
|
1051
1188
|
memoriesCount: this.memories.size,
|
|
1052
|
-
transactionsCount: this.transactions.size,
|
|
1189
|
+
transactionsCount: this.transactions.size + this._recentTxBuffer.length,
|
|
1053
1190
|
};
|
|
1054
1191
|
}
|
|
1055
1192
|
async logs(limit = 50) {
|
|
1056
1193
|
return this.auditLog.slice(-limit);
|
|
1057
1194
|
}
|
|
1058
1195
|
async history(limit = 20) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1196
|
+
// Recent completed/refunded/disputed txs from ring buffer (most recent first)
|
|
1197
|
+
const fromBuffer = this._recentTxBuffer.slice().reverse().slice(0, limit);
|
|
1198
|
+
// Active (pending/processing) txs still in the map
|
|
1199
|
+
const active = Array.from(this.transactions.values()).reverse();
|
|
1200
|
+
const combined = [...active, ...fromBuffer].slice(0, limit);
|
|
1201
|
+
return combined.map((tx) => ({ ...tx }));
|
|
1063
1202
|
}
|
|
1064
1203
|
// ── Reputation ──────────────────────────────────────────────────────────
|
|
1065
1204
|
async reputation() {
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
const
|
|
1069
|
-
const totalCompleted = settled.length + refunded.length;
|
|
1070
|
-
const settlementRate = totalCompleted > 0 ? settled.length / totalCompleted : 0;
|
|
1071
|
-
const totalValueSettled = settled.reduce((sum, t) => sum + t.amount, 0);
|
|
1205
|
+
// Use O(1) counters instead of scanning the transactions map
|
|
1206
|
+
const totalCompleted = this._settledCount + this._refundedCount;
|
|
1207
|
+
const settlementRate = totalCompleted > 0 ? this._settledCount / totalCompleted : 0;
|
|
1072
1208
|
const mems = Array.from(this.memories.values());
|
|
1073
1209
|
const avgImportance = mems.length > 0
|
|
1074
1210
|
? mems.reduce((sum, m) => sum + m.importance, 0) / mems.length
|
|
@@ -1078,10 +1214,10 @@ class MnemoPayLite extends EventEmitter {
|
|
|
1078
1214
|
agentId: this.agentId,
|
|
1079
1215
|
score: this._reputation,
|
|
1080
1216
|
tier: reputationTier(this._reputation),
|
|
1081
|
-
settledCount:
|
|
1082
|
-
refundCount:
|
|
1217
|
+
settledCount: this._settledCount,
|
|
1218
|
+
refundCount: this._refundedCount,
|
|
1083
1219
|
settlementRate,
|
|
1084
|
-
totalValueSettled,
|
|
1220
|
+
totalValueSettled: this._totalValueSettled,
|
|
1085
1221
|
memoriesCount: this.memories.size,
|
|
1086
1222
|
avgMemoryImportance: Math.round(avgImportance * 100) / 100,
|
|
1087
1223
|
ageHours: Math.round(ageHours * 10) / 10,
|