@hivemind-os/collective-core 0.2.0

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.
Files changed (145) hide show
  1. package/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
  2. package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
  3. package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
  4. package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
  5. package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
  6. package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
  7. package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
  8. package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
  9. package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
  10. package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
  11. package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
  12. package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
  13. package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
  14. package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
  15. package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
  16. package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
  17. package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
  18. package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
  19. package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
  20. package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
  21. package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
  22. package/.turbo/turbo-build.log +14 -0
  23. package/dist/index.d.ts +1675 -0
  24. package/dist/index.js +8006 -0
  25. package/dist/index.js.map +1 -0
  26. package/package.json +41 -0
  27. package/src/auth/device-flow.ts +108 -0
  28. package/src/auth/ed25519-provider.ts +43 -0
  29. package/src/auth/errors.ts +82 -0
  30. package/src/auth/evm-key.ts +55 -0
  31. package/src/auth/index.ts +8 -0
  32. package/src/auth/session-state.ts +25 -0
  33. package/src/auth/session-store.ts +510 -0
  34. package/src/auth/types.ts +81 -0
  35. package/src/auth/zklogin-provider.ts +902 -0
  36. package/src/blobstore/WALRUS_FINDINGS.md +284 -0
  37. package/src/blobstore/encrypted-store.ts +56 -0
  38. package/src/blobstore/fs-store.ts +91 -0
  39. package/src/blobstore/hybrid-store.ts +144 -0
  40. package/src/blobstore/index.ts +5 -0
  41. package/src/blobstore/interface.ts +33 -0
  42. package/src/blobstore/walrus-spike.ts +345 -0
  43. package/src/blobstore/walrus-store.ts +551 -0
  44. package/src/cache/agent-cache.ts +403 -0
  45. package/src/cache/index.ts +1 -0
  46. package/src/crypto/encryption.ts +152 -0
  47. package/src/crypto/index.ts +2 -0
  48. package/src/crypto/x25519.ts +41 -0
  49. package/src/dispute/client.ts +191 -0
  50. package/src/dispute/index.ts +1 -0
  51. package/src/events/index.ts +2 -0
  52. package/src/events/parser.ts +291 -0
  53. package/src/events/subscription.ts +131 -0
  54. package/src/evm/constants.ts +6 -0
  55. package/src/evm/index.ts +2 -0
  56. package/src/evm/wallet.ts +136 -0
  57. package/src/identity/did.ts +36 -0
  58. package/src/identity/index.ts +4 -0
  59. package/src/identity/keypair.ts +199 -0
  60. package/src/identity/signing.ts +28 -0
  61. package/src/index.ts +22 -0
  62. package/src/internal/parsing.ts +416 -0
  63. package/src/marketplace/client.ts +349 -0
  64. package/src/marketplace/index.ts +1 -0
  65. package/src/metering/hash-chain.ts +94 -0
  66. package/src/metering/index.ts +4 -0
  67. package/src/metering/meter.ts +80 -0
  68. package/src/metering/streaming.ts +196 -0
  69. package/src/metering/verification.ts +104 -0
  70. package/src/payment/index.ts +1 -0
  71. package/src/payment/rail-selector.ts +41 -0
  72. package/src/registry/client.ts +328 -0
  73. package/src/registry/index.ts +1 -0
  74. package/src/relay/consumer-client.ts +497 -0
  75. package/src/relay/index.ts +1 -0
  76. package/src/relay-registry/client.ts +295 -0
  77. package/src/relay-registry/discovery.ts +109 -0
  78. package/src/relay-registry/index.ts +2 -0
  79. package/src/reputation/anchor-client.ts +126 -0
  80. package/src/reputation/event-publisher.ts +67 -0
  81. package/src/reputation/index.ts +5 -0
  82. package/src/reputation/merkle.ts +79 -0
  83. package/src/reputation/score-calculator.ts +133 -0
  84. package/src/reputation/serialization.ts +37 -0
  85. package/src/reputation/store.ts +165 -0
  86. package/src/reputation/validation.ts +135 -0
  87. package/src/routing/circuit-breaker.ts +111 -0
  88. package/src/routing/fan-out.ts +266 -0
  89. package/src/routing/index.ts +4 -0
  90. package/src/routing/performance.ts +244 -0
  91. package/src/routing/selector.ts +225 -0
  92. package/src/spending/index.ts +1 -0
  93. package/src/spending/policy.ts +271 -0
  94. package/src/staking/client.ts +319 -0
  95. package/src/staking/index.ts +1 -0
  96. package/src/sui/client.ts +214 -0
  97. package/src/sui/index.ts +2 -0
  98. package/src/sui/tx-helpers.ts +1070 -0
  99. package/src/task/client.ts +215 -0
  100. package/src/task/index.ts +1 -0
  101. package/src/x402/client.ts +295 -0
  102. package/src/x402/index.ts +1 -0
  103. package/tests/auth/device-flow.test.ts +62 -0
  104. package/tests/auth/ed25519-provider.test.ts +24 -0
  105. package/tests/auth/evm-key.test.ts +31 -0
  106. package/tests/auth/session-store.test.ts +201 -0
  107. package/tests/auth/zklogin-provider.test.ts +366 -0
  108. package/tests/blobstore/encrypted-store.test.ts +78 -0
  109. package/tests/blobstore.test.ts +91 -0
  110. package/tests/cache.test.ts +124 -0
  111. package/tests/crypto/encryption.test.ts +70 -0
  112. package/tests/crypto/x25519.test.ts +47 -0
  113. package/tests/dispute/client.test.ts +238 -0
  114. package/tests/events.test.ts +202 -0
  115. package/tests/evm/wallet.test.ts +101 -0
  116. package/tests/hybrid-store.test.ts +121 -0
  117. package/tests/identity.test.ts +161 -0
  118. package/tests/marketplace.test.ts +308 -0
  119. package/tests/metering/hash-chain.test.ts +32 -0
  120. package/tests/metering/meter.test.ts +23 -0
  121. package/tests/metering/streaming.test.ts +52 -0
  122. package/tests/metering/verification.test.ts +27 -0
  123. package/tests/payment/rail-selector.test.ts +95 -0
  124. package/tests/registry.test.ts +183 -0
  125. package/tests/relay-consumer-client.test.ts +119 -0
  126. package/tests/relay-registry/client.test.ts +261 -0
  127. package/tests/reputation/event-publisher.test.ts +70 -0
  128. package/tests/reputation/merkle.test.ts +44 -0
  129. package/tests/reputation/score-calculator.test.ts +104 -0
  130. package/tests/reputation/store.test.ts +94 -0
  131. package/tests/routing/circuit-breaker.test.ts +45 -0
  132. package/tests/routing/fan-out.test.ts +123 -0
  133. package/tests/routing/performance.test.ts +49 -0
  134. package/tests/routing/selector.test.ts +114 -0
  135. package/tests/spending.test.ts +133 -0
  136. package/tests/staking/client.test.ts +286 -0
  137. package/tests/sui-client.test.ts +85 -0
  138. package/tests/task.test.ts +249 -0
  139. package/tests/tx-helpers.test.ts +70 -0
  140. package/tests/walrus-spike.test.ts +100 -0
  141. package/tests/walrus-store.test.ts +196 -0
  142. package/tests/x402/client.test.ts +116 -0
  143. package/tsconfig.json +9 -0
  144. package/tsup.config.ts +11 -0
  145. package/vitest.config.ts +8 -0
@@ -0,0 +1,308 @@
1
+ import type { SuiEvent } from '@mysten/sui/client';
2
+ import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { BidStatus, MarketplaceClient, TaskStatus, type MeshSuiClient } from '../src/index.js';
6
+
7
+ const networkConfig = {
8
+ rpcUrl: 'http://127.0.0.1:9000',
9
+ faucetUrl: 'http://127.0.0.1:9123',
10
+ packageId: '0x1',
11
+ registryId: '0x2',
12
+ };
13
+
14
+ function getCommands(tx: { getData: () => { commands: Array<Record<string, unknown>> } }): Array<Record<string, unknown>> {
15
+ return tx.getData().commands;
16
+ }
17
+
18
+ function createBidPlacedEvent(payload: Record<string, unknown>): SuiEvent {
19
+ return {
20
+ id: { txDigest: '0xtx', eventSeq: '0' },
21
+ packageId: networkConfig.packageId,
22
+ transactionModule: 'marketplace',
23
+ type: `${networkConfig.packageId}::marketplace::BidPlaced`,
24
+ sender: '0xbidder',
25
+ timestampMs: '1000',
26
+ parsedJson: payload,
27
+ bcs: '',
28
+ bcsEncoding: 'base64',
29
+ } as unknown as SuiEvent;
30
+ }
31
+
32
+ function createTaskPostedEvent(payload: Record<string, unknown>): SuiEvent {
33
+ return {
34
+ id: { txDigest: '0xtask', eventSeq: '0' },
35
+ packageId: networkConfig.packageId,
36
+ transactionModule: 'task',
37
+ type: `${networkConfig.packageId}::task::TaskPosted`,
38
+ sender: '0xrequester',
39
+ timestampMs: '1000',
40
+ parsedJson: payload,
41
+ bcs: '',
42
+ bcsEncoding: 'base64',
43
+ } as unknown as SuiEvent;
44
+ }
45
+
46
+ describe('MarketplaceClient', () => {
47
+ it('places bids with explicit reputation scores', async () => {
48
+ const executeTransaction = vi.fn().mockResolvedValue({
49
+ digest: '0xtx',
50
+ objectChanges: [
51
+ {
52
+ type: 'created',
53
+ objectType: '0x1::marketplace::Bid',
54
+ objectId: '0xb01',
55
+ },
56
+ ],
57
+ });
58
+ const client = new MarketplaceClient(
59
+ {
60
+ executeTransaction,
61
+ getObject: vi.fn(),
62
+ queryEvents: vi.fn(),
63
+ client: { getOwnedObjects: vi.fn() },
64
+ } as unknown as MeshSuiClient,
65
+ networkConfig,
66
+ );
67
+
68
+ const result = await client.placeBid({
69
+ taskId: '0xa01',
70
+ bidPriceMist: 500n,
71
+ reputationScore: 88n,
72
+ evidenceBlob: 'proposal',
73
+ signer: {} as unknown as Ed25519Keypair,
74
+ });
75
+
76
+ const commands = getCommands(executeTransaction.mock.calls[0]?.[0]);
77
+ expect(commands[0]?.$kind).toBe('MoveCall');
78
+ expect(result).toEqual({ bidId: '0xb01', txDigest: '0xtx', reputationScore: 88n });
79
+ });
80
+
81
+ it('accepts bids and rejects competing active bids in the same transaction', async () => {
82
+ const executeTransaction = vi.fn().mockResolvedValue({ digest: '0xaccept' });
83
+ const queryEvents = vi.fn().mockResolvedValue({
84
+ events: [
85
+ createBidPlacedEvent({ bid_id: '0xb11', task_id: '0xa11' }),
86
+ createBidPlacedEvent({ bid_id: '0xb12', task_id: '0xa11' }),
87
+ ],
88
+ nextCursor: null,
89
+ hasMore: false,
90
+ });
91
+ const getObject = vi.fn(async (objectId: string) => {
92
+ if (objectId === '0xb11') {
93
+ return {
94
+ id: '0xb11',
95
+ task_id: '0xa11',
96
+ bidder: '0xprovider1',
97
+ bid_price: '600',
98
+ reputation_score: '40',
99
+ evidence_blob: 'first',
100
+ created_at: 1_000,
101
+ status: BidStatus.ACTIVE,
102
+ };
103
+ }
104
+ return {
105
+ id: '0xb12',
106
+ task_id: '0xa11',
107
+ bidder: '0xprovider2',
108
+ bid_price: '500',
109
+ reputation_score: '80',
110
+ evidence_blob: 'second',
111
+ created_at: 1_100,
112
+ status: BidStatus.ACTIVE,
113
+ };
114
+ });
115
+ const client = new MarketplaceClient(
116
+ {
117
+ executeTransaction,
118
+ getObject,
119
+ queryEvents,
120
+ client: { getOwnedObjects: vi.fn() },
121
+ } as unknown as MeshSuiClient,
122
+ networkConfig,
123
+ );
124
+
125
+ const result = await client.acceptBid({
126
+ taskId: '0xa11',
127
+ bidId: '0xb12',
128
+ signer: {} as unknown as Ed25519Keypair,
129
+ });
130
+
131
+ const commands = getCommands(executeTransaction.mock.calls[0]?.[0]);
132
+ expect(commands).toHaveLength(2);
133
+ expect(result).toEqual({ txDigest: '0xaccept', rejectedBidIds: ['0xb11'] });
134
+ });
135
+
136
+ it('accepts a bid without rejecting competitors when requested', async () => {
137
+ const executeTransaction = vi.fn().mockResolvedValue({ digest: '0xaccept' });
138
+ const client = new MarketplaceClient(
139
+ {
140
+ executeTransaction,
141
+ getObject: vi.fn(),
142
+ queryEvents: vi.fn(),
143
+ client: { getOwnedObjects: vi.fn() },
144
+ } as unknown as MeshSuiClient,
145
+ networkConfig,
146
+ );
147
+
148
+ const result = await client.acceptBid({
149
+ taskId: '0xa11',
150
+ bidId: '0xb12',
151
+ rejectCompeting: false,
152
+ signer: {} as unknown as Ed25519Keypair,
153
+ });
154
+
155
+ const commands = getCommands(executeTransaction.mock.calls[0]?.[0]);
156
+ expect(commands).toHaveLength(1);
157
+ expect(result).toEqual({ txDigest: '0xaccept', rejectedBidIds: [] });
158
+ });
159
+
160
+ it('parses bids for a task and ranks the recommended bid', async () => {
161
+ const queryEvents = vi.fn().mockResolvedValue({
162
+ events: [
163
+ createBidPlacedEvent({ bid_id: '0xb21', task_id: '0xa21' }),
164
+ createBidPlacedEvent({ bid_id: '0xb22', task_id: '0xa21' }),
165
+ ],
166
+ nextCursor: null,
167
+ hasMore: false,
168
+ });
169
+ const getObject = vi.fn(async (objectId: string) => {
170
+ if (objectId === '0xb21') {
171
+ return {
172
+ id: '0xb21',
173
+ task_id: '0xa21',
174
+ bidder: '0xprovider1',
175
+ bid_price: '700',
176
+ reputation_score: '100',
177
+ evidence_blob: 'strong reputation',
178
+ created_at: 1_000,
179
+ status: BidStatus.ACTIVE,
180
+ };
181
+ }
182
+ return {
183
+ id: '0xb22',
184
+ task_id: '0xa21',
185
+ bidder: '0xprovider2',
186
+ bid_price: '500',
187
+ reputation_score: '50',
188
+ evidence_blob: 'cheaper',
189
+ created_at: 1_100,
190
+ status: BidStatus.ACTIVE,
191
+ };
192
+ });
193
+ const client = new MarketplaceClient(
194
+ {
195
+ executeTransaction: vi.fn(),
196
+ getObject,
197
+ queryEvents,
198
+ client: { getOwnedObjects: vi.fn() },
199
+ } as unknown as MeshSuiClient,
200
+ networkConfig,
201
+ );
202
+
203
+ const bids = await client.getBidsForTask('0xa21');
204
+ const recommended = await client.getRecommendedBid('0xa21', { reputationWeight: 20n, priceWeight: 1n });
205
+
206
+ expect(bids).toHaveLength(2);
207
+ expect(bids[0]?.evidenceBlob).toBe('strong reputation');
208
+ expect(recommended?.bid.id).toBe('0xb21');
209
+ expect(recommended?.score).toBeGreaterThan(0n);
210
+ });
211
+
212
+ it('rejects invalid browse filters and negative recommendation weights', async () => {
213
+ const client = new MarketplaceClient(
214
+ {
215
+ executeTransaction: vi.fn(),
216
+ getObject: vi.fn(async () => ({
217
+ id: '0xb21',
218
+ task_id: '0xa21',
219
+ bidder: '0xprovider1',
220
+ bid_price: '700',
221
+ reputation_score: '100',
222
+ evidence_blob: 'strong reputation',
223
+ created_at: 1_000,
224
+ status: BidStatus.ACTIVE,
225
+ })),
226
+ queryEvents: vi.fn().mockResolvedValue({
227
+ events: [createBidPlacedEvent({ bid_id: '0xb21', task_id: '0xa21' })],
228
+ nextCursor: null,
229
+ hasMore: false,
230
+ }),
231
+ client: { getOwnedObjects: vi.fn() },
232
+ } as unknown as MeshSuiClient,
233
+ networkConfig,
234
+ );
235
+
236
+ await expect(client.browseOpenTasks({ limit: Number.NaN })).rejects.toThrow('filters.limit must be a positive safe integer.');
237
+ await expect(client.browseOpenTasks({ minPriceMist: 10n, maxPriceMist: 5n })).rejects.toThrow(
238
+ 'filters.minPriceMist must be less than or equal to filters.maxPriceMist.',
239
+ );
240
+ await expect(client.getRecommendedBid('0xa21', { reputationWeight: -1n })).rejects.toThrow('options.reputationWeight must be non-negative.');
241
+ });
242
+
243
+ it('browses only open tasks that match category filters', async () => {
244
+ const queryEvents = vi.fn().mockResolvedValue({
245
+ events: [
246
+ createTaskPostedEvent({
247
+ task_id: '0xaa1',
248
+ requester: '0xrequester',
249
+ provider: '0x0',
250
+ capability: 'summarize',
251
+ category: 'analysis',
252
+ input_blob_id: 'blob-1',
253
+ agreement_hash: 'hash-1',
254
+ price: '500',
255
+ status: TaskStatus.OPEN,
256
+ dispute_window_ms: 60_000,
257
+ expires_at: Date.now() + 10_000,
258
+ created_at: 1_000,
259
+ }),
260
+ createTaskPostedEvent({
261
+ task_id: '0xaa2',
262
+ requester: '0xrequester',
263
+ provider: '0x0',
264
+ capability: 'code',
265
+ category: 'code',
266
+ input_blob_id: 'blob-2',
267
+ agreement_hash: 'hash-2',
268
+ price: '700',
269
+ status: TaskStatus.OPEN,
270
+ dispute_window_ms: 60_000,
271
+ expires_at: Date.now() + 10_000,
272
+ created_at: 2_000,
273
+ }),
274
+ ],
275
+ nextCursor: null,
276
+ hasMore: false,
277
+ });
278
+ const getObject = vi.fn(async (objectId: string) => ({
279
+ id: objectId,
280
+ requester: '0xrequester',
281
+ provider: '0x0',
282
+ capability: objectId === '0xaa1' ? 'summarize' : 'code',
283
+ category: objectId === '0xaa1' ? 'analysis' : 'code',
284
+ input_blob_id: objectId === '0xaa1' ? 'blob-1' : 'blob-2',
285
+ agreement_hash: objectId === '0xaa1' ? 'hash-1' : 'hash-2',
286
+ price: objectId === '0xaa1' ? '500' : '700',
287
+ status: objectId === '0xaa1' ? TaskStatus.OPEN : TaskStatus.ACCEPTED,
288
+ dispute_window_ms: 60_000,
289
+ created_at: objectId === '0xaa1' ? 1_000 : 2_000,
290
+ expires_at: Date.now() + 10_000,
291
+ }));
292
+ const client = new MarketplaceClient(
293
+ {
294
+ executeTransaction: vi.fn(),
295
+ getObject,
296
+ queryEvents,
297
+ client: { getOwnedObjects: vi.fn() },
298
+ } as unknown as MeshSuiClient,
299
+ networkConfig,
300
+ );
301
+
302
+ const tasks = await client.browseOpenTasks({ category: 'analysis', maxPriceMist: 600n });
303
+
304
+ expect(tasks).toHaveLength(1);
305
+ expect(tasks[0]?.id).toBe('0xaa1');
306
+ expect(tasks[0]?.category).toBe('analysis');
307
+ });
308
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { HashChain } from '../../src/metering/hash-chain.js';
4
+
5
+ describe('HashChain', () => {
6
+ it('builds and verifies a hash chain', () => {
7
+ const seed = new TextEncoder().encode('seed');
8
+ const units = [
9
+ new TextEncoder().encode('one'),
10
+ new TextEncoder().encode('two'),
11
+ ];
12
+ const chain = new HashChain(seed);
13
+ for (const unit of units) {
14
+ chain.addUnit(unit);
15
+ }
16
+
17
+ const proof = chain.getProof();
18
+
19
+ expect(proof.unitCount).toBe(2);
20
+ expect(proof.intermediateHashes).toHaveLength(2);
21
+ expect(HashChain.verifyChain(proof, units, seed)).toBe(true);
22
+ });
23
+
24
+ it('detects tampering', () => {
25
+ const seed = new TextEncoder().encode('seed');
26
+ const chain = new HashChain(seed);
27
+ chain.addUnit(new TextEncoder().encode('one'));
28
+ const proof = chain.getProof();
29
+
30
+ expect(HashChain.verifyChain(proof, [new TextEncoder().encode('other')], seed)).toBe(false);
31
+ });
32
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { UsageMeter } from '../../src/metering/meter.js';
4
+
5
+ describe('UsageMeter', () => {
6
+ it('tracks usage and computes capped cost', () => {
7
+ const meter = new UsageMeter({ taskId: 'task-1', maxPrice: 500n, unitPrice: 300n });
8
+
9
+ meter.recordUnit(new TextEncoder().encode('a'));
10
+ meter.recordUnit(new TextEncoder().encode('b'));
11
+
12
+ expect(meter.getActualUnits()).toBe(2);
13
+ expect(meter.getCost()).toBe(500n);
14
+ expect(meter.getReport()).toEqual({
15
+ taskId: 'task-1',
16
+ actualUnits: 2,
17
+ actualCost: 500n,
18
+ maxPrice: 500n,
19
+ refundAmount: 0n,
20
+ verificationHash: meter.getVerificationHash(),
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+
5
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { StreamingPaymentManager } from '../../src/metering/streaming.js';
8
+
9
+ const createdPaths: string[] = [];
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })));
13
+ });
14
+
15
+ async function createDbPath(): Promise<string> {
16
+ const dir = resolve(process.cwd(), '.test-data', randomUUID());
17
+ createdPaths.push(dir);
18
+ await mkdir(dir, { recursive: true });
19
+ return resolve(dir, 'streaming.sqlite');
20
+ }
21
+
22
+ describe('StreamingPaymentManager', () => {
23
+ it('tracks a stream lifecycle and audit trail', async () => {
24
+ const paymentProcessor = vi.fn();
25
+ const manager = new StreamingPaymentManager({ dbPath: await createDbPath(), now: () => 1_700_000_000_000, paymentProcessor });
26
+
27
+ expect(manager.startStream('task-1', 1_000n, 400n)).toEqual({
28
+ taskId: 'task-1',
29
+ totalPaid: 0n,
30
+ maxBudget: 1_000n,
31
+ currentUnit: 0,
32
+ lastPaymentTimestamp: 1_700_000_000_000,
33
+ });
34
+ await expect(manager.payUnit('task-1')).resolves.toMatchObject({ totalPaid: 400n, currentUnit: 1 });
35
+ await expect(manager.payUnit('task-1')).resolves.toMatchObject({ totalPaid: 800n, currentUnit: 2 });
36
+ await expect(manager.payUnit('task-1')).resolves.toMatchObject({ totalPaid: 1_000n, currentUnit: 3 });
37
+
38
+ expect(paymentProcessor).toHaveBeenCalledTimes(3);
39
+ expect(manager.getAuditTrail('task-1')).toHaveLength(3);
40
+ expect(manager.finalizeStream('task-1')).toEqual({
41
+ state: {
42
+ taskId: 'task-1',
43
+ totalPaid: 1_000n,
44
+ maxBudget: 1_000n,
45
+ currentUnit: 3,
46
+ lastPaymentTimestamp: 1_700_000_000_000,
47
+ },
48
+ refundAmount: 0n,
49
+ });
50
+ manager.close();
51
+ });
52
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { HashChain } from '../../src/metering/hash-chain.js';
4
+ import { ResultVerifier, createMeteredResultEnvelope, decodeMeteredResult, getMeteredResultUnits, parseMeteredResultEnvelope, serializeMeteredResultEnvelope } from '../../src/metering/verification.js';
5
+
6
+ describe('ResultVerifier', () => {
7
+ it('verifies metered result envelopes against task hashes', () => {
8
+ const seed = new TextEncoder().encode('agentic-mesh:metering:v1:task-1');
9
+ const chain = new HashChain(seed);
10
+ const units = [
11
+ new TextEncoder().encode('chunk-a'),
12
+ new TextEncoder().encode('chunk-b'),
13
+ ];
14
+ for (const unit of units) {
15
+ chain.addUnit(unit);
16
+ }
17
+
18
+ const proof = chain.getProof();
19
+ const envelope = createMeteredResultEnvelope(new Uint8Array(Buffer.concat(units.map((unit) => Buffer.from(unit)))), proof, 7);
20
+ const parsed = parseMeteredResultEnvelope(serializeMeteredResultEnvelope(envelope));
21
+ const verifier = new ResultVerifier();
22
+
23
+ expect(parsed).not.toBeNull();
24
+ expect(Buffer.from(decodeMeteredResult(parsed!)).toString('utf8')).toBe('chunk-achunk-b');
25
+ expect(verifier.verify({ id: 'task-1', verificationHash: proof.root }, parsed!.proof, getMeteredResultUnits(parsed!))).toBe(true);
26
+ });
27
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { PaymentRailSelector } from '../../src/index.js';
4
+
5
+ const selector = new PaymentRailSelector();
6
+
7
+ describe('PaymentRailSelector', () => {
8
+ it('always chooses Sui escrow for async tasks', () => {
9
+ expect(
10
+ selector.selectRail({
11
+ executionMode: 'async',
12
+ consumerHasSuiWallet: true,
13
+ consumerHasEvmWallet: true,
14
+ providerAcceptsSui: true,
15
+ providerAcceptsX402: true,
16
+ amount: 1n,
17
+ currency: 'USDC',
18
+ }),
19
+ ).toBe('sui-escrow');
20
+ });
21
+
22
+ it('prefers direct Sui settlement for sync tasks when both parties support it', () => {
23
+ expect(
24
+ selector.selectRail({
25
+ executionMode: 'sync',
26
+ consumerHasSuiWallet: true,
27
+ consumerHasEvmWallet: true,
28
+ providerAcceptsSui: true,
29
+ providerAcceptsX402: true,
30
+ amount: 1n,
31
+ currency: 'USDC',
32
+ }),
33
+ ).toBe('sui-transfer');
34
+ });
35
+
36
+ it('falls back to x402 when only EVM payment is available', () => {
37
+ expect(
38
+ selector.selectRail({
39
+ executionMode: 'sync',
40
+ consumerHasSuiWallet: false,
41
+ consumerHasEvmWallet: true,
42
+ providerAcceptsSui: false,
43
+ providerAcceptsX402: true,
44
+ amount: 1n,
45
+ currency: 'USDC',
46
+ }),
47
+ ).toBe('x402-base');
48
+ });
49
+
50
+ it('throws when no payment capability is available', () => {
51
+ expect(() =>
52
+ selector.selectRail({
53
+ executionMode: 'sync',
54
+ consumerHasSuiWallet: false,
55
+ consumerHasEvmWallet: false,
56
+ providerAcceptsSui: false,
57
+ providerAcceptsX402: false,
58
+ amount: 1n,
59
+ currency: 'USDC',
60
+ }),
61
+ ).toThrow('No compatible payment rail is available for this task.');
62
+ });
63
+
64
+ it('returns expected rails across wallet/provider combinations', () => {
65
+ const cases = [
66
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: false, providerAcceptsSui: false, providerAcceptsX402: false, expected: [] },
67
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: false, providerAcceptsSui: false, providerAcceptsX402: false, expected: [] },
68
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: true, providerAcceptsSui: false, providerAcceptsX402: false, expected: [] },
69
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: true, providerAcceptsSui: false, providerAcceptsX402: false, expected: [] },
70
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: false, providerAcceptsSui: true, providerAcceptsX402: false, expected: [] },
71
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: false, providerAcceptsSui: true, providerAcceptsX402: false, expected: ['sui-transfer'] },
72
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: true, providerAcceptsSui: true, providerAcceptsX402: false, expected: [] },
73
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: true, providerAcceptsSui: true, providerAcceptsX402: false, expected: ['sui-transfer'] },
74
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: false, providerAcceptsSui: false, providerAcceptsX402: true, expected: [] },
75
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: false, providerAcceptsSui: false, providerAcceptsX402: true, expected: [] },
76
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: true, providerAcceptsSui: false, providerAcceptsX402: true, expected: ['x402-base'] },
77
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: true, providerAcceptsSui: false, providerAcceptsX402: true, expected: ['x402-base'] },
78
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: false, providerAcceptsSui: true, providerAcceptsX402: true, expected: [] },
79
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: false, providerAcceptsSui: true, providerAcceptsX402: true, expected: ['sui-transfer'] },
80
+ { consumerHasSuiWallet: false, consumerHasEvmWallet: true, providerAcceptsSui: true, providerAcceptsX402: true, expected: ['x402-base'] },
81
+ { consumerHasSuiWallet: true, consumerHasEvmWallet: true, providerAcceptsSui: true, providerAcceptsX402: true, expected: ['sui-transfer', 'x402-base'] },
82
+ ] as const;
83
+
84
+ for (const testCase of cases) {
85
+ expect(
86
+ selector.getAvailableRails({
87
+ executionMode: 'sync',
88
+ amount: 1n,
89
+ currency: 'USDC',
90
+ ...testCase,
91
+ }),
92
+ ).toEqual(testCase.expected);
93
+ }
94
+ });
95
+ });