@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.
- package/.test-data/05a845c4-1682-41b9-97e5-65a5263156c0/spending.sqlite +0 -0
- package/.test-data/10e18ba5-98f0-42e0-8899-06a09459ae85/agents.sqlite +0 -0
- package/.test-data/20464456-23cb-4ff7-8df5-3c129fb95a90/agents.sqlite +0 -0
- package/.test-data/2e2ec66c-e8b4-43eb-945f-cca84ed0a0f6/agents.sqlite +0 -0
- package/.test-data/2f5ef9b7-01da-4ba0-a6fa-af8063a2567c/agents.sqlite +0 -0
- package/.test-data/3ac13cd5-84c9-4e71-8b89-6d3ef4dd819c/agents.sqlite +0 -0
- package/.test-data/40b7dd4a-fae0-4a5d-a6a0-64c9048af35e/agents.sqlite +0 -0
- package/.test-data/59511a04-42f8-4286-bb1e-733795e08749/agents.sqlite +0 -0
- package/.test-data/6778e8c5-6eb5-416d-8aca-9d559cdf83e4/agents.sqlite +0 -0
- package/.test-data/7648dc86-df90-4460-8f9c-55cdb481324b/agents.sqlite +0 -0
- package/.test-data/77162d98-6b22-41b1-a50f-536fab739f8a/agents.sqlite +0 -0
- package/.test-data/798dbdab-cbe7-4edd-8a5b-ae99c285fe17/agents.sqlite +0 -0
- package/.test-data/8033d6ac-8b85-454d-b708-00ac695b22f8/agents.sqlite +0 -0
- package/.test-data/9155a4f4-cda3-487c-9eed-921c82d7550f/agents.sqlite +0 -0
- package/.test-data/9bfeee53-c231-46c3-8c93-2180933f5d50/agents.sqlite +0 -0
- package/.test-data/a4c64287-79f6-46e9-847d-2803c63a74fd/agents.sqlite +0 -0
- package/.test-data/b8f58952-1ed8-46ff-abd7-21fb86e9457f/agents.sqlite +0 -0
- package/.test-data/c3060504-3187-41ed-8532-82332be48b0b/spending.sqlite +0 -0
- package/.test-data/cc471629-8006-4fc1-b8a1-399d2df2cc4e/agents.sqlite +0 -0
- package/.test-data/dbca3bef-397d-4bbc-bd4c-4f7b14103e04/spending.sqlite +0 -0
- package/.test-data/f1283dd1-6602-4de7-a050-16aac7abc288/agents.sqlite +0 -0
- package/.turbo/turbo-build.log +14 -0
- package/dist/index.d.ts +1675 -0
- package/dist/index.js +8006 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/auth/device-flow.ts +108 -0
- package/src/auth/ed25519-provider.ts +43 -0
- package/src/auth/errors.ts +82 -0
- package/src/auth/evm-key.ts +55 -0
- package/src/auth/index.ts +8 -0
- package/src/auth/session-state.ts +25 -0
- package/src/auth/session-store.ts +510 -0
- package/src/auth/types.ts +81 -0
- package/src/auth/zklogin-provider.ts +902 -0
- package/src/blobstore/WALRUS_FINDINGS.md +284 -0
- package/src/blobstore/encrypted-store.ts +56 -0
- package/src/blobstore/fs-store.ts +91 -0
- package/src/blobstore/hybrid-store.ts +144 -0
- package/src/blobstore/index.ts +5 -0
- package/src/blobstore/interface.ts +33 -0
- package/src/blobstore/walrus-spike.ts +345 -0
- package/src/blobstore/walrus-store.ts +551 -0
- package/src/cache/agent-cache.ts +403 -0
- package/src/cache/index.ts +1 -0
- package/src/crypto/encryption.ts +152 -0
- package/src/crypto/index.ts +2 -0
- package/src/crypto/x25519.ts +41 -0
- package/src/dispute/client.ts +191 -0
- package/src/dispute/index.ts +1 -0
- package/src/events/index.ts +2 -0
- package/src/events/parser.ts +291 -0
- package/src/events/subscription.ts +131 -0
- package/src/evm/constants.ts +6 -0
- package/src/evm/index.ts +2 -0
- package/src/evm/wallet.ts +136 -0
- package/src/identity/did.ts +36 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keypair.ts +199 -0
- package/src/identity/signing.ts +28 -0
- package/src/index.ts +22 -0
- package/src/internal/parsing.ts +416 -0
- package/src/marketplace/client.ts +349 -0
- package/src/marketplace/index.ts +1 -0
- package/src/metering/hash-chain.ts +94 -0
- package/src/metering/index.ts +4 -0
- package/src/metering/meter.ts +80 -0
- package/src/metering/streaming.ts +196 -0
- package/src/metering/verification.ts +104 -0
- package/src/payment/index.ts +1 -0
- package/src/payment/rail-selector.ts +41 -0
- package/src/registry/client.ts +328 -0
- package/src/registry/index.ts +1 -0
- package/src/relay/consumer-client.ts +497 -0
- package/src/relay/index.ts +1 -0
- package/src/relay-registry/client.ts +295 -0
- package/src/relay-registry/discovery.ts +109 -0
- package/src/relay-registry/index.ts +2 -0
- package/src/reputation/anchor-client.ts +126 -0
- package/src/reputation/event-publisher.ts +67 -0
- package/src/reputation/index.ts +5 -0
- package/src/reputation/merkle.ts +79 -0
- package/src/reputation/score-calculator.ts +133 -0
- package/src/reputation/serialization.ts +37 -0
- package/src/reputation/store.ts +165 -0
- package/src/reputation/validation.ts +135 -0
- package/src/routing/circuit-breaker.ts +111 -0
- package/src/routing/fan-out.ts +266 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/performance.ts +244 -0
- package/src/routing/selector.ts +225 -0
- package/src/spending/index.ts +1 -0
- package/src/spending/policy.ts +271 -0
- package/src/staking/client.ts +319 -0
- package/src/staking/index.ts +1 -0
- package/src/sui/client.ts +214 -0
- package/src/sui/index.ts +2 -0
- package/src/sui/tx-helpers.ts +1070 -0
- package/src/task/client.ts +215 -0
- package/src/task/index.ts +1 -0
- package/src/x402/client.ts +295 -0
- package/src/x402/index.ts +1 -0
- package/tests/auth/device-flow.test.ts +62 -0
- package/tests/auth/ed25519-provider.test.ts +24 -0
- package/tests/auth/evm-key.test.ts +31 -0
- package/tests/auth/session-store.test.ts +201 -0
- package/tests/auth/zklogin-provider.test.ts +366 -0
- package/tests/blobstore/encrypted-store.test.ts +78 -0
- package/tests/blobstore.test.ts +91 -0
- package/tests/cache.test.ts +124 -0
- package/tests/crypto/encryption.test.ts +70 -0
- package/tests/crypto/x25519.test.ts +47 -0
- package/tests/dispute/client.test.ts +238 -0
- package/tests/events.test.ts +202 -0
- package/tests/evm/wallet.test.ts +101 -0
- package/tests/hybrid-store.test.ts +121 -0
- package/tests/identity.test.ts +161 -0
- package/tests/marketplace.test.ts +308 -0
- package/tests/metering/hash-chain.test.ts +32 -0
- package/tests/metering/meter.test.ts +23 -0
- package/tests/metering/streaming.test.ts +52 -0
- package/tests/metering/verification.test.ts +27 -0
- package/tests/payment/rail-selector.test.ts +95 -0
- package/tests/registry.test.ts +183 -0
- package/tests/relay-consumer-client.test.ts +119 -0
- package/tests/relay-registry/client.test.ts +261 -0
- package/tests/reputation/event-publisher.test.ts +70 -0
- package/tests/reputation/merkle.test.ts +44 -0
- package/tests/reputation/score-calculator.test.ts +104 -0
- package/tests/reputation/store.test.ts +94 -0
- package/tests/routing/circuit-breaker.test.ts +45 -0
- package/tests/routing/fan-out.test.ts +123 -0
- package/tests/routing/performance.test.ts +49 -0
- package/tests/routing/selector.test.ts +114 -0
- package/tests/spending.test.ts +133 -0
- package/tests/staking/client.test.ts +286 -0
- package/tests/sui-client.test.ts +85 -0
- package/tests/task.test.ts +249 -0
- package/tests/tx-helpers.test.ts +70 -0
- package/tests/walrus-spike.test.ts +100 -0
- package/tests/walrus-store.test.ts +196 -0
- package/tests/x402/client.test.ts +116 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { BlobIntegrityError, FilesystemBlobStore } from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
const createdPaths: string[] = [];
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await Promise.all(
|
|
14
|
+
createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function createBaseDir(): Promise<string> {
|
|
19
|
+
const baseDir = resolve(process.cwd(), '.test-data', randomUUID());
|
|
20
|
+
createdPaths.push(baseDir);
|
|
21
|
+
await mkdir(baseDir, { recursive: true });
|
|
22
|
+
return baseDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('FilesystemBlobStore', () => {
|
|
26
|
+
it('stores blobs with SHA-256 metadata', async () => {
|
|
27
|
+
const store = new FilesystemBlobStore(await createBaseDir());
|
|
28
|
+
const data = encoder.encode('hello world');
|
|
29
|
+
|
|
30
|
+
const result = await store.store(data);
|
|
31
|
+
const checksum = createHash('sha256').update(data).digest('hex');
|
|
32
|
+
|
|
33
|
+
expect(result).toMatchObject({
|
|
34
|
+
blobId: checksum,
|
|
35
|
+
hash: checksum,
|
|
36
|
+
checksum,
|
|
37
|
+
contentHash: checksum,
|
|
38
|
+
size: data.byteLength,
|
|
39
|
+
});
|
|
40
|
+
expect(result.storedAt).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('fetches stored blobs and returns metadata', async () => {
|
|
44
|
+
const baseDir = await createBaseDir();
|
|
45
|
+
const store = new FilesystemBlobStore(baseDir);
|
|
46
|
+
const data = encoder.encode('fetch me');
|
|
47
|
+
const { blobId } = await store.store(data);
|
|
48
|
+
|
|
49
|
+
const fetched = await store.fetch(blobId);
|
|
50
|
+
const metadata = await store.getMetadata(blobId);
|
|
51
|
+
|
|
52
|
+
expect(Buffer.from(fetched ?? [])).toEqual(Buffer.from(data));
|
|
53
|
+
expect(metadata).toMatchObject({
|
|
54
|
+
blobId,
|
|
55
|
+
contentHash: blobId,
|
|
56
|
+
size: data.byteLength,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns null for missing blobs and after delete', async () => {
|
|
61
|
+
const store = new FilesystemBlobStore(await createBaseDir());
|
|
62
|
+
const data = encoder.encode('delete me');
|
|
63
|
+
const { blobId } = await store.store(data);
|
|
64
|
+
|
|
65
|
+
expect(await store.fetch('missing')).toBeNull();
|
|
66
|
+
await store.delete(blobId);
|
|
67
|
+
expect(await store.fetch(blobId)).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('uses content-addressed ids for identical blobs', async () => {
|
|
71
|
+
const store = new FilesystemBlobStore(await createBaseDir());
|
|
72
|
+
const data = encoder.encode('same data');
|
|
73
|
+
|
|
74
|
+
const first = await store.store(data);
|
|
75
|
+
const second = await store.store(data);
|
|
76
|
+
|
|
77
|
+
expect(first.blobId).toBe(second.blobId);
|
|
78
|
+
expect(await store.exists(first.blobId)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('throws on content corruption', async () => {
|
|
82
|
+
const baseDir = await createBaseDir();
|
|
83
|
+
const store = new FilesystemBlobStore(baseDir);
|
|
84
|
+
const data = encoder.encode('keep me safe');
|
|
85
|
+
const { blobId } = await store.store(data);
|
|
86
|
+
|
|
87
|
+
await writeFile(join(baseDir, blobId), encoder.encode('tampered'));
|
|
88
|
+
|
|
89
|
+
await expect(store.fetch(blobId)).rejects.toBeInstanceOf(BlobIntegrityError);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
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 { AgentCache, PaymentRail, type AgentCard } from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
const createdPaths: string[] = [];
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await Promise.all(
|
|
13
|
+
createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function createDbPath(): Promise<string> {
|
|
18
|
+
const dir = resolve(process.cwd(), '.test-data', randomUUID());
|
|
19
|
+
createdPaths.push(dir);
|
|
20
|
+
await mkdir(dir, { recursive: true });
|
|
21
|
+
return resolve(dir, 'agents.sqlite');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createAgent(overrides: Partial<AgentCard> = {}): AgentCard {
|
|
25
|
+
return {
|
|
26
|
+
id: '0xagent-1',
|
|
27
|
+
owner: '0xowner',
|
|
28
|
+
did: 'did:mesh:agent-1' as AgentCard['did'],
|
|
29
|
+
name: 'Summarizer',
|
|
30
|
+
description: 'Summarizes long documents',
|
|
31
|
+
capabilities: [
|
|
32
|
+
{
|
|
33
|
+
name: 'summarize',
|
|
34
|
+
description: 'Summarize text',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
pricing: {
|
|
37
|
+
rail: PaymentRail.SUI_ESCROW,
|
|
38
|
+
amount: 100n,
|
|
39
|
+
currency: 'MIST',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
endpoint: 'https://example.com',
|
|
44
|
+
active: true,
|
|
45
|
+
version: 1,
|
|
46
|
+
registeredAt: 1_000,
|
|
47
|
+
updatedAt: 1_000,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('AgentCache', () => {
|
|
53
|
+
it('upserts and reads agents', async () => {
|
|
54
|
+
const cache = new AgentCache(await createDbPath());
|
|
55
|
+
const agent = createAgent();
|
|
56
|
+
|
|
57
|
+
cache.upsertAgent(agent);
|
|
58
|
+
|
|
59
|
+
expect(cache.getAgent(agent.id)).toEqual(agent);
|
|
60
|
+
cache.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('updates existing agents', async () => {
|
|
64
|
+
const cache = new AgentCache(await createDbPath());
|
|
65
|
+
cache.upsertAgent(createAgent());
|
|
66
|
+
cache.upsertAgent(createAgent({ name: 'Translator', version: 2, updatedAt: 2_000 }));
|
|
67
|
+
|
|
68
|
+
const updated = cache.getAgent('0xagent-1');
|
|
69
|
+
expect(updated?.name).toBe('Translator');
|
|
70
|
+
expect(updated?.version).toBe(2);
|
|
71
|
+
cache.close();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('supports FTS capability searches', async () => {
|
|
75
|
+
const cache = new AgentCache(await createDbPath());
|
|
76
|
+
cache.upsertAgent(createAgent());
|
|
77
|
+
|
|
78
|
+
const matches = cache.searchByCapability('summarize');
|
|
79
|
+
expect(matches.map((entry) => entry.id)).toContain('0xagent-1');
|
|
80
|
+
cache.close();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('removes agents', async () => {
|
|
84
|
+
const cache = new AgentCache(await createDbPath());
|
|
85
|
+
cache.upsertAgent(createAgent());
|
|
86
|
+
cache.removeAgent('0xagent-1');
|
|
87
|
+
|
|
88
|
+
expect(cache.getAgent('0xagent-1')).toBeNull();
|
|
89
|
+
cache.close();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('finds agents by DID', async () => {
|
|
93
|
+
const cache = new AgentCache(await createDbPath());
|
|
94
|
+
cache.upsertAgent(createAgent());
|
|
95
|
+
|
|
96
|
+
expect(cache.getAgentByDID('did:mesh:agent-1')?.id).toBe('0xagent-1');
|
|
97
|
+
cache.close();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns only active agents', async () => {
|
|
101
|
+
const cache = new AgentCache(await createDbPath());
|
|
102
|
+
cache.upsertAgent(createAgent());
|
|
103
|
+
cache.upsertAgent(
|
|
104
|
+
createAgent({ id: '0xagent-2', did: 'did:mesh:agent-2' as AgentCard['did'], active: false }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(cache.getAllActive().map((entry) => entry.id)).toEqual(['0xagent-1']);
|
|
108
|
+
cache.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('supports advanced queries locally and via delegate', async () => {
|
|
112
|
+
const delegated = [createAgent({ id: '0xdelegate', did: 'did:mesh:delegate' as AgentCard['did'] })];
|
|
113
|
+
const delegate = vi.fn().mockResolvedValue(delegated);
|
|
114
|
+
const delegatedCache = new AgentCache(await createDbPath(), { queryDelegate: delegate });
|
|
115
|
+
await expect(delegatedCache.queryAgentsAdvanced({ capability: 'summarize' })).resolves.toEqual(delegated);
|
|
116
|
+
delegatedCache.close();
|
|
117
|
+
|
|
118
|
+
const cache = new AgentCache(await createDbPath());
|
|
119
|
+
cache.upsertAgent(createAgent({ totalTasksCompleted: 4, totalTasksFailed: 1 }));
|
|
120
|
+
const results = await cache.queryAgentsAdvanced({ capability: 'summarize', minReputation: 0.7, sortBy: 'reputation' });
|
|
121
|
+
expect(results).toHaveLength(1);
|
|
122
|
+
cache.close();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
decryptFromSender,
|
|
5
|
+
encryptForRecipient,
|
|
6
|
+
generateX25519KeyPair,
|
|
7
|
+
parseEncryptedPayload,
|
|
8
|
+
serializeEncryptedPayload,
|
|
9
|
+
} from '../../src/index.js';
|
|
10
|
+
|
|
11
|
+
function createLargePayload(): Uint8Array {
|
|
12
|
+
return Uint8Array.from({ length: 1024 * 1024 }, (_value, index) => index % 251);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('payload encryption', () => {
|
|
16
|
+
it('encrypts and decrypts payloads', async () => {
|
|
17
|
+
const sender = generateX25519KeyPair();
|
|
18
|
+
const recipient = generateX25519KeyPair();
|
|
19
|
+
const plaintext = new TextEncoder().encode('hello encrypted mesh');
|
|
20
|
+
|
|
21
|
+
const payload = await encryptForRecipient(plaintext, sender.privateKey, recipient.publicKey);
|
|
22
|
+
const decrypted = await decryptFromSender(payload, recipient.privateKey);
|
|
23
|
+
|
|
24
|
+
expect(Buffer.from(decrypted)).toEqual(Buffer.from(plaintext));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('fails to decrypt with the wrong recipient key', async () => {
|
|
28
|
+
const sender = generateX25519KeyPair();
|
|
29
|
+
const recipient = generateX25519KeyPair();
|
|
30
|
+
const outsider = generateX25519KeyPair();
|
|
31
|
+
const payload = await encryptForRecipient(new TextEncoder().encode('secret'), sender.privateKey, recipient.publicKey);
|
|
32
|
+
|
|
33
|
+
await expect(decryptFromSender(payload, outsider.privateKey)).rejects.toBeInstanceOf(Error);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('uses a fresh nonce for each encryption', async () => {
|
|
37
|
+
const sender = generateX25519KeyPair();
|
|
38
|
+
const recipient = generateX25519KeyPair();
|
|
39
|
+
const plaintext = new TextEncoder().encode('repeatable input');
|
|
40
|
+
|
|
41
|
+
const first = await encryptForRecipient(plaintext, sender.privateKey, recipient.publicKey);
|
|
42
|
+
const second = await encryptForRecipient(plaintext, sender.privateKey, recipient.publicKey);
|
|
43
|
+
|
|
44
|
+
expect(first.nonce).not.toBe(second.nonce);
|
|
45
|
+
expect(first.ciphertext).not.toBe(second.ciphertext);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('handles large payloads', async () => {
|
|
49
|
+
const sender = generateX25519KeyPair();
|
|
50
|
+
const recipient = generateX25519KeyPair();
|
|
51
|
+
const plaintext = createLargePayload();
|
|
52
|
+
|
|
53
|
+
const payload = await encryptForRecipient(plaintext, sender.privateKey, recipient.publicKey);
|
|
54
|
+
const decrypted = await decryptFromSender(payload, recipient.privateKey);
|
|
55
|
+
|
|
56
|
+
expect(Buffer.from(decrypted)).toEqual(Buffer.from(plaintext));
|
|
57
|
+
}, 15_000);
|
|
58
|
+
|
|
59
|
+
it('serializes and parses encrypted payloads', async () => {
|
|
60
|
+
const sender = generateX25519KeyPair();
|
|
61
|
+
const recipient = generateX25519KeyPair();
|
|
62
|
+
const payload = await encryptForRecipient(new TextEncoder().encode('serialize me'), sender.privateKey, recipient.publicKey);
|
|
63
|
+
|
|
64
|
+
const serialized = serializeEncryptedPayload(payload);
|
|
65
|
+
const parsed = parseEncryptedPayload(serialized);
|
|
66
|
+
|
|
67
|
+
expect(parsed).toEqual(payload);
|
|
68
|
+
await expect(decryptFromSender(parsed as NonNullable<typeof parsed>, recipient.privateKey)).resolves.toBeInstanceOf(Uint8Array);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { computeSharedSecret, ed25519ToX25519, generateX25519KeyPair } from '../../src/index.js';
|
|
4
|
+
|
|
5
|
+
function fixedSecret(start: number): Uint8Array {
|
|
6
|
+
return Uint8Array.from({ length: 32 }, (_value, index) => (start + index) % 256);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('x25519', () => {
|
|
10
|
+
it('generates 32-byte keypairs', () => {
|
|
11
|
+
const keyPair = generateX25519KeyPair();
|
|
12
|
+
|
|
13
|
+
expect(keyPair.privateKey).toHaveLength(32);
|
|
14
|
+
expect(keyPair.publicKey).toHaveLength(32);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('converts Ed25519 keys deterministically', () => {
|
|
18
|
+
const secretKey = fixedSecret(1);
|
|
19
|
+
|
|
20
|
+
const first = ed25519ToX25519(secretKey);
|
|
21
|
+
const second = ed25519ToX25519(secretKey);
|
|
22
|
+
|
|
23
|
+
expect(Buffer.from(first.privateKey)).toEqual(Buffer.from(second.privateKey));
|
|
24
|
+
expect(Buffer.from(first.publicKey)).toEqual(Buffer.from(second.publicKey));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('derives the same shared secret for both peers', () => {
|
|
28
|
+
const alice = generateX25519KeyPair();
|
|
29
|
+
const bob = generateX25519KeyPair();
|
|
30
|
+
|
|
31
|
+
const aliceShared = computeSharedSecret(alice.privateKey, bob.publicKey);
|
|
32
|
+
const bobShared = computeSharedSecret(bob.privateKey, alice.publicKey);
|
|
33
|
+
|
|
34
|
+
expect(Buffer.from(aliceShared)).toEqual(Buffer.from(bobShared));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('produces different shared secrets for different peers', () => {
|
|
38
|
+
const alice = generateX25519KeyPair();
|
|
39
|
+
const bob = generateX25519KeyPair();
|
|
40
|
+
const carol = generateX25519KeyPair();
|
|
41
|
+
|
|
42
|
+
const sharedWithBob = computeSharedSecret(alice.privateKey, bob.publicKey);
|
|
43
|
+
const sharedWithCarol = computeSharedSecret(alice.privateKey, carol.publicKey);
|
|
44
|
+
|
|
45
|
+
expect(Buffer.from(sharedWithBob)).not.toEqual(Buffer.from(sharedWithCarol));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { DisputeClient, DisputeStatus, type MeshSuiClient } from '../../src/index.js';
|
|
5
|
+
|
|
6
|
+
const contractConfig = { packageId: '0x1' };
|
|
7
|
+
|
|
8
|
+
function getMoveTargets(tx: { getData: () => { commands: Array<Record<string, unknown>> } }): string[] {
|
|
9
|
+
return tx
|
|
10
|
+
.getData()
|
|
11
|
+
.commands.map((command) => {
|
|
12
|
+
if ('MoveCall' in command && typeof command.MoveCall === 'object' && command.MoveCall) {
|
|
13
|
+
const moveCall = command.MoveCall as {
|
|
14
|
+
package: string;
|
|
15
|
+
module: string;
|
|
16
|
+
function: string;
|
|
17
|
+
};
|
|
18
|
+
return `${moveCall.package}::${moveCall.module}::${moveCall.function}`;
|
|
19
|
+
}
|
|
20
|
+
return '';
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('DisputeClient', () => {
|
|
26
|
+
it('opens disputes and returns the created dispute id', async () => {
|
|
27
|
+
const executeTransaction = vi.fn().mockResolvedValue({
|
|
28
|
+
digest: '0xtx',
|
|
29
|
+
objectChanges: [
|
|
30
|
+
{
|
|
31
|
+
type: 'created',
|
|
32
|
+
objectType: '0x1::dispute::Dispute',
|
|
33
|
+
objectId: '0xdispute',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
events: [],
|
|
37
|
+
});
|
|
38
|
+
const client = new DisputeClient(
|
|
39
|
+
{
|
|
40
|
+
executeTransaction,
|
|
41
|
+
getObject: vi.fn(),
|
|
42
|
+
queryEvents: vi.fn(),
|
|
43
|
+
} as unknown as MeshSuiClient,
|
|
44
|
+
contractConfig,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const result = await client.openDispute({
|
|
48
|
+
taskId: '0x3',
|
|
49
|
+
evidenceBlobId: 'walrus:evidence',
|
|
50
|
+
proposedSplitMist: 500n,
|
|
51
|
+
arbitratorAddress: '0x2',
|
|
52
|
+
signer: {} as Ed25519Keypair,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const tx = executeTransaction.mock.calls[0]?.[0];
|
|
56
|
+
expect(getMoveTargets(tx).some((target) => target.endsWith('::dispute::open_dispute'))).toBe(true);
|
|
57
|
+
expect(result).toEqual({ disputeId: '0xdispute', txDigest: '0xtx' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('supports respond, accept, and arbitrate flows', async () => {
|
|
61
|
+
const executeTransaction = vi
|
|
62
|
+
.fn()
|
|
63
|
+
.mockResolvedValueOnce({ digest: '0xrespond', events: [], objectChanges: [] })
|
|
64
|
+
.mockResolvedValueOnce({
|
|
65
|
+
digest: '0xaccept',
|
|
66
|
+
events: [
|
|
67
|
+
{
|
|
68
|
+
type: '0x1::dispute::DisputeMutuallyResolved',
|
|
69
|
+
parsedJson: { requester_amount: '250', provider_amount: '750' },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
objectChanges: [],
|
|
73
|
+
})
|
|
74
|
+
.mockResolvedValueOnce({ digest: '0xarb', events: [], objectChanges: [] });
|
|
75
|
+
const client = new DisputeClient(
|
|
76
|
+
{
|
|
77
|
+
executeTransaction,
|
|
78
|
+
getObject: vi.fn(),
|
|
79
|
+
queryEvents: vi.fn(),
|
|
80
|
+
} as unknown as MeshSuiClient,
|
|
81
|
+
contractConfig,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await expect(client.respondToDispute({
|
|
85
|
+
disputeId: '0x4',
|
|
86
|
+
evidenceBlobId: 'walrus:reply',
|
|
87
|
+
proposedSplitMist: 250n,
|
|
88
|
+
signer: {} as Ed25519Keypair,
|
|
89
|
+
})).resolves.toEqual({ txDigest: '0xrespond' });
|
|
90
|
+
|
|
91
|
+
await expect(client.acceptResolution({
|
|
92
|
+
disputeId: '0x4',
|
|
93
|
+
taskId: '0x3',
|
|
94
|
+
signer: {} as Ed25519Keypair,
|
|
95
|
+
})).resolves.toEqual({ requesterAmount: 250n, providerAmount: 750n, txDigest: '0xaccept' });
|
|
96
|
+
|
|
97
|
+
await expect(client.arbitrate({
|
|
98
|
+
disputeId: '0x4',
|
|
99
|
+
taskId: '0x3',
|
|
100
|
+
rulingSplitMist: 400n,
|
|
101
|
+
signer: {} as Ed25519Keypair,
|
|
102
|
+
})).resolves.toEqual({ txDigest: '0xarb' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('parses dispute objects', async () => {
|
|
106
|
+
const client = new DisputeClient(
|
|
107
|
+
{
|
|
108
|
+
executeTransaction: vi.fn(),
|
|
109
|
+
getObject: vi.fn().mockResolvedValue({
|
|
110
|
+
objectId: '0xdispute',
|
|
111
|
+
task_id: '0xtask',
|
|
112
|
+
requester: '0xrequester',
|
|
113
|
+
provider: '0xprovider',
|
|
114
|
+
escrow_amount: '1000',
|
|
115
|
+
status: 1,
|
|
116
|
+
requester_evidence_blob: 'walrus:req',
|
|
117
|
+
provider_evidence_blob: 'walrus:prov',
|
|
118
|
+
requester_proposed_split: '600',
|
|
119
|
+
provider_proposed_split: '400',
|
|
120
|
+
arbitrator: '0x0',
|
|
121
|
+
ruling_split: '0',
|
|
122
|
+
opened_at: 100,
|
|
123
|
+
responded_at: 200,
|
|
124
|
+
resolved_at: 0,
|
|
125
|
+
resolution_deadline: 300,
|
|
126
|
+
}),
|
|
127
|
+
queryEvents: vi.fn(),
|
|
128
|
+
} as unknown as MeshSuiClient,
|
|
129
|
+
contractConfig,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const dispute = await client.getDispute('0xdispute');
|
|
133
|
+
|
|
134
|
+
expect(dispute).toEqual({
|
|
135
|
+
id: '0xdispute',
|
|
136
|
+
taskId: '0xtask',
|
|
137
|
+
requester: '0xrequester',
|
|
138
|
+
provider: '0xprovider',
|
|
139
|
+
escrowAmount: 1000n,
|
|
140
|
+
status: DisputeStatus.RESPONDED,
|
|
141
|
+
requesterEvidenceBlob: 'walrus:req',
|
|
142
|
+
providerEvidenceBlob: 'walrus:prov',
|
|
143
|
+
requesterProposedSplit: 600n,
|
|
144
|
+
providerProposedSplit: 400n,
|
|
145
|
+
arbitrator: undefined,
|
|
146
|
+
rulingSplit: 0n,
|
|
147
|
+
openedAt: 100,
|
|
148
|
+
respondedAt: 200,
|
|
149
|
+
resolvedAt: undefined,
|
|
150
|
+
resolutionDeadline: 300,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('finds disputes by task id', async () => {
|
|
155
|
+
const getObject = vi.fn().mockResolvedValue({
|
|
156
|
+
objectId: '0xdispute',
|
|
157
|
+
task_id: '0xtask',
|
|
158
|
+
requester: '0xrequester',
|
|
159
|
+
provider: '0xprovider',
|
|
160
|
+
escrow_amount: '1000',
|
|
161
|
+
status: 0,
|
|
162
|
+
requester_evidence_blob: 'walrus:req',
|
|
163
|
+
provider_evidence_blob: '',
|
|
164
|
+
requester_proposed_split: '500',
|
|
165
|
+
provider_proposed_split: '0',
|
|
166
|
+
arbitrator: '0x3',
|
|
167
|
+
ruling_split: '0',
|
|
168
|
+
opened_at: 100,
|
|
169
|
+
responded_at: 0,
|
|
170
|
+
resolved_at: 0,
|
|
171
|
+
resolution_deadline: 300,
|
|
172
|
+
});
|
|
173
|
+
const client = new DisputeClient(
|
|
174
|
+
{
|
|
175
|
+
executeTransaction: vi.fn(),
|
|
176
|
+
getObject,
|
|
177
|
+
queryEvents: vi.fn().mockResolvedValue({
|
|
178
|
+
events: [
|
|
179
|
+
{
|
|
180
|
+
type: '0x1::dispute::DisputeOpened',
|
|
181
|
+
parsedJson: { dispute_id: '0xdispute', task_id: '0xtask' },
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
nextCursor: null,
|
|
185
|
+
hasMore: false,
|
|
186
|
+
}),
|
|
187
|
+
} as unknown as MeshSuiClient,
|
|
188
|
+
contractConfig,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const dispute = await client.getDisputeByTask('0xtask');
|
|
192
|
+
|
|
193
|
+
expect(getObject).toHaveBeenCalledWith('0xdispute');
|
|
194
|
+
expect(dispute?.id).toBe('0xdispute');
|
|
195
|
+
expect(dispute?.arbitrator).toBe('0x3');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('fails when the mutual resolution event is missing payout fields', async () => {
|
|
199
|
+
const client = new DisputeClient(
|
|
200
|
+
{
|
|
201
|
+
executeTransaction: vi.fn().mockResolvedValue({
|
|
202
|
+
digest: '0xaccept',
|
|
203
|
+
events: [{ type: '0x1::dispute::DisputeMutuallyResolved', parsedJson: {} }],
|
|
204
|
+
objectChanges: [],
|
|
205
|
+
}),
|
|
206
|
+
getObject: vi.fn(),
|
|
207
|
+
queryEvents: vi.fn(),
|
|
208
|
+
} as unknown as MeshSuiClient,
|
|
209
|
+
contractConfig,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
await expect(client.acceptResolution({
|
|
213
|
+
disputeId: '0x4',
|
|
214
|
+
taskId: '0x3',
|
|
215
|
+
signer: {} as Ed25519Keypair,
|
|
216
|
+
})).rejects.toThrow('DisputeMutuallyResolved event did not include requester_amount.');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('rejects invalid dispute ids before submitting dispute mutations', async () => {
|
|
220
|
+
const executeTransaction = vi.fn();
|
|
221
|
+
const client = new DisputeClient(
|
|
222
|
+
{
|
|
223
|
+
executeTransaction,
|
|
224
|
+
getObject: vi.fn(),
|
|
225
|
+
queryEvents: vi.fn(),
|
|
226
|
+
} as unknown as MeshSuiClient,
|
|
227
|
+
contractConfig,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await expect(client.respondToDispute({
|
|
231
|
+
disputeId: 'not-an-object-id',
|
|
232
|
+
evidenceBlobId: 'walrus:reply',
|
|
233
|
+
proposedSplitMist: 1n,
|
|
234
|
+
signer: {} as Ed25519Keypair,
|
|
235
|
+
})).rejects.toThrow('disputeId must be a 0x-prefixed hex object id.');
|
|
236
|
+
expect(executeTransaction).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
});
|