@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,202 @@
|
|
|
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 } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { PaymentScheme, SqliteCursorStore, TaskStatus, parseRawEvent } from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
const createdPaths: string[] = [];
|
|
10
|
+
const packageId = '0xpackage';
|
|
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 createDbPath(): Promise<string> {
|
|
19
|
+
const dir = resolve(process.cwd(), '.test-data', randomUUID());
|
|
20
|
+
createdPaths.push(dir);
|
|
21
|
+
await mkdir(dir, { recursive: true });
|
|
22
|
+
return resolve(dir, 'cursor.sqlite');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('event parsing', () => {
|
|
26
|
+
it('parses AgentRegistered events', () => {
|
|
27
|
+
const event = parseRawEvent(
|
|
28
|
+
{
|
|
29
|
+
id: { txDigest: '0xtx', eventSeq: '0' },
|
|
30
|
+
packageId,
|
|
31
|
+
transactionModule: 'registry',
|
|
32
|
+
type: `${packageId}::registry::AgentRegistered`,
|
|
33
|
+
sender: '0xowner',
|
|
34
|
+
timestampMs: '1000',
|
|
35
|
+
parsedJson: {
|
|
36
|
+
card_id: '0xcard',
|
|
37
|
+
agent: '0xowner',
|
|
38
|
+
did: 'did:mesh:test',
|
|
39
|
+
name: 'Agent One',
|
|
40
|
+
description: 'Helpful agent',
|
|
41
|
+
capabilities: [
|
|
42
|
+
{
|
|
43
|
+
name: 'summarize',
|
|
44
|
+
description: 'Summarizes text',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
price_mist: '100',
|
|
47
|
+
currency: 'MIST',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
endpoint: 'https://example.com',
|
|
51
|
+
active: true,
|
|
52
|
+
version: 1,
|
|
53
|
+
registered_at: 1000,
|
|
54
|
+
updated_at: 1000,
|
|
55
|
+
},
|
|
56
|
+
bcs: '',
|
|
57
|
+
bcsEncoding: 'base64',
|
|
58
|
+
},
|
|
59
|
+
packageId,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(event?.type).toBe('agent.registered');
|
|
63
|
+
if (event?.type !== 'agent.registered') {
|
|
64
|
+
throw new Error('Unexpected event type');
|
|
65
|
+
}
|
|
66
|
+
expect(event.agent.id).toBe('0xcard');
|
|
67
|
+
expect(event.agent.capabilities[0]?.name).toBe('summarize');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('parses TaskPosted events', () => {
|
|
71
|
+
const event = parseRawEvent(
|
|
72
|
+
{
|
|
73
|
+
id: { txDigest: '0xtask', eventSeq: '1' },
|
|
74
|
+
packageId,
|
|
75
|
+
transactionModule: 'task',
|
|
76
|
+
type: `${packageId}::task::TaskPosted`,
|
|
77
|
+
sender: '0xrequester',
|
|
78
|
+
timestampMs: '2000',
|
|
79
|
+
parsedJson: {
|
|
80
|
+
task_id: '0xtask-id',
|
|
81
|
+
requester: '0xrequester',
|
|
82
|
+
provider: '0x0',
|
|
83
|
+
capability: 'summarize',
|
|
84
|
+
category: 'analysis',
|
|
85
|
+
input_blob_id: [105, 110, 112, 117, 116],
|
|
86
|
+
agreement_hash: [104, 97, 115, 104],
|
|
87
|
+
price: '1000',
|
|
88
|
+
status: 0,
|
|
89
|
+
dispute_window_ms: 5000,
|
|
90
|
+
expires_at: 10000,
|
|
91
|
+
created_at: 2000,
|
|
92
|
+
},
|
|
93
|
+
bcs: '',
|
|
94
|
+
bcsEncoding: 'base64',
|
|
95
|
+
},
|
|
96
|
+
packageId,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(event?.type).toBe('task.posted');
|
|
100
|
+
if (event?.type !== 'task.posted') {
|
|
101
|
+
throw new Error('Unexpected event type');
|
|
102
|
+
}
|
|
103
|
+
expect(event.task.id).toBe('0xtask-id');
|
|
104
|
+
expect(event.task.category).toBe('analysis');
|
|
105
|
+
expect(event.task.inputBlobId).toBe('input');
|
|
106
|
+
expect(event.task.status).toBe(TaskStatus.OPEN);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('parses metered completion and release events', () => {
|
|
110
|
+
const completed = parseRawEvent(
|
|
111
|
+
{
|
|
112
|
+
id: { txDigest: '0xcomplete', eventSeq: '2' },
|
|
113
|
+
packageId,
|
|
114
|
+
transactionModule: 'task',
|
|
115
|
+
type: `${packageId}::task::TaskCompleted`,
|
|
116
|
+
sender: '0xprovider',
|
|
117
|
+
timestampMs: '3000',
|
|
118
|
+
parsedJson: {
|
|
119
|
+
task_id: '0xtask-id',
|
|
120
|
+
provider: '0xprovider',
|
|
121
|
+
result_blob_id: [114, 101, 115, 117, 108, 116],
|
|
122
|
+
payment_scheme: 1,
|
|
123
|
+
metered_units: 2,
|
|
124
|
+
verification_hash: Array.from(Buffer.from('aa'.repeat(32), 'hex')),
|
|
125
|
+
completed_at: 3000,
|
|
126
|
+
},
|
|
127
|
+
bcs: '',
|
|
128
|
+
bcsEncoding: 'base64',
|
|
129
|
+
},
|
|
130
|
+
packageId,
|
|
131
|
+
);
|
|
132
|
+
const released = parseRawEvent(
|
|
133
|
+
{
|
|
134
|
+
id: { txDigest: '0xrelease', eventSeq: '3' },
|
|
135
|
+
packageId,
|
|
136
|
+
transactionModule: 'task',
|
|
137
|
+
type: `${packageId}::task::TaskPaymentReleased`,
|
|
138
|
+
sender: '0xrequester',
|
|
139
|
+
timestampMs: '4000',
|
|
140
|
+
parsedJson: {
|
|
141
|
+
task_id: '0xtask-id',
|
|
142
|
+
requester: '0xrequester',
|
|
143
|
+
provider: '0xprovider',
|
|
144
|
+
refund_amount: '7',
|
|
145
|
+
},
|
|
146
|
+
bcs: '',
|
|
147
|
+
bcsEncoding: 'base64',
|
|
148
|
+
},
|
|
149
|
+
packageId,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(completed?.type).toBe('task.completed');
|
|
153
|
+
if (completed?.type !== 'task.completed') {
|
|
154
|
+
throw new Error('Unexpected event type');
|
|
155
|
+
}
|
|
156
|
+
expect(completed.paymentScheme).toBe(PaymentScheme.UPTO);
|
|
157
|
+
expect(completed.meteredUnits).toBe(2);
|
|
158
|
+
expect(completed.verificationHash).toBe('aa'.repeat(32));
|
|
159
|
+
|
|
160
|
+
expect(released?.type).toBe('task.released');
|
|
161
|
+
if (released?.type !== 'task.released') {
|
|
162
|
+
throw new Error('Unexpected event type');
|
|
163
|
+
}
|
|
164
|
+
expect(released.refundAmount).toBe(7n);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns null for unknown event types', () => {
|
|
168
|
+
const event = parseRawEvent(
|
|
169
|
+
{
|
|
170
|
+
id: { txDigest: '0xunknown', eventSeq: '0' },
|
|
171
|
+
packageId,
|
|
172
|
+
transactionModule: 'misc',
|
|
173
|
+
type: `${packageId}::misc::Unknown`,
|
|
174
|
+
sender: '0xsender',
|
|
175
|
+
timestampMs: '0',
|
|
176
|
+
parsedJson: {},
|
|
177
|
+
bcs: '',
|
|
178
|
+
bcsEncoding: 'base64',
|
|
179
|
+
},
|
|
180
|
+
packageId,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(event).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('SqliteCursorStore', () => {
|
|
188
|
+
it('round-trips stored cursors', async () => {
|
|
189
|
+
const store = new SqliteCursorStore(await createDbPath());
|
|
190
|
+
const cursor = { txDigest: '0xtx', eventSeq: '7' };
|
|
191
|
+
|
|
192
|
+
await store.setCursor('event-type', cursor);
|
|
193
|
+
await expect(store.getCursor('event-type')).resolves.toEqual(cursor);
|
|
194
|
+
store.close();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('returns null for unknown event types', async () => {
|
|
198
|
+
const store = new SqliteCursorStore(await createDbPath());
|
|
199
|
+
await expect(store.getCursor('missing')).resolves.toBeNull();
|
|
200
|
+
store.close();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { hexToBytes, recoverMessageAddress, verifyTypedData } from 'viem';
|
|
3
|
+
|
|
4
|
+
import { EvmWallet } from '../../src/index.js';
|
|
5
|
+
|
|
6
|
+
const PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
|
|
7
|
+
const ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
|
|
8
|
+
const TOKEN_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.unstubAllGlobals();
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('EvmWallet', () => {
|
|
16
|
+
it('creates a wallet from a private key with the expected address', () => {
|
|
17
|
+
const wallet = new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'base' });
|
|
18
|
+
|
|
19
|
+
expect(wallet.address).toBe(ADDRESS);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('signs messages with a recoverable signature', async () => {
|
|
23
|
+
const wallet = new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'base' });
|
|
24
|
+
|
|
25
|
+
const signature = await wallet.signMessage('hello x402');
|
|
26
|
+
const recovered = await recoverMessageAddress({ message: 'hello x402', signature: signature as `0x${string}` });
|
|
27
|
+
|
|
28
|
+
expect(recovered).toBe(ADDRESS);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('signs typed data that verifies against the wallet address', async () => {
|
|
32
|
+
const wallet = new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'base-sepolia' });
|
|
33
|
+
const typedData = {
|
|
34
|
+
domain: {
|
|
35
|
+
name: 'AgenticMeshTest',
|
|
36
|
+
version: '1',
|
|
37
|
+
chainId: 84_532,
|
|
38
|
+
verifyingContract: '0x0000000000000000000000000000000000000001',
|
|
39
|
+
},
|
|
40
|
+
types: {
|
|
41
|
+
PaymentAuthorization: [
|
|
42
|
+
{ name: 'paymentAddress', type: 'address' },
|
|
43
|
+
{ name: 'amount', type: 'uint256' },
|
|
44
|
+
{ name: 'nonce', type: 'string' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
primaryType: 'PaymentAuthorization',
|
|
48
|
+
message: {
|
|
49
|
+
paymentAddress: '0x0000000000000000000000000000000000000002',
|
|
50
|
+
amount: 123n,
|
|
51
|
+
nonce: 'nonce-1',
|
|
52
|
+
},
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
const signature = await wallet.signTypedData(typedData);
|
|
56
|
+
const verified = await verifyTypedData({
|
|
57
|
+
address: ADDRESS,
|
|
58
|
+
...typedData,
|
|
59
|
+
signature: signature as `0x${string}`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(verified).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('queries balances through RPC calls', async () => {
|
|
66
|
+
const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
67
|
+
const body = JSON.parse(String(init?.body)) as { id: number; method: string };
|
|
68
|
+
if (body.method === 'eth_getBalance') {
|
|
69
|
+
return new Response(JSON.stringify({ jsonrpc: '2.0', id: body.id, result: '0x5af3107a4000' }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (body.method === 'eth_call') {
|
|
73
|
+
return new Response(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
jsonrpc: '2.0',
|
|
76
|
+
id: body.id,
|
|
77
|
+
result: '0x00000000000000000000000000000000000000000000000000000000000004d2',
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new Error(`Unexpected RPC method: ${body.method}`);
|
|
83
|
+
});
|
|
84
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
85
|
+
|
|
86
|
+
const wallet = new EvmWallet(hexToBytes(PRIVATE_KEY), {
|
|
87
|
+
network: 'base-sepolia',
|
|
88
|
+
rpcUrl: 'https://example.com/rpc',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await expect(wallet.getBalance()).resolves.toBe(100_000_000_000_000n);
|
|
92
|
+
await expect(wallet.getTokenBalance(TOKEN_ADDRESS)).resolves.toBe(1_234n);
|
|
93
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('uses the expected chain ids for supported networks', () => {
|
|
97
|
+
expect(new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'base' }).chain.id).toBe(8453);
|
|
98
|
+
expect(new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'base-sepolia' }).chain.id).toBe(84_532);
|
|
99
|
+
expect(new EvmWallet(hexToBytes(PRIVATE_KEY), { network: 'localhost' }).chain.id).toBe(31_337);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash, 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 { FilesystemBlobStore, HybridBlobStore, createWalrusBlobReference } from '../src/index.js';
|
|
8
|
+
import type { WalrusBlobStore } from '../src/blobstore/walrus-store.js';
|
|
9
|
+
|
|
10
|
+
const createdPaths: string[] = [];
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
|
|
13
|
+
async function createBaseDir(): Promise<string> {
|
|
14
|
+
const baseDir = resolve(process.cwd(), '.test-data', randomUUID());
|
|
15
|
+
createdPaths.push(baseDir);
|
|
16
|
+
await mkdir(baseDir, { recursive: true });
|
|
17
|
+
return baseDir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await Promise.all(createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function sha256(data: Uint8Array): string {
|
|
25
|
+
return createHash('sha256').update(data).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('HybridBlobStore', () => {
|
|
29
|
+
it('uses Walrus when available', async () => {
|
|
30
|
+
const local = new FilesystemBlobStore(await createBaseDir());
|
|
31
|
+
const walrus = {
|
|
32
|
+
store: vi.fn().mockResolvedValue({
|
|
33
|
+
blobId: createWalrusBlobReference(Buffer.alloc(32, 0x22).toString('base64url'), 'a'.repeat(64)),
|
|
34
|
+
hash: 'a'.repeat(64),
|
|
35
|
+
checksum: 'a'.repeat(64),
|
|
36
|
+
contentHash: 'a'.repeat(64),
|
|
37
|
+
size: 4,
|
|
38
|
+
storedAt: Date.now(),
|
|
39
|
+
}),
|
|
40
|
+
fetch: vi.fn(),
|
|
41
|
+
exists: vi.fn(),
|
|
42
|
+
delete: vi.fn(),
|
|
43
|
+
getMetadata: vi.fn(),
|
|
44
|
+
} as unknown as WalrusBlobStore;
|
|
45
|
+
const store = new HybridBlobStore(walrus, local, { preferWalrus: true, cacheLocally: false });
|
|
46
|
+
|
|
47
|
+
const result = await store.store(encoder.encode('data'));
|
|
48
|
+
|
|
49
|
+
expect(walrus.store).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(result.blobId).toContain('walrus:');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('falls back to the filesystem when Walrus is unavailable', async () => {
|
|
54
|
+
const local = new FilesystemBlobStore(await createBaseDir());
|
|
55
|
+
const walrus = {
|
|
56
|
+
store: vi.fn().mockRejectedValue(new Error('walrus down')),
|
|
57
|
+
fetch: vi.fn(),
|
|
58
|
+
exists: vi.fn(),
|
|
59
|
+
delete: vi.fn(),
|
|
60
|
+
getMetadata: vi.fn(),
|
|
61
|
+
} as unknown as WalrusBlobStore;
|
|
62
|
+
const store = new HybridBlobStore(walrus, local, { preferWalrus: true, cacheLocally: true });
|
|
63
|
+
const data = encoder.encode('fallback');
|
|
64
|
+
|
|
65
|
+
const result = await store.store(data);
|
|
66
|
+
|
|
67
|
+
expect(result.blobId).toMatch(/^[a-f0-9]{64}$/);
|
|
68
|
+
expect(await local.exists(result.blobId)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('caches Walrus blobs locally and serves fetches from the cache', async () => {
|
|
72
|
+
const local = new FilesystemBlobStore(await createBaseDir());
|
|
73
|
+
const data = encoder.encode('cached');
|
|
74
|
+
const contentHash = sha256(data);
|
|
75
|
+
const walrus = {
|
|
76
|
+
store: vi.fn().mockResolvedValue({
|
|
77
|
+
blobId: createWalrusBlobReference(Buffer.alloc(32, 0x33).toString('base64url'), contentHash),
|
|
78
|
+
hash: contentHash,
|
|
79
|
+
checksum: contentHash,
|
|
80
|
+
contentHash,
|
|
81
|
+
size: data.byteLength,
|
|
82
|
+
storedAt: Date.now(),
|
|
83
|
+
}),
|
|
84
|
+
fetch: vi.fn().mockResolvedValue(data),
|
|
85
|
+
exists: vi.fn(),
|
|
86
|
+
delete: vi.fn(),
|
|
87
|
+
getMetadata: vi.fn(),
|
|
88
|
+
} as unknown as WalrusBlobStore;
|
|
89
|
+
const store = new HybridBlobStore(walrus, local, { preferWalrus: true, cacheLocally: true });
|
|
90
|
+
|
|
91
|
+
const stored = await store.store(data);
|
|
92
|
+
const fetched = await store.fetch(stored.blobId);
|
|
93
|
+
|
|
94
|
+
expect(walrus.store).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(walrus.fetch).not.toHaveBeenCalled();
|
|
96
|
+
expect(Buffer.from(fetched ?? [])).toEqual(Buffer.from(data));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('checks the local cache before querying Walrus', async () => {
|
|
100
|
+
const local = new FilesystemBlobStore(await createBaseDir());
|
|
101
|
+
const data = encoder.encode('remote then cached');
|
|
102
|
+
const contentHash = sha256(data);
|
|
103
|
+
const blobId = createWalrusBlobReference(Buffer.alloc(32, 0x44).toString('base64url'), contentHash);
|
|
104
|
+
const walrus = {
|
|
105
|
+
store: vi.fn(),
|
|
106
|
+
fetch: vi.fn().mockResolvedValue(data),
|
|
107
|
+
exists: vi.fn().mockResolvedValue(true),
|
|
108
|
+
delete: vi.fn(),
|
|
109
|
+
getMetadata: vi.fn(),
|
|
110
|
+
} as unknown as WalrusBlobStore;
|
|
111
|
+
const store = new HybridBlobStore(walrus, local, { preferWalrus: true, cacheLocally: true });
|
|
112
|
+
|
|
113
|
+
const firstFetch = await store.fetch(blobId);
|
|
114
|
+
const secondFetch = await store.fetch(blobId);
|
|
115
|
+
|
|
116
|
+
expect(Buffer.from(firstFetch ?? [])).toEqual(Buffer.from(data));
|
|
117
|
+
expect(Buffer.from(secondFetch ?? [])).toEqual(Buffer.from(data));
|
|
118
|
+
expect(walrus.fetch).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(await local.exists(contentHash)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
|
|
7
|
+
const createdPaths: string[] = [];
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
|
|
10
|
+
interface KeytarMock {
|
|
11
|
+
getPassword: ReturnType<typeof vi.fn>;
|
|
12
|
+
setPassword: ReturnType<typeof vi.fn>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
vi.doUnmock('keytar');
|
|
18
|
+
await Promise.all(
|
|
19
|
+
createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function createDataDir(): Promise<string> {
|
|
24
|
+
const dataDir = resolve(process.cwd(), '.test-data', randomUUID());
|
|
25
|
+
createdPaths.push(dataDir);
|
|
26
|
+
await mkdir(dataDir, { recursive: true });
|
|
27
|
+
return dataDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
await access(path);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createKeytarMock(): { keytar: KeytarMock; secrets: Map<string, string> } {
|
|
40
|
+
const secrets = new Map<string, string>();
|
|
41
|
+
const keytar = {
|
|
42
|
+
getPassword: vi.fn(async (service: string, account: string) => secrets.get(`${service}:${account}`) ?? null),
|
|
43
|
+
setPassword: vi.fn(async (service: string, account: string, password: string) => {
|
|
44
|
+
secrets.set(`${service}:${account}`, password);
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return { keytar, secrets };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createUnavailableKeytarMock(): KeytarMock {
|
|
52
|
+
return {
|
|
53
|
+
getPassword: vi.fn(async () => {
|
|
54
|
+
throw new Error('keytar unavailable');
|
|
55
|
+
}),
|
|
56
|
+
setPassword: vi.fn(async () => {
|
|
57
|
+
throw new Error('keytar unavailable');
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function loadKeypairModule(options?: { keytar?: KeytarMock }) {
|
|
63
|
+
vi.resetModules();
|
|
64
|
+
vi.doUnmock('keytar');
|
|
65
|
+
|
|
66
|
+
if (options?.keytar) {
|
|
67
|
+
vi.doMock('keytar', () => ({
|
|
68
|
+
default: options.keytar,
|
|
69
|
+
...options.keytar,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return await import('../src/identity/keypair.js');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('identity', () => {
|
|
77
|
+
it('generates a keypair with a 32-byte public key', async () => {
|
|
78
|
+
const { generateKeypair } = await loadKeypairModule();
|
|
79
|
+
const keypair = generateKeypair();
|
|
80
|
+
|
|
81
|
+
expect(keypair.publicKey).toHaveLength(32);
|
|
82
|
+
expect(keypair.secretKey).toHaveLength(32);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('stores and loads identity keys from the OS keychain when available', async () => {
|
|
86
|
+
const dataDir = await createDataDir();
|
|
87
|
+
const keyPath = join(dataDir, 'identity.key');
|
|
88
|
+
const { keytar, secrets } = createKeytarMock();
|
|
89
|
+
const { loadOrCreateKeypair } = await loadKeypairModule({ keytar });
|
|
90
|
+
|
|
91
|
+
const first = await loadOrCreateKeypair(dataDir);
|
|
92
|
+
const second = await loadOrCreateKeypair(dataDir);
|
|
93
|
+
|
|
94
|
+
expect(Buffer.from(second.secretKey)).toEqual(Buffer.from(first.secretKey));
|
|
95
|
+
expect(Buffer.from(second.publicKey)).toEqual(Buffer.from(first.publicKey));
|
|
96
|
+
expect(await fileExists(keyPath)).toBe(false);
|
|
97
|
+
expect(secrets.get('hivemind-collective:identity-key')).toBe(Buffer.from(first.secretKey).toString('hex'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('migrates legacy file-based keys into the OS keychain', async () => {
|
|
101
|
+
const dataDir = await createDataDir();
|
|
102
|
+
const keyPath = join(dataDir, 'identity.key');
|
|
103
|
+
const { generateKeypair } = await loadKeypairModule();
|
|
104
|
+
const legacyKeypair = generateKeypair();
|
|
105
|
+
await writeFile(keyPath, Buffer.from(legacyKeypair.secretKey).toString('hex'));
|
|
106
|
+
|
|
107
|
+
const { keytar, secrets } = createKeytarMock();
|
|
108
|
+
const { loadOrCreateKeypair } = await loadKeypairModule({ keytar });
|
|
109
|
+
const loaded = await loadOrCreateKeypair(dataDir);
|
|
110
|
+
|
|
111
|
+
expect(Buffer.from(loaded.secretKey)).toEqual(Buffer.from(legacyKeypair.secretKey));
|
|
112
|
+
expect(await fileExists(keyPath)).toBe(false);
|
|
113
|
+
expect(secrets.get('hivemind-collective:identity-key')).toBe(Buffer.from(legacyKeypair.secretKey).toString('hex'));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('falls back to file storage when keychain support is unavailable', async () => {
|
|
117
|
+
const dataDir = await createDataDir();
|
|
118
|
+
const keyPath = join(dataDir, 'identity.key');
|
|
119
|
+
const { loadOrCreateKeypair } = await loadKeypairModule({ keytar: createUnavailableKeytarMock() });
|
|
120
|
+
|
|
121
|
+
const first = await loadOrCreateKeypair(dataDir);
|
|
122
|
+
const second = await loadOrCreateKeypair(dataDir);
|
|
123
|
+
|
|
124
|
+
expect(Buffer.from(second.secretKey)).toEqual(Buffer.from(first.secretKey));
|
|
125
|
+
expect(await fileExists(keyPath)).toBe(true);
|
|
126
|
+
expect((await readFile(keyPath, 'utf8')).trim()).toBe(Buffer.from(first.secretKey).toString('hex'));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('creates and parses DIDs', async () => {
|
|
130
|
+
const [{ createDID, parseDID }, { generateKeypair }] = await Promise.all([
|
|
131
|
+
import('../src/identity/did.js'),
|
|
132
|
+
loadKeypairModule(),
|
|
133
|
+
]);
|
|
134
|
+
const keypair = generateKeypair();
|
|
135
|
+
const did = createDID(keypair.publicKey);
|
|
136
|
+
const parsed = parseDID(did);
|
|
137
|
+
|
|
138
|
+
expect(did).toMatch(/^did:mesh:/);
|
|
139
|
+
expect(Buffer.from(parsed.publicKey)).toEqual(Buffer.from(keypair.publicKey));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('signs and verifies messages', async () => {
|
|
143
|
+
const [{ sign, verify }, { generateKeypair }] = await Promise.all([
|
|
144
|
+
import('../src/identity/signing.js'),
|
|
145
|
+
loadKeypairModule(),
|
|
146
|
+
]);
|
|
147
|
+
const keypair = generateKeypair();
|
|
148
|
+
const message = encoder.encode('agentic mesh');
|
|
149
|
+
const signature = sign(message, keypair.secretKey);
|
|
150
|
+
|
|
151
|
+
expect(verify(message, signature, keypair.publicKey)).toBe(true);
|
|
152
|
+
expect(verify(encoder.encode('tampered'), signature, keypair.publicKey)).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('rejects invalid DID formats', async () => {
|
|
156
|
+
const { isValidDID, parseDID } = await import('../src/identity/did.js');
|
|
157
|
+
|
|
158
|
+
expect(isValidDID('did:other:abc')).toBe(false);
|
|
159
|
+
expect(() => parseDID('did:mesh:')).toThrow();
|
|
160
|
+
});
|
|
161
|
+
});
|