@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,525 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { UMP, PricingTemplates, PricingEngine } from '../src';
|
|
3
|
+
|
|
4
|
+
describe('UMP SDK v2.0', () => {
|
|
5
|
+
let ump: UMP;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
ump = new UMP({ apiKey: 'ump_sk_test_123' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// ═══════════════════════════════════════════════════════════
|
|
12
|
+
// LAYER 1: Identity & Value
|
|
13
|
+
// ═══════════════════════════════════════════════════════════
|
|
14
|
+
|
|
15
|
+
describe('Agent Identity', () => {
|
|
16
|
+
it('should create an agent with authority scope', () => {
|
|
17
|
+
const agent = ump.agents.create({
|
|
18
|
+
name: 'test-agent',
|
|
19
|
+
type: 'AI_AGENT',
|
|
20
|
+
authority: { maxPerTransaction: '$50', maxPerDay: '$500' },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(agent.agentId).toMatch(/^agt_/);
|
|
24
|
+
expect(agent.displayName).toBe('test-agent');
|
|
25
|
+
expect(agent.agentType).toBe('AI_AGENT');
|
|
26
|
+
expect(agent.authorityScope.maxPerTransaction).toBe(50);
|
|
27
|
+
expect(agent.authorityScope.maxPerDay).toBe(500);
|
|
28
|
+
expect(agent.status).toBe('ACTIVE');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should enforce child authority cannot exceed parent', () => {
|
|
32
|
+
const parent = ump.agents.create({
|
|
33
|
+
name: 'parent-org',
|
|
34
|
+
type: 'ORGANIZATION',
|
|
35
|
+
authority: { maxPerTransaction: '$100', maxPerDay: '$1000' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const child = ump.agents.create({
|
|
39
|
+
name: 'child-agent',
|
|
40
|
+
type: 'AI_AGENT',
|
|
41
|
+
parentId: parent.agentId,
|
|
42
|
+
authority: { maxPerTransaction: '$200', maxPerDay: '$2000' }, // exceeds parent
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Child should be capped at parent's limits
|
|
46
|
+
expect(child.authorityScope.maxPerTransaction).toBe(100);
|
|
47
|
+
expect(child.authorityScope.maxPerDay).toBe(1000);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should cascade revocation to children', () => {
|
|
51
|
+
const parent = ump.agents.create({
|
|
52
|
+
name: 'parent',
|
|
53
|
+
type: 'ORGANIZATION',
|
|
54
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const child1 = ump.agents.create({
|
|
58
|
+
name: 'child-1',
|
|
59
|
+
type: 'AI_AGENT',
|
|
60
|
+
parentId: parent.agentId,
|
|
61
|
+
authority: { maxPerTransaction: 50, maxPerDay: 500 },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const child2 = ump.agents.create({
|
|
65
|
+
name: 'child-2',
|
|
66
|
+
type: 'AI_AGENT',
|
|
67
|
+
parentId: parent.agentId,
|
|
68
|
+
authority: { maxPerTransaction: 50, maxPerDay: 500 },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const revoked = ump.agents.revoke(parent.agentId);
|
|
72
|
+
expect(revoked).toContain(parent.agentId);
|
|
73
|
+
expect(revoked).toContain(child1.agentId);
|
|
74
|
+
expect(revoked).toContain(child2.agentId);
|
|
75
|
+
expect(ump.agents.get(child1.agentId).status).toBe('REVOKED');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should check authority for counterparty allowlist', () => {
|
|
79
|
+
const agent = ump.agents.create({
|
|
80
|
+
name: 'restricted-agent',
|
|
81
|
+
type: 'AI_AGENT',
|
|
82
|
+
authority: {
|
|
83
|
+
maxPerTransaction: 100,
|
|
84
|
+
maxPerDay: 1000,
|
|
85
|
+
allowedCounterparties: ['agt_acme_*', 'agt_specific_123'],
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const allowed = ump.agents.checkAuthority(agent.agentId, 50, 'agt_acme_test');
|
|
90
|
+
expect(allowed.allowed).toBe(true);
|
|
91
|
+
|
|
92
|
+
const blocked = ump.agents.checkAuthority(agent.agentId, 50, 'agt_evil_corp');
|
|
93
|
+
expect(blocked.allowed).toBe(false);
|
|
94
|
+
expect(blocked.reason).toContain('not in allowlist');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Wallet', () => {
|
|
99
|
+
it('should create a wallet and fund it', () => {
|
|
100
|
+
const handle = ump.registerAgent({
|
|
101
|
+
name: 'funded-agent',
|
|
102
|
+
type: 'AI_AGENT',
|
|
103
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
handle.wallet.fund({ amount: '$100' });
|
|
107
|
+
expect(handle.wallet.balance()).toBe(100);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should debit and credit between wallets', () => {
|
|
111
|
+
const source = ump.registerAgent({
|
|
112
|
+
name: 'buyer',
|
|
113
|
+
type: 'AI_AGENT',
|
|
114
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
115
|
+
});
|
|
116
|
+
const target = ump.registerAgent({
|
|
117
|
+
name: 'seller',
|
|
118
|
+
type: 'SERVICE',
|
|
119
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
source.wallet.fund({ amount: 100 });
|
|
123
|
+
|
|
124
|
+
const sourceWallet = ump.wallets.getByAgent(source.id);
|
|
125
|
+
const targetWallet = ump.wallets.getByAgent(target.id);
|
|
126
|
+
|
|
127
|
+
ump.wallets.debit(sourceWallet.walletId, 25, target.id, 'txn_test');
|
|
128
|
+
ump.wallets.credit(targetWallet.walletId, 25, source.id, 'txn_test');
|
|
129
|
+
|
|
130
|
+
expect(source.wallet.balance()).toBe(75);
|
|
131
|
+
expect(target.wallet.balance()).toBe(25);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should reject transactions on frozen wallet', () => {
|
|
135
|
+
const handle = ump.registerAgent({
|
|
136
|
+
name: 'freeze-test',
|
|
137
|
+
type: 'AI_AGENT',
|
|
138
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
handle.wallet.fund({ amount: 100 });
|
|
142
|
+
handle.wallet.freeze();
|
|
143
|
+
|
|
144
|
+
const wallet = ump.wallets.getByAgent(handle.id);
|
|
145
|
+
expect(() => {
|
|
146
|
+
ump.wallets.debit(wallet.walletId, 10, 'other', 'txn_test');
|
|
147
|
+
}).toThrow('frozen');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should reject insufficient funds', () => {
|
|
151
|
+
const handle = ump.registerAgent({
|
|
152
|
+
name: 'broke-agent',
|
|
153
|
+
type: 'AI_AGENT',
|
|
154
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
handle.wallet.fund({ amount: 5 });
|
|
158
|
+
|
|
159
|
+
const wallet = ump.wallets.getByAgent(handle.id);
|
|
160
|
+
expect(() => {
|
|
161
|
+
ump.wallets.debit(wallet.walletId, 50, 'other', 'txn_test');
|
|
162
|
+
}).toThrow('Insufficient funds');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should maintain immutable ledger', () => {
|
|
166
|
+
const handle = ump.registerAgent({
|
|
167
|
+
name: 'ledger-test',
|
|
168
|
+
type: 'AI_AGENT',
|
|
169
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const wallet = ump.wallets.getByAgent(handle.id);
|
|
173
|
+
handle.wallet.fund({ amount: 100 });
|
|
174
|
+
ump.wallets.debit(wallet.walletId, 30, 'other', 'txn_1');
|
|
175
|
+
ump.wallets.credit(wallet.walletId, 10, 'other', 'txn_2');
|
|
176
|
+
|
|
177
|
+
const ledger = ump.wallets.getLedger(wallet.walletId);
|
|
178
|
+
expect(ledger).toHaveLength(3); // fund + debit + credit
|
|
179
|
+
expect(ledger[0].type).toBe('TOPUP');
|
|
180
|
+
expect(ledger[1].type).toBe('DEBIT');
|
|
181
|
+
expect(ledger[2].type).toBe('CREDIT');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ═══════════════════════════════════════════════════════════
|
|
186
|
+
// LAYER 2: Terms & Metering
|
|
187
|
+
// ═══════════════════════════════════════════════════════════
|
|
188
|
+
|
|
189
|
+
describe('Pricing Engine', () => {
|
|
190
|
+
const engine = new PricingEngine();
|
|
191
|
+
|
|
192
|
+
it('should evaluate FIXED pricing', () => {
|
|
193
|
+
const amount = engine.simulate({
|
|
194
|
+
ruleId: 'r1', name: 'Flat', primitive: 'FIXED',
|
|
195
|
+
amount: 9.99, period: 'MONTHLY',
|
|
196
|
+
}, 1);
|
|
197
|
+
expect(amount).toBe(9.99);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should evaluate UNIT_RATE pricing', () => {
|
|
201
|
+
const amount = engine.simulate({
|
|
202
|
+
ruleId: 'r1', name: 'Per-token', primitive: 'UNIT_RATE',
|
|
203
|
+
rate: 0.00003, unit: 'TOKEN',
|
|
204
|
+
}, 1000);
|
|
205
|
+
expect(amount).toBe(0.03);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should evaluate TIERED graduated pricing', () => {
|
|
209
|
+
const amount = engine.simulate({
|
|
210
|
+
ruleId: 'r1', name: 'Tiered', primitive: 'TIERED',
|
|
211
|
+
mode: 'GRADUATED',
|
|
212
|
+
tiers: [
|
|
213
|
+
{ from: 0, to: 100, rate: 0.10 },
|
|
214
|
+
{ from: 100, to: 1000, rate: 0.05 },
|
|
215
|
+
{ from: 1000, to: null, rate: 0.01 },
|
|
216
|
+
],
|
|
217
|
+
}, 500);
|
|
218
|
+
// First 100 at $0.10 = $10, next 400 at $0.05 = $20 → $30
|
|
219
|
+
expect(amount).toBe(30);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should evaluate PERCENTAGE with min/max', () => {
|
|
223
|
+
const amount = engine.simulate({
|
|
224
|
+
ruleId: 'r1', name: 'Commission', primitive: 'PERCENTAGE',
|
|
225
|
+
percentage: 0.10, referenceField: 'transaction_amount',
|
|
226
|
+
min: 1.00, max: 50.00,
|
|
227
|
+
}, 1, { transaction_amount: 5 });
|
|
228
|
+
// 10% of $5 = $0.50, but min is $1.00
|
|
229
|
+
expect(amount).toBe(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should evaluate COMPOSITE (ADD) — hybrid pricing', () => {
|
|
233
|
+
const amount = engine.simulate({
|
|
234
|
+
ruleId: 'r1', name: 'Hybrid', primitive: 'COMPOSITE',
|
|
235
|
+
operator: 'ADD',
|
|
236
|
+
rules: [
|
|
237
|
+
{ ruleId: 'r2', name: 'Base', primitive: 'FIXED', amount: 10, period: 'MONTHLY' },
|
|
238
|
+
{ ruleId: 'r3', name: 'Usage', primitive: 'UNIT_RATE', rate: 0.05, unit: 'CALL' },
|
|
239
|
+
],
|
|
240
|
+
}, 100);
|
|
241
|
+
// $10 fixed + 100 × $0.05 = $15
|
|
242
|
+
expect(amount).toBe(15);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should use PricingTemplates.perToken', () => {
|
|
246
|
+
const rule = PricingTemplates.perToken(30, 60); // $30/M input, $60/M output
|
|
247
|
+
const inputCost = engine.simulate(rule, 1_000_000, { direction: 'input' });
|
|
248
|
+
const outputCost = engine.simulate(rule, 1_000_000, { direction: 'output' });
|
|
249
|
+
expect(inputCost).toBe(30);
|
|
250
|
+
expect(outputCost).toBe(60);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should explain pricing calculations', () => {
|
|
254
|
+
const rule = PricingTemplates.subscriptionPlusUsage(99, 1000, 0.05);
|
|
255
|
+
const explanation = engine.explain(rule, {
|
|
256
|
+
event: {
|
|
257
|
+
eventId: 'e1', sourceAgentId: 's', targetAgentId: 't',
|
|
258
|
+
contractId: 'c', serviceId: 'svc', timestamp: new Date(),
|
|
259
|
+
quantity: 1500, unit: 'CALL', dimensions: {},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
expect(explanation).toContain('COMPOSITE');
|
|
263
|
+
expect(explanation).toContain('FIXED');
|
|
264
|
+
expect(explanation).toContain('THRESHOLD');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('Contracts', () => {
|
|
269
|
+
it('should create and find active contracts', () => {
|
|
270
|
+
const agent1 = ump.agents.create({
|
|
271
|
+
name: 'buyer', type: 'AI_AGENT',
|
|
272
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
273
|
+
});
|
|
274
|
+
const agent2 = ump.agents.create({
|
|
275
|
+
name: 'seller', type: 'SERVICE',
|
|
276
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const contract = ump.contracts.create(agent1.agentId, {
|
|
280
|
+
targetAgentId: agent2.agentId,
|
|
281
|
+
pricingRules: [{
|
|
282
|
+
ruleId: 'r1', name: 'Per-call', primitive: 'UNIT_RATE',
|
|
283
|
+
rate: 0.01, unit: 'API_CALL',
|
|
284
|
+
}],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(contract.status).toBe('ACTIVE');
|
|
288
|
+
|
|
289
|
+
const found = ump.contracts.findActive(agent1.agentId, agent2.agentId);
|
|
290
|
+
expect(found?.contractId).toBe(contract.contractId);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should support dynamic negotiation flow', () => {
|
|
294
|
+
const a1 = ump.agents.create({
|
|
295
|
+
name: 'a1', type: 'AI_AGENT',
|
|
296
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
297
|
+
});
|
|
298
|
+
const a2 = ump.agents.create({
|
|
299
|
+
name: 'a2', type: 'AI_AGENT',
|
|
300
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// a1 proposes
|
|
304
|
+
const proposal = ump.contracts.propose(a1.agentId, {
|
|
305
|
+
targetAgentId: a2.agentId,
|
|
306
|
+
pricingRules: [{
|
|
307
|
+
name: 'Per-file', primitive: 'UNIT_RATE', rate: 0.50, unit: 'FILE',
|
|
308
|
+
} as any],
|
|
309
|
+
});
|
|
310
|
+
expect(proposal.status).toBe('PROPOSED');
|
|
311
|
+
|
|
312
|
+
// a2 counters
|
|
313
|
+
const counter = ump.contracts.counter(proposal.contractId, a2.agentId, [{
|
|
314
|
+
name: 'Per-file', primitive: 'UNIT_RATE', rate: 0.35, unit: 'FILE',
|
|
315
|
+
} as any]);
|
|
316
|
+
expect(counter.status).toBe('PROPOSED');
|
|
317
|
+
expect(counter.metadata).toHaveProperty('counterTo');
|
|
318
|
+
|
|
319
|
+
// a1 accepts
|
|
320
|
+
const accepted = ump.contracts.accept(counter.contractId);
|
|
321
|
+
expect(accepted.status).toBe('ACTIVE');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('Metering', () => {
|
|
326
|
+
it('should record usage events idempotently', () => {
|
|
327
|
+
const e1 = ump.metering.record({
|
|
328
|
+
eventId: 'evt_dedup_test',
|
|
329
|
+
sourceAgentId: 's1', targetAgentId: 't1',
|
|
330
|
+
contractId: 'c1', serviceId: 'svc1',
|
|
331
|
+
quantity: 100, unit: 'TOKEN', dimensions: {},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Resubmit same ID → should return original
|
|
335
|
+
const e2 = ump.metering.record({
|
|
336
|
+
eventId: 'evt_dedup_test',
|
|
337
|
+
sourceAgentId: 's1', targetAgentId: 't1',
|
|
338
|
+
contractId: 'c1', serviceId: 'svc1',
|
|
339
|
+
quantity: 999, unit: 'TOKEN', dimensions: {},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(e2.quantity).toBe(100); // original, not 999
|
|
343
|
+
expect(e1.eventId).toBe(e2.eventId);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ═══════════════════════════════════════════════════════════
|
|
348
|
+
// LAYER 3: Settlement & Governance
|
|
349
|
+
// ═══════════════════════════════════════════════════════════
|
|
350
|
+
|
|
351
|
+
describe('Settlement Bus', () => {
|
|
352
|
+
it('should execute instant drawdown settlement', () => {
|
|
353
|
+
const buyer = ump.registerAgent({
|
|
354
|
+
name: 'buyer', type: 'AI_AGENT',
|
|
355
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
356
|
+
});
|
|
357
|
+
const seller = ump.registerAgent({
|
|
358
|
+
name: 'seller', type: 'SERVICE',
|
|
359
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
buyer.wallet.fund({ amount: 100 });
|
|
363
|
+
|
|
364
|
+
const result = ump.settlement.settleInstant(
|
|
365
|
+
buyer.id, seller.id,
|
|
366
|
+
[{
|
|
367
|
+
ratedRecordId: 'r1', usageEventId: 'e1', contractId: 'c1',
|
|
368
|
+
pricingRuleId: 'p1', quantity: 1000, rate: 0.00003,
|
|
369
|
+
amount: 0.03, currency: 'USD', ratedAt: new Date(),
|
|
370
|
+
}],
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(result.settlement.status).toBe('SETTLED');
|
|
374
|
+
expect(result.settlement.totalAmount).toBe(0.03);
|
|
375
|
+
expect(buyer.wallet.balance()).toBe(99.97);
|
|
376
|
+
expect(seller.wallet.balance()).toBe(0.03);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should execute escrow → release flow', () => {
|
|
380
|
+
const buyer = ump.registerAgent({
|
|
381
|
+
name: 'buyer', type: 'AI_AGENT',
|
|
382
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
383
|
+
});
|
|
384
|
+
const seller = ump.registerAgent({
|
|
385
|
+
name: 'seller', type: 'SERVICE',
|
|
386
|
+
authority: { maxPerTransaction: 100, maxPerDay: 1000 },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
buyer.wallet.fund({ amount: 100 });
|
|
390
|
+
|
|
391
|
+
// Create escrow
|
|
392
|
+
const escrowId = ump.settlement.createEscrow(buyer.id, seller.id, 50, 'txn_1');
|
|
393
|
+
|
|
394
|
+
// Buyer's available should be reduced, but total still shows reserved
|
|
395
|
+
expect(buyer.wallet.balance()).toBe(50); // 100 - 50 reserved
|
|
396
|
+
|
|
397
|
+
// Release escrow (simulate outcome verified)
|
|
398
|
+
const result = ump.settlement.releaseEscrow(escrowId, 47); // partial: 47 of 50
|
|
399
|
+
|
|
400
|
+
expect(result.settlement.totalAmount).toBe(47);
|
|
401
|
+
expect(seller.wallet.balance()).toBe(47);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should execute waterfall splits', () => {
|
|
405
|
+
const buyer = ump.registerAgent({
|
|
406
|
+
name: 'buyer', type: 'AI_AGENT',
|
|
407
|
+
authority: { maxPerTransaction: 200, maxPerDay: 2000 },
|
|
408
|
+
});
|
|
409
|
+
const platform = ump.registerAgent({
|
|
410
|
+
name: 'platform', type: 'SERVICE',
|
|
411
|
+
authority: { maxPerTransaction: 200, maxPerDay: 2000 },
|
|
412
|
+
});
|
|
413
|
+
const vendor = ump.registerAgent({
|
|
414
|
+
name: 'vendor', type: 'SERVICE',
|
|
415
|
+
authority: { maxPerTransaction: 200, maxPerDay: 2000 },
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
buyer.wallet.fund({ amount: 100 });
|
|
419
|
+
|
|
420
|
+
const results = ump.settlement.settleWaterfall(
|
|
421
|
+
buyer.id,
|
|
422
|
+
[
|
|
423
|
+
{ agentId: platform.id, amount: 15 }, // 15% commission
|
|
424
|
+
{ agentId: vendor.id, amount: 85 }, // 85% to vendor
|
|
425
|
+
],
|
|
426
|
+
[],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
expect(results).toHaveLength(2);
|
|
430
|
+
expect(buyer.wallet.balance()).toBe(0);
|
|
431
|
+
expect(platform.wallet.balance()).toBe(15);
|
|
432
|
+
expect(vendor.wallet.balance()).toBe(85);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('Audit Trail', () => {
|
|
437
|
+
it('should record and query audit entries', () => {
|
|
438
|
+
const auditId = ump.audit.record({
|
|
439
|
+
what: { operation: 'TEST', entityType: 'test', entityId: 'e1', amount: 42 },
|
|
440
|
+
who: { sourceAgentId: 'a1', targetAgentId: 'a2' },
|
|
441
|
+
why: { contractId: 'c1', justification: 'Test audit' },
|
|
442
|
+
how: { policiesEvaluated: ['SPENDING_LIMIT'], policiesPassed: ['SPENDING_LIMIT'] },
|
|
443
|
+
result: { status: 'SUCCESS' },
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(auditId).toMatch(/^aud_/);
|
|
447
|
+
|
|
448
|
+
const records = ump.audit.query({ agentId: 'a1' });
|
|
449
|
+
expect(records.length).toBeGreaterThan(0);
|
|
450
|
+
expect(records[0].what.operation).toBe('TEST');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should fire onAudit callback', () => {
|
|
454
|
+
const captured: any[] = [];
|
|
455
|
+
const ump2 = new UMP({
|
|
456
|
+
apiKey: 'test',
|
|
457
|
+
onAudit: (record) => captured.push(record),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
ump2.audit.record({
|
|
461
|
+
what: { operation: 'CB_TEST', entityType: 'test', entityId: 'e1' },
|
|
462
|
+
who: { sourceAgentId: 'a1', targetAgentId: 'a2' },
|
|
463
|
+
why: {},
|
|
464
|
+
how: { policiesEvaluated: [], policiesPassed: [] },
|
|
465
|
+
result: { status: 'OK' },
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(captured).toHaveLength(1);
|
|
469
|
+
expect(captured[0].what.operation).toBe('CB_TEST');
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ═══════════════════════════════════════════════════════════
|
|
474
|
+
// END-TO-END: The 10-Line Quick Start
|
|
475
|
+
// ═══════════════════════════════════════════════════════════
|
|
476
|
+
|
|
477
|
+
describe('End-to-End Transaction', () => {
|
|
478
|
+
it('should execute the Quick Start flow', async () => {
|
|
479
|
+
// 1. Initialize
|
|
480
|
+
const ump = new UMP({ apiKey: 'ump_sk_test' });
|
|
481
|
+
|
|
482
|
+
// 2. Register agents
|
|
483
|
+
const buyer = ump.registerAgent({
|
|
484
|
+
name: 'my-coding-agent',
|
|
485
|
+
type: 'AI_AGENT',
|
|
486
|
+
authority: { maxPerTransaction: '$50', maxPerDay: '$500' },
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const seller = ump.registerAgent({
|
|
490
|
+
name: 'code-review-service',
|
|
491
|
+
type: 'SERVICE',
|
|
492
|
+
authority: { maxPerTransaction: '$100', maxPerDay: '$5000' },
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 3. Fund the wallet
|
|
496
|
+
buyer.wallet.fund({ amount: '$100' });
|
|
497
|
+
|
|
498
|
+
// 4. Create contract
|
|
499
|
+
ump.contracts.create(buyer.id, {
|
|
500
|
+
targetAgentId: seller.id,
|
|
501
|
+
pricingRules: [{
|
|
502
|
+
ruleId: 'default',
|
|
503
|
+
name: 'Per code review',
|
|
504
|
+
primitive: 'FIXED',
|
|
505
|
+
amount: 0.12,
|
|
506
|
+
period: 'PER_EVENT',
|
|
507
|
+
}],
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// 5. Transact!
|
|
511
|
+
const result = await ump.transact({
|
|
512
|
+
from: buyer.id,
|
|
513
|
+
to: seller.id,
|
|
514
|
+
service: 'code_review',
|
|
515
|
+
payload: { repo: 'github.com/acme/app', pr: 42 },
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
expect(result.cost).toBe(0.12);
|
|
519
|
+
expect(result.auditId).toMatch(/^aud_/);
|
|
520
|
+
expect(result.duration).toBeLessThan(1000); // sub-second
|
|
521
|
+
expect(buyer.wallet.balance()).toBe(99.88);
|
|
522
|
+
expect(seller.wallet.balance()).toBe(0.12);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": ["./src/*"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"include": ["src/**/*.ts"],
|
|
27
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
28
|
+
}
|