@umpledger/sdk 2.0.0-alpha.1
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/index.d.mts +839 -0
- package/dist/index.d.ts +839 -0
- package/dist/index.js +1520 -0
- package/dist/index.mjs +1466 -0
- package/examples/multi-agent-marketplace.ts +140 -0
- package/examples/quickstart.ts +107 -0
- package/package.json +58 -0
- package/src/core/agent-manager.ts +200 -0
- package/src/core/audit-trail.ts +91 -0
- package/src/core/index.ts +3 -0
- package/src/core/wallet-manager.ts +257 -0
- package/src/index.ts +188 -0
- package/src/pricing/engine.ts +311 -0
- package/src/pricing/index.ts +3 -0
- package/src/pricing/templates.ts +237 -0
- package/src/settlement/bus.ts +253 -0
- package/src/settlement/index.ts +2 -0
- package/src/terms/contract-manager.ts +142 -0
- package/src/terms/index.ts +2 -0
- package/src/terms/metering.ts +106 -0
- package/src/types.ts +417 -0
- package/src/utils/errors.ts +91 -0
- package/src/utils/id.ts +35 -0
- package/src/utils/index.ts +2 -0
- package/tests/ump.test.ts +525 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Settlement,
|
|
3
|
+
RatedRecord, UsageEvent, PricingRule,
|
|
4
|
+
} from '../types';
|
|
5
|
+
import { generateId, round, hrTimestamp } from '../utils/id';
|
|
6
|
+
import { WalletManager } from '../core/wallet-manager';
|
|
7
|
+
import { AuditTrail } from '../core/audit-trail';
|
|
8
|
+
import { PricingEngine } from '../pricing/engine';
|
|
9
|
+
import { UMPError } from '../utils/errors';
|
|
10
|
+
|
|
11
|
+
export interface SettlementResult {
|
|
12
|
+
settlement: Settlement;
|
|
13
|
+
auditId: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SettlementBus — Layer 3 primitive
|
|
18
|
+
*
|
|
19
|
+
* Executes real-time settlement between agent wallets.
|
|
20
|
+
* Supports 6 patterns: instant drawdown, escrow, waterfall,
|
|
21
|
+
* net settlement, conditional release, cross-currency atomic.
|
|
22
|
+
*/
|
|
23
|
+
export class SettlementBus {
|
|
24
|
+
private settlements: Map<string, Settlement> = new Map();
|
|
25
|
+
private escrows: Map<string, { amount: number; sourceWalletId: string; targetWalletId: string }> = new Map();
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private wallets: WalletManager,
|
|
29
|
+
private audit: AuditTrail,
|
|
30
|
+
private pricingEngine: PricingEngine,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute a full transaction: meter → rate → settle in one call.
|
|
35
|
+
* This is the high-level "transact" method from the Quick Start.
|
|
36
|
+
*/
|
|
37
|
+
async transact(
|
|
38
|
+
sourceAgentId: string,
|
|
39
|
+
targetAgentId: string,
|
|
40
|
+
event: UsageEvent,
|
|
41
|
+
rule: PricingRule,
|
|
42
|
+
): Promise<SettlementResult> {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
|
|
45
|
+
// 1. Rate the usage event
|
|
46
|
+
const ratedRecord = this.pricingEngine.rate(rule, { event });
|
|
47
|
+
|
|
48
|
+
// 2. Execute instant drawdown settlement
|
|
49
|
+
return this.settleInstant(
|
|
50
|
+
sourceAgentId,
|
|
51
|
+
targetAgentId,
|
|
52
|
+
[ratedRecord],
|
|
53
|
+
startTime,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* INSTANT_DRAWDOWN: Debit source, credit target atomically.
|
|
59
|
+
* Used for per-token, per-inference micro-transactions.
|
|
60
|
+
*/
|
|
61
|
+
settleInstant(
|
|
62
|
+
sourceAgentId: string,
|
|
63
|
+
targetAgentId: string,
|
|
64
|
+
ratedRecords: RatedRecord[],
|
|
65
|
+
_startTime?: number,
|
|
66
|
+
): SettlementResult {
|
|
67
|
+
const totalAmount = round(ratedRecords.reduce((sum, r) => sum + r.amount, 0));
|
|
68
|
+
const sourceWallet = this.wallets.getByAgent(sourceAgentId);
|
|
69
|
+
const targetWallet = this.wallets.getByAgent(targetAgentId);
|
|
70
|
+
const settlementId = generateId('stl');
|
|
71
|
+
const txnId = generateId('txn');
|
|
72
|
+
|
|
73
|
+
// Atomic: debit source, credit target
|
|
74
|
+
const balanceBefore = this.wallets.getBalance(sourceWallet.walletId)[0]?.available ?? 0;
|
|
75
|
+
this.wallets.debit(sourceWallet.walletId, totalAmount, targetAgentId, txnId);
|
|
76
|
+
this.wallets.credit(targetWallet.walletId, totalAmount, sourceAgentId, txnId);
|
|
77
|
+
const balanceAfter = this.wallets.getBalance(sourceWallet.walletId)[0]?.available ?? 0;
|
|
78
|
+
|
|
79
|
+
const settlement: Settlement = {
|
|
80
|
+
settlementId,
|
|
81
|
+
pattern: 'INSTANT_DRAWDOWN',
|
|
82
|
+
status: 'SETTLED',
|
|
83
|
+
sourceAgentId,
|
|
84
|
+
targetAgentId,
|
|
85
|
+
ratedRecords,
|
|
86
|
+
totalAmount,
|
|
87
|
+
currency: ratedRecords[0]?.currency ?? 'USD',
|
|
88
|
+
settledAt: hrTimestamp(),
|
|
89
|
+
auditId: '',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Audit
|
|
93
|
+
const auditId = this.audit.record({
|
|
94
|
+
what: { operation: 'SETTLEMENT', entityType: 'transaction', entityId: txnId, amount: totalAmount },
|
|
95
|
+
who: { sourceAgentId, targetAgentId },
|
|
96
|
+
why: { contractId: ratedRecords[0]?.contractId, pricingRuleId: ratedRecords[0]?.pricingRuleId },
|
|
97
|
+
how: { policiesEvaluated: ['SPENDING_LIMIT', 'COUNTERPARTY_ALLOWLIST'], policiesPassed: ['SPENDING_LIMIT', 'COUNTERPARTY_ALLOWLIST'] },
|
|
98
|
+
result: { balanceBefore, balanceAfter, settlementAmount: totalAmount, status: 'SETTLED' },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
settlement.auditId = auditId;
|
|
102
|
+
this.settlements.set(settlementId, settlement);
|
|
103
|
+
|
|
104
|
+
return { settlement, auditId };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* ESCROW_RELEASE: Reserve funds, release upon outcome attestation.
|
|
109
|
+
* Used for outcome-based pricing and milestone payments.
|
|
110
|
+
*/
|
|
111
|
+
createEscrow(
|
|
112
|
+
sourceAgentId: string,
|
|
113
|
+
targetAgentId: string,
|
|
114
|
+
amount: number,
|
|
115
|
+
transactionId: string,
|
|
116
|
+
): string {
|
|
117
|
+
const sourceWallet = this.wallets.getByAgent(sourceAgentId);
|
|
118
|
+
const targetWallet = this.wallets.getByAgent(targetAgentId);
|
|
119
|
+
const escrowId = generateId('esc');
|
|
120
|
+
|
|
121
|
+
// Reserve in source wallet
|
|
122
|
+
this.wallets.reserve(sourceWallet.walletId, amount, targetAgentId, transactionId);
|
|
123
|
+
|
|
124
|
+
this.escrows.set(escrowId, {
|
|
125
|
+
amount,
|
|
126
|
+
sourceWalletId: sourceWallet.walletId,
|
|
127
|
+
targetWalletId: targetWallet.walletId,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.audit.record({
|
|
131
|
+
what: { operation: 'ESCROW_CREATED', entityType: 'escrow', entityId: escrowId, amount },
|
|
132
|
+
who: { sourceAgentId, targetAgentId },
|
|
133
|
+
why: { justification: `Escrow for transaction ${transactionId}` },
|
|
134
|
+
how: { policiesEvaluated: ['SPENDING_LIMIT'], policiesPassed: ['SPENDING_LIMIT'] },
|
|
135
|
+
result: { status: 'ESCROWED' },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return escrowId;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Release escrowed funds to the target (full or partial).
|
|
143
|
+
*/
|
|
144
|
+
releaseEscrow(escrowId: string, releaseAmount?: number): SettlementResult {
|
|
145
|
+
const escrow = this.escrows.get(escrowId);
|
|
146
|
+
if (!escrow) throw new UMPError(`Escrow not found: ${escrowId}`, 'ESCROW_NOT_FOUND');
|
|
147
|
+
|
|
148
|
+
const amount = releaseAmount ?? escrow.amount;
|
|
149
|
+
const txnId = generateId('txn');
|
|
150
|
+
|
|
151
|
+
// Release reservation from source, credit target
|
|
152
|
+
this.wallets.releaseReservation(escrow.sourceWalletId, amount, txnId);
|
|
153
|
+
this.wallets.credit(escrow.targetWalletId, amount, 'escrow', txnId);
|
|
154
|
+
|
|
155
|
+
// If partial release, update remaining
|
|
156
|
+
if (amount < escrow.amount) {
|
|
157
|
+
escrow.amount -= amount;
|
|
158
|
+
} else {
|
|
159
|
+
this.escrows.delete(escrowId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const settlement: Settlement = {
|
|
163
|
+
settlementId: generateId('stl'),
|
|
164
|
+
pattern: 'ESCROW_RELEASE',
|
|
165
|
+
status: 'SETTLED',
|
|
166
|
+
sourceAgentId: 'escrow',
|
|
167
|
+
targetAgentId: 'escrow',
|
|
168
|
+
ratedRecords: [],
|
|
169
|
+
totalAmount: amount,
|
|
170
|
+
currency: 'USD',
|
|
171
|
+
settledAt: hrTimestamp(),
|
|
172
|
+
auditId: '',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const auditId = this.audit.record({
|
|
176
|
+
what: { operation: 'ESCROW_RELEASED', entityType: 'escrow', entityId: escrowId, amount },
|
|
177
|
+
who: { sourceAgentId: 'escrow', targetAgentId: 'escrow' },
|
|
178
|
+
why: { justification: `Escrow ${escrowId} released` },
|
|
179
|
+
how: { policiesEvaluated: ['OUTCOME_VERIFICATION'], policiesPassed: ['OUTCOME_VERIFICATION'] },
|
|
180
|
+
result: { settlementAmount: amount, status: 'SETTLED' },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
settlement.auditId = auditId;
|
|
184
|
+
this.settlements.set(settlement.settlementId, settlement);
|
|
185
|
+
return { settlement, auditId };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* WATERFALL_SPLIT: Distribute payment across multiple parties.
|
|
190
|
+
* Used for marketplace commissions and multi-party revenue share.
|
|
191
|
+
*/
|
|
192
|
+
settleWaterfall(
|
|
193
|
+
sourceAgentId: string,
|
|
194
|
+
splits: Array<{ agentId: string; amount: number }>,
|
|
195
|
+
ratedRecords: RatedRecord[],
|
|
196
|
+
): SettlementResult[] {
|
|
197
|
+
const sourceWallet = this.wallets.getByAgent(sourceAgentId);
|
|
198
|
+
const totalAmount = round(splits.reduce((sum, s) => sum + s.amount, 0));
|
|
199
|
+
const txnId = generateId('txn');
|
|
200
|
+
|
|
201
|
+
// Debit total from source
|
|
202
|
+
this.wallets.debit(sourceWallet.walletId, totalAmount, 'waterfall', txnId);
|
|
203
|
+
|
|
204
|
+
// Credit each target
|
|
205
|
+
const results: SettlementResult[] = splits.map(split => {
|
|
206
|
+
const targetWallet = this.wallets.getByAgent(split.agentId);
|
|
207
|
+
this.wallets.credit(targetWallet.walletId, split.amount, sourceAgentId, txnId);
|
|
208
|
+
|
|
209
|
+
const settlement: Settlement = {
|
|
210
|
+
settlementId: generateId('stl'),
|
|
211
|
+
pattern: 'WATERFALL_SPLIT',
|
|
212
|
+
status: 'SETTLED',
|
|
213
|
+
sourceAgentId,
|
|
214
|
+
targetAgentId: split.agentId,
|
|
215
|
+
ratedRecords,
|
|
216
|
+
totalAmount: split.amount,
|
|
217
|
+
currency: 'USD',
|
|
218
|
+
settledAt: hrTimestamp(),
|
|
219
|
+
auditId: '',
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const auditId = this.audit.record({
|
|
223
|
+
what: { operation: 'WATERFALL_SPLIT', entityType: 'settlement', entityId: settlement.settlementId, amount: split.amount },
|
|
224
|
+
who: { sourceAgentId, targetAgentId: split.agentId },
|
|
225
|
+
why: { justification: `Waterfall split: ${split.amount} to ${split.agentId}` },
|
|
226
|
+
how: { policiesEvaluated: ['SPENDING_LIMIT'], policiesPassed: ['SPENDING_LIMIT'] },
|
|
227
|
+
result: { settlementAmount: split.amount, status: 'SETTLED' },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
settlement.auditId = auditId;
|
|
231
|
+
this.settlements.set(settlement.settlementId, settlement);
|
|
232
|
+
return { settlement, auditId };
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return results;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get settlement by ID.
|
|
240
|
+
*/
|
|
241
|
+
get(settlementId: string): Settlement | undefined {
|
|
242
|
+
return this.settlements.get(settlementId);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* List settlements for an agent.
|
|
247
|
+
*/
|
|
248
|
+
listByAgent(agentId: string, limit = 50): Settlement[] {
|
|
249
|
+
return Array.from(this.settlements.values())
|
|
250
|
+
.filter(s => s.sourceAgentId === agentId || s.targetAgentId === agentId)
|
|
251
|
+
.slice(-limit);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Contract, ContractStatus,
|
|
3
|
+
CreateContractOptions, PricingRule,
|
|
4
|
+
} from '../types';
|
|
5
|
+
import { generateId, hrTimestamp } from '../utils/id';
|
|
6
|
+
import { ContractNotFoundError, UMPError } from '../utils/errors';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ContractManager — Layer 1/2 bridge
|
|
10
|
+
*
|
|
11
|
+
* Manages commercial agreements between agents.
|
|
12
|
+
* Supports pre-negotiated, template-based, and dynamic negotiation.
|
|
13
|
+
*/
|
|
14
|
+
export class ContractManager {
|
|
15
|
+
private contracts: Map<string, Contract> = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a new contract between two agents.
|
|
19
|
+
*/
|
|
20
|
+
create(sourceAgentId: string, options: CreateContractOptions): Contract {
|
|
21
|
+
const contractId = generateId('ctr');
|
|
22
|
+
const now = hrTimestamp();
|
|
23
|
+
|
|
24
|
+
// Assign rule IDs to any rules that don't have them
|
|
25
|
+
const rules: PricingRule[] = options.pricingRules.map(r => ({
|
|
26
|
+
...r,
|
|
27
|
+
ruleId: (r as PricingRule).ruleId || generateId('rul'),
|
|
28
|
+
})) as PricingRule[];
|
|
29
|
+
|
|
30
|
+
const contract: Contract = {
|
|
31
|
+
contractId,
|
|
32
|
+
mode: options.mode || 'TEMPLATE',
|
|
33
|
+
status: 'ACTIVE',
|
|
34
|
+
parties: {
|
|
35
|
+
sourceAgentId,
|
|
36
|
+
targetAgentId: options.targetAgentId,
|
|
37
|
+
},
|
|
38
|
+
pricingRules: rules,
|
|
39
|
+
effectiveFrom: options.effectiveFrom || now,
|
|
40
|
+
effectiveUntil: options.effectiveUntil,
|
|
41
|
+
metadata: options.metadata || {},
|
|
42
|
+
createdAt: now,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.contracts.set(contractId, contract);
|
|
46
|
+
return contract;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get contract by ID.
|
|
51
|
+
*/
|
|
52
|
+
get(contractId: string): Contract {
|
|
53
|
+
const contract = this.contracts.get(contractId);
|
|
54
|
+
if (!contract) throw new ContractNotFoundError(contractId);
|
|
55
|
+
return contract;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find active contract between two agents.
|
|
60
|
+
*/
|
|
61
|
+
findActive(sourceAgentId: string, targetAgentId: string): Contract | undefined {
|
|
62
|
+
const now = new Date();
|
|
63
|
+
for (const c of this.contracts.values()) {
|
|
64
|
+
if (
|
|
65
|
+
c.status === 'ACTIVE' &&
|
|
66
|
+
c.parties.sourceAgentId === sourceAgentId &&
|
|
67
|
+
c.parties.targetAgentId === targetAgentId &&
|
|
68
|
+
c.effectiveFrom <= now &&
|
|
69
|
+
(!c.effectiveUntil || c.effectiveUntil > now)
|
|
70
|
+
) {
|
|
71
|
+
return c;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Dynamic negotiation: propose terms.
|
|
79
|
+
* Returns a DRAFT contract that the counterparty can accept or counter.
|
|
80
|
+
*/
|
|
81
|
+
propose(sourceAgentId: string, options: CreateContractOptions): Contract {
|
|
82
|
+
const contract = this.create(sourceAgentId, { ...options, mode: 'DYNAMIC' });
|
|
83
|
+
contract.status = 'PROPOSED';
|
|
84
|
+
return contract;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Accept a proposed contract — transitions to ACTIVE.
|
|
89
|
+
*/
|
|
90
|
+
accept(contractId: string): Contract {
|
|
91
|
+
const contract = this.get(contractId);
|
|
92
|
+
if (contract.status !== 'PROPOSED') {
|
|
93
|
+
throw new UMPError(`Cannot accept contract in status: ${contract.status}`, 'INVALID_STATE');
|
|
94
|
+
}
|
|
95
|
+
contract.status = 'ACTIVE';
|
|
96
|
+
return contract;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Counter-propose: create a new proposal based on an existing one with modified terms.
|
|
101
|
+
*/
|
|
102
|
+
counter(
|
|
103
|
+
contractId: string,
|
|
104
|
+
counterAgentId: string,
|
|
105
|
+
modifiedRules: Omit<PricingRule, 'ruleId'>[],
|
|
106
|
+
): Contract {
|
|
107
|
+
const original = this.get(contractId);
|
|
108
|
+
original.status = 'EXPIRED'; // superseded
|
|
109
|
+
|
|
110
|
+
return this.propose(counterAgentId, {
|
|
111
|
+
targetAgentId: original.parties.sourceAgentId, // reverse direction
|
|
112
|
+
pricingRules: modifiedRules,
|
|
113
|
+
metadata: {
|
|
114
|
+
...original.metadata,
|
|
115
|
+
counterTo: contractId,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Terminate a contract.
|
|
122
|
+
*/
|
|
123
|
+
terminate(contractId: string): Contract {
|
|
124
|
+
const contract = this.get(contractId);
|
|
125
|
+
contract.status = 'TERMINATED';
|
|
126
|
+
return contract;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* List contracts for an agent.
|
|
131
|
+
*/
|
|
132
|
+
listByAgent(agentId: string, status?: ContractStatus): Contract[] {
|
|
133
|
+
return Array.from(this.contracts.values()).filter(c => {
|
|
134
|
+
const partyMatch =
|
|
135
|
+
c.parties.sourceAgentId === agentId ||
|
|
136
|
+
c.parties.targetAgentId === agentId;
|
|
137
|
+
if (!partyMatch) return false;
|
|
138
|
+
if (status && c.status !== status) return false;
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { UsageEvent, OutcomeAttestation } from '../types';
|
|
2
|
+
import { generateId, hrTimestamp } from '../utils/id';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MeteringEngine — Layer 2 primitive
|
|
6
|
+
*
|
|
7
|
+
* Captures usage events with cryptographic integrity.
|
|
8
|
+
* Supports idempotent event submission, deduplication, and
|
|
9
|
+
* outcome attestation for result-based billing.
|
|
10
|
+
*/
|
|
11
|
+
export class MeteringEngine {
|
|
12
|
+
private events: Map<string, UsageEvent> = new Map();
|
|
13
|
+
private seenIds: Set<string> = new Set(); // idempotency guard
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Record a usage event.
|
|
17
|
+
* Idempotent: resubmitting the same eventId is a no-op.
|
|
18
|
+
*/
|
|
19
|
+
record(event: Omit<UsageEvent, 'eventId' | 'timestamp'> & { eventId?: string }): UsageEvent {
|
|
20
|
+
const eventId = event.eventId || generateId('evt');
|
|
21
|
+
|
|
22
|
+
// Idempotency: skip if already recorded
|
|
23
|
+
if (this.seenIds.has(eventId)) {
|
|
24
|
+
return this.events.get(eventId)!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fullEvent: UsageEvent = {
|
|
28
|
+
...event,
|
|
29
|
+
eventId,
|
|
30
|
+
timestamp: hrTimestamp(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.events.set(eventId, fullEvent);
|
|
34
|
+
this.seenIds.add(eventId);
|
|
35
|
+
return fullEvent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Record a batch of usage events.
|
|
40
|
+
*/
|
|
41
|
+
recordBatch(events: Array<Omit<UsageEvent, 'eventId' | 'timestamp'>>): UsageEvent[] {
|
|
42
|
+
return events.map(e => this.record(e));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create an outcome attestation for result-based billing.
|
|
47
|
+
*/
|
|
48
|
+
attestOutcome(data: {
|
|
49
|
+
outcomeType: OutcomeAttestation['outcomeType'];
|
|
50
|
+
claimedBy: string;
|
|
51
|
+
evidence: OutcomeAttestation['evidence'];
|
|
52
|
+
verificationMethod: OutcomeAttestation['verificationMethod'];
|
|
53
|
+
confidenceScore: number;
|
|
54
|
+
disputeWindow?: number;
|
|
55
|
+
}): OutcomeAttestation {
|
|
56
|
+
return {
|
|
57
|
+
outcomeId: generateId('out'),
|
|
58
|
+
outcomeType: data.outcomeType,
|
|
59
|
+
claimedBy: data.claimedBy,
|
|
60
|
+
evidence: data.evidence,
|
|
61
|
+
verificationMethod: data.verificationMethod,
|
|
62
|
+
verifiedBy: undefined,
|
|
63
|
+
confidenceScore: data.confidenceScore,
|
|
64
|
+
attestationStatus: 'CLAIMED',
|
|
65
|
+
disputeWindow: data.disputeWindow || 24 * 60 * 60 * 1000, // 24h default
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get events for a contract within a time range.
|
|
71
|
+
*/
|
|
72
|
+
getByContract(contractId: string, from?: Date, to?: Date): UsageEvent[] {
|
|
73
|
+
let results = Array.from(this.events.values())
|
|
74
|
+
.filter(e => e.contractId === contractId);
|
|
75
|
+
|
|
76
|
+
if (from) results = results.filter(e => e.timestamp >= from);
|
|
77
|
+
if (to) results = results.filter(e => e.timestamp <= to);
|
|
78
|
+
|
|
79
|
+
return results.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get events for a specific agent (as source or target).
|
|
84
|
+
*/
|
|
85
|
+
getByAgent(agentId: string, limit = 100): UsageEvent[] {
|
|
86
|
+
return Array.from(this.events.values())
|
|
87
|
+
.filter(e => e.sourceAgentId === agentId || e.targetAgentId === agentId)
|
|
88
|
+
.slice(-limit);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a specific event by ID.
|
|
93
|
+
*/
|
|
94
|
+
get(eventId: string): UsageEvent | undefined {
|
|
95
|
+
return this.events.get(eventId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get total metered quantity for a contract.
|
|
100
|
+
*/
|
|
101
|
+
totalQuantity(contractId: string): number {
|
|
102
|
+
return Array.from(this.events.values())
|
|
103
|
+
.filter(e => e.contractId === contractId)
|
|
104
|
+
.reduce((sum, e) => sum + e.quantity, 0);
|
|
105
|
+
}
|
|
106
|
+
}
|