@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,183 @@
1
+ import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { PaymentRail, RegistryClient, type MeshSuiClient } from '../src/index.js';
5
+
6
+ const networkConfig = {
7
+ rpcUrl: 'http://127.0.0.1:9000',
8
+ faucetUrl: 'http://127.0.0.1:9123',
9
+ packageId: '0x1',
10
+ registryId: '0x2',
11
+ };
12
+
13
+ function getMoveTargets(tx: { getData: () => { commands: Array<Record<string, unknown>> } }): string[] {
14
+ return tx
15
+ .getData()
16
+ .commands.map((command) => {
17
+ if ('MoveCall' in command && typeof command.MoveCall === 'object' && command.MoveCall) {
18
+ const moveCall = command.MoveCall as {
19
+ package: string;
20
+ module: string;
21
+ function: string;
22
+ };
23
+ return `${moveCall.package}::${moveCall.module}::${moveCall.function}`;
24
+ }
25
+
26
+ return '';
27
+ })
28
+ .filter(Boolean);
29
+ }
30
+
31
+ describe('RegistryClient', () => {
32
+ it('builds a register agent transaction', async () => {
33
+ const executeTransaction = vi.fn().mockResolvedValue({
34
+ digest: '0xtx',
35
+ objectChanges: [
36
+ {
37
+ type: 'created',
38
+ objectType: '0x1::registry::AgentCard',
39
+ objectId: '0x3',
40
+ },
41
+ ],
42
+ });
43
+ const client = new RegistryClient(
44
+ {
45
+ executeTransaction,
46
+ queryEvents: vi.fn(),
47
+ getObject: vi.fn(),
48
+ } as unknown as MeshSuiClient,
49
+ networkConfig,
50
+ );
51
+
52
+ const result = await client.registerAgent({
53
+ name: 'Agent One',
54
+ did: 'did:mesh:agent-1',
55
+ description: 'Helpful',
56
+ capabilities: [
57
+ {
58
+ name: 'summarize',
59
+ description: 'Summarize text',
60
+ version: '1.0.0',
61
+ pricing: {
62
+ rail: PaymentRail.SUI_ESCROW,
63
+ amount: 100n,
64
+ currency: 'MIST',
65
+ },
66
+ },
67
+ ],
68
+ endpoint: 'https://example.com',
69
+ keypair: {} as unknown as Ed25519Keypair,
70
+ });
71
+
72
+ const tx = executeTransaction.mock.calls[0]?.[0];
73
+ expect(getMoveTargets(tx).some((target) => target.endsWith('::registry::register_agent'))).toBe(true);
74
+ expect(result).toEqual({ txDigest: '0xtx', agentCardId: '0x3' });
75
+ });
76
+
77
+ it('sets an encryption key after registration when provided', async () => {
78
+ const executeTransaction = vi
79
+ .fn()
80
+ .mockResolvedValueOnce({
81
+ digest: '0xtx',
82
+ objectChanges: [
83
+ {
84
+ type: 'created',
85
+ objectType: '0x1::registry::AgentCard',
86
+ objectId: '0x3',
87
+ },
88
+ ],
89
+ })
90
+ .mockResolvedValueOnce({ digest: '0xtx2', objectChanges: [] });
91
+ const client = new RegistryClient(
92
+ {
93
+ executeTransaction,
94
+ queryEvents: vi.fn(),
95
+ getObject: vi.fn(),
96
+ } as unknown as MeshSuiClient,
97
+ networkConfig,
98
+ );
99
+ const encryptionPublicKey = Uint8Array.from({ length: 32 }, (_value, index) => index);
100
+
101
+ const result = await client.registerAgent({
102
+ name: 'Agent One',
103
+ did: 'did:mesh:agent-1',
104
+ description: 'Helpful',
105
+ capabilities: [],
106
+ endpoint: 'https://example.com',
107
+ encryptionPublicKey,
108
+ keypair: {} as unknown as Ed25519Keypair,
109
+ });
110
+
111
+ expect(executeTransaction).toHaveBeenCalledTimes(2);
112
+ expect(getMoveTargets(executeTransaction.mock.calls[1]?.[0]).some((target) => target.endsWith('::registry::set_encryption_key'))).toBe(true);
113
+ expect(result).toEqual({ txDigest: '0xtx', agentCardId: '0x3' });
114
+ });
115
+
116
+ it('parses encryption public keys from agent cards', async () => {
117
+ const client = new RegistryClient(
118
+ {
119
+ executeTransaction: vi.fn(),
120
+ queryEvents: vi.fn(),
121
+ getObject: vi.fn().mockResolvedValue({
122
+ id: '0xcard',
123
+ owner: '0xowner',
124
+ did: 'did:mesh:agent-1',
125
+ name: 'Agent One',
126
+ description: 'Helpful',
127
+ capabilities: [],
128
+ endpoint: 'https://example.com',
129
+ encryption_public_key: Array.from({ length: 32 }, (_value, index) => index),
130
+ active: true,
131
+ version: 1,
132
+ registered_at: 1,
133
+ updated_at: 2,
134
+ }),
135
+ } as unknown as MeshSuiClient,
136
+ networkConfig,
137
+ );
138
+
139
+ const card = await client.getAgentCard('0xcard');
140
+
141
+ expect(card?.encryptionPublicKey).toBe('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');
142
+ });
143
+
144
+ it('queries AgentRegistered events for capability discovery', async () => {
145
+ const queryEvents = vi.fn().mockResolvedValue({ events: [], nextCursor: null, hasMore: false });
146
+ const client = new RegistryClient(
147
+ {
148
+ executeTransaction: vi.fn(),
149
+ queryEvents,
150
+ getObject: vi.fn(),
151
+ } as unknown as MeshSuiClient,
152
+ networkConfig,
153
+ );
154
+
155
+ await client.discoverByCapability('summarize', 5);
156
+
157
+ expect(queryEvents).toHaveBeenCalledWith('0x1::registry::AgentRegistered', null, 20);
158
+ });
159
+
160
+ it('rejects invalid DIDs before submitting a transaction', async () => {
161
+ const executeTransaction = vi.fn();
162
+ const client = new RegistryClient(
163
+ {
164
+ executeTransaction,
165
+ queryEvents: vi.fn(),
166
+ getObject: vi.fn(),
167
+ } as unknown as MeshSuiClient,
168
+ networkConfig,
169
+ );
170
+
171
+ await expect(
172
+ client.registerAgent({
173
+ name: 'Agent One',
174
+ did: 'invalid-did',
175
+ description: 'Helpful',
176
+ capabilities: [],
177
+ endpoint: 'https://example.com',
178
+ keypair: {} as unknown as Ed25519Keypair,
179
+ }),
180
+ ).rejects.toThrow('did must be a did:mesh identifier.');
181
+ expect(executeTransaction).not.toHaveBeenCalled();
182
+ });
183
+ });
@@ -0,0 +1,119 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { PaymentRail } from '@hivemind-os/collective-types';
4
+
5
+ import type { AuthProvider, X402Client } from '../src/index.js';
6
+ import { RelayConsumerClient } from '../src/relay/consumer-client.js';
7
+
8
+ const originalFetch = globalThis.fetch;
9
+ const identity = {
10
+ getDID: () => 'did:mesh:consumer',
11
+ toSuiSigner: () => ({
12
+ sign: vi.fn(async () => new Uint8Array([1, 2, 3])),
13
+ }),
14
+ } as unknown as AuthProvider;
15
+
16
+ afterEach(() => {
17
+ globalThis.fetch = originalFetch;
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ describe('RelayConsumerClient', () => {
22
+ it('returns direct sync responses without payment negotiation', async () => {
23
+ const fetchMock = vi.fn(async () =>
24
+ new Response(JSON.stringify({ ok: true }), {
25
+ status: 200,
26
+ headers: {
27
+ 'content-type': 'application/json',
28
+ 'x-mesh-response-id': 'task-1',
29
+ 'x-mesh-provider': 'did:mesh:provider',
30
+ },
31
+ }),
32
+ );
33
+ globalThis.fetch = fetchMock as typeof fetch;
34
+
35
+ const client = new RelayConsumerClient(null, identity, { relayUrl: 'https://relay.example' });
36
+ const result = await client.executeSync({
37
+ providerDid: 'did:mesh:provider',
38
+ capability: 'echo',
39
+ input: { message: 'hello' },
40
+ paymentRail: PaymentRail.SUI_TRANSFER,
41
+ });
42
+
43
+ expect(result.result).toEqual({ ok: true });
44
+ expect(result.taskId).toBe('task-1');
45
+ expect(result.providerDid).toBe('did:mesh:provider');
46
+ expect(fetchMock).toHaveBeenCalledTimes(1);
47
+ expect(fetchMock.mock.calls[0]?.[1]?.headers).toMatchObject({
48
+ 'x-mesh-target-provider': 'did:mesh:provider',
49
+ 'x-mesh-payment-rail': PaymentRail.SUI_TRANSFER,
50
+ 'x-mesh-sequence': '1',
51
+ });
52
+ });
53
+
54
+ it('retries x402 challenges with a payment header', async () => {
55
+ const x402Client = {
56
+ parse402Response: vi.fn(() => ({ challenge: true })),
57
+ createPaymentHeader: vi.fn(() => 'signed-x402'),
58
+ } as unknown as X402Client;
59
+
60
+ const fetchMock = vi
61
+ .fn()
62
+ .mockResolvedValueOnce(
63
+ new Response(
64
+ JSON.stringify({
65
+ payment: {
66
+ rail: PaymentRail.X402_BASE,
67
+ paymentAddress: '0x0000000000000000000000000000000000000abc',
68
+ amount: '25',
69
+ currency: 'USDC',
70
+ network: 'base-sepolia',
71
+ relayFee: '5',
72
+ expiresAt: Date.now() + 60_000,
73
+ nonce: 'nonce-1',
74
+ extra: { 'payment-required': 'encoded-402' },
75
+ },
76
+ }),
77
+ {
78
+ status: 402,
79
+ headers: { 'content-type': 'application/json' },
80
+ },
81
+ ),
82
+ )
83
+ .mockResolvedValueOnce(
84
+ new Response(JSON.stringify({ ok: true }), {
85
+ status: 200,
86
+ headers: {
87
+ 'content-type': 'application/json',
88
+ 'payment-response': 'receipt-1',
89
+ 'x-mesh-response-id': 'task-2',
90
+ },
91
+ }),
92
+ );
93
+ globalThis.fetch = fetchMock as typeof fetch;
94
+
95
+ const client = new RelayConsumerClient(x402Client, identity, { relayUrl: 'https://relay.example' });
96
+ const result = await client.executeSync({
97
+ providerDid: 'did:mesh:provider',
98
+ capability: 'echo',
99
+ input: { message: 'paid' },
100
+ paymentRail: PaymentRail.X402_BASE,
101
+ });
102
+
103
+ expect(x402Client.parse402Response).toHaveBeenCalledWith(
104
+ { 'payment-required': 'encoded-402' },
105
+ expect.objectContaining({
106
+ payment: expect.objectContaining({ nonce: 'nonce-1', amount: '25' }),
107
+ }),
108
+ );
109
+ expect(x402Client.createPaymentHeader).toHaveBeenCalledWith({ challenge: true });
110
+ expect(fetchMock).toHaveBeenCalledTimes(2);
111
+ expect(fetchMock.mock.calls[1]?.[1]?.headers).toMatchObject({
112
+ 'payment-signature': 'signed-x402',
113
+ 'x-mesh-payment-nonce': 'nonce-1',
114
+ 'x-mesh-sequence': '2',
115
+ });
116
+ expect(result.paymentReceipt).toBe('receipt-1');
117
+ expect(result.taskId).toBe('task-2');
118
+ });
119
+ });
@@ -0,0 +1,261 @@
1
+ import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { RelayDiscovery, RelayNodeStatus, RelayRegistryClient, StakingClient, type MeshSuiClient, type RelayNode } 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
+ function createRelay(overrides: Partial<RelayNode> = {}): RelayNode {
26
+ return {
27
+ id: '0x111',
28
+ operator: '0xaaa',
29
+ endpoint: 'wss://relay-1.mesh.example/ws',
30
+ stakePositionId: '0x123',
31
+ capabilities: ['routing', 'streaming'],
32
+ region: 'us-east',
33
+ status: RelayNodeStatus.ACTIVE,
34
+ registeredAt: 1_000,
35
+ lastHeartbeat: 1_500,
36
+ routingFeeBps: 50,
37
+ totalRouted: 10,
38
+ totalFeesEarnedMist: 999n,
39
+ stakeAmountMist: 100_000_000_000n,
40
+ heartbeatAgeMs: 500,
41
+ isHeartbeatFresh: true,
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ afterEach(() => {
47
+ vi.restoreAllMocks();
48
+ vi.useRealTimers();
49
+ });
50
+
51
+ describe('RelayRegistryClient', () => {
52
+ it('builds relay registry transactions', async () => {
53
+ const executeTransaction = vi
54
+ .fn()
55
+ .mockResolvedValueOnce({
56
+ digest: '0xregister',
57
+ objectChanges: [{ type: 'created', objectType: '0x1::relay_registry::RelayNode', objectId: '0x111' }],
58
+ events: [{ type: '0x1::relay_registry::RelayRegistered', parsedJson: { relay_id: '0x111' } }],
59
+ })
60
+ .mockResolvedValueOnce({
61
+ digest: '0xheartbeat',
62
+ objectChanges: [],
63
+ events: [{ type: '0x1::relay_registry::RelayHeartbeat', parsedJson: { last_heartbeat: 1234 } }],
64
+ })
65
+ .mockResolvedValueOnce({ digest: '0xdeactivate', objectChanges: [], events: [] });
66
+ const client = new RelayRegistryClient(
67
+ {
68
+ executeTransaction,
69
+ queryEvents: vi.fn(),
70
+ getObject: vi.fn(),
71
+ } as unknown as MeshSuiClient,
72
+ contractConfig,
73
+ );
74
+
75
+ await expect(
76
+ client.registerRelay({
77
+ endpoint: 'wss://relay.mesh.example/ws',
78
+ stakeId: '0x123',
79
+ capabilities: ['routing'],
80
+ region: 'us-east',
81
+ routingFeeBps: 50,
82
+ signer: {} as Ed25519Keypair,
83
+ }),
84
+ ).resolves.toEqual({ relayId: '0x111', txDigest: '0xregister' });
85
+ await expect(client.heartbeat({ relayId: '0x111', signer: {} as Ed25519Keypair })).resolves.toEqual({
86
+ lastHeartbeat: 1234,
87
+ txDigest: '0xheartbeat',
88
+ });
89
+ await expect(client.deactivateRelay({ relayId: '0x111', signer: {} as Ed25519Keypair })).resolves.toEqual({
90
+ txDigest: '0xdeactivate',
91
+ });
92
+
93
+ expect(getMoveTargets(executeTransaction.mock.calls[0]?.[0]).some((target) => target.endsWith('::relay_registry::register_relay'))).toBe(true);
94
+ expect(getMoveTargets(executeTransaction.mock.calls[1]?.[0]).some((target) => target.endsWith('::relay_registry::heartbeat'))).toBe(true);
95
+ expect(getMoveTargets(executeTransaction.mock.calls[2]?.[0]).some((target) => target.endsWith('::relay_registry::deactivate_relay'))).toBe(true);
96
+ });
97
+
98
+ it('rejects external routing metric reports', async () => {
99
+ const executeTransaction = vi.fn();
100
+ const client = new RelayRegistryClient(
101
+ {
102
+ executeTransaction,
103
+ queryEvents: vi.fn(),
104
+ getObject: vi.fn(),
105
+ } as unknown as MeshSuiClient,
106
+ contractConfig,
107
+ );
108
+
109
+ await expect(
110
+ client.recordRouting({ relayId: '0x111', feeAmountMist: 25n, signer: {} as Ed25519Keypair }),
111
+ ).rejects.toThrow('Relay routing metrics are package-internal and cannot be reported by external operators.');
112
+ expect(executeTransaction).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it('parses relay nodes and enriches stake metadata', async () => {
116
+ vi.useFakeTimers();
117
+ vi.setSystemTime(2_000);
118
+ vi.spyOn(StakingClient.prototype, 'getStakePosition').mockResolvedValue({
119
+ id: '0x123',
120
+ owner: '0xaaa',
121
+ stakeType: 'relay',
122
+ balanceMist: 200_000_000_000n,
123
+ stakedAt: 1,
124
+ deactivatedAt: 0,
125
+ slashedAmount: 0n,
126
+ isActive: true,
127
+ meetsMinium: true,
128
+ meetsMinimum: true,
129
+ });
130
+ const client = new RelayRegistryClient(
131
+ {
132
+ executeTransaction: vi.fn(),
133
+ queryEvents: vi.fn(),
134
+ getObject: vi.fn().mockResolvedValue({
135
+ objectId: '0x111',
136
+ operator: '0xaaa',
137
+ endpoint: 'wss://relay.mesh.example/ws',
138
+ stake_position_id: '0x123',
139
+ capabilities: ['routing', 'streaming'],
140
+ region: 'us-east',
141
+ status: 0,
142
+ registered_at: 1_000,
143
+ last_heartbeat: 1_500,
144
+ routing_fee_bps: 50,
145
+ total_routed: 7,
146
+ total_fees_earned: '999',
147
+ }),
148
+ } as unknown as MeshSuiClient,
149
+ contractConfig,
150
+ );
151
+
152
+ await expect(client.getRelay('0x111')).resolves.toEqual({
153
+ id: '0x111',
154
+ operator: '0xaaa',
155
+ endpoint: 'wss://relay.mesh.example/ws',
156
+ stakePositionId: '0x123',
157
+ capabilities: ['routing', 'streaming'],
158
+ region: 'us-east',
159
+ status: RelayNodeStatus.ACTIVE,
160
+ registeredAt: 1_000,
161
+ lastHeartbeat: 1_500,
162
+ routingFeeBps: 50,
163
+ totalRouted: 7,
164
+ totalFeesEarnedMist: 999n,
165
+ stakeAmountMist: 200_000_000_000n,
166
+ heartbeatAgeMs: 500,
167
+ isHeartbeatFresh: true,
168
+ });
169
+ });
170
+
171
+ it('lists relays with active filtering by default', async () => {
172
+ vi.useFakeTimers();
173
+ vi.setSystemTime(10_000);
174
+ vi.spyOn(StakingClient.prototype, 'getStakePosition').mockImplementation(async (stakeId: string) => ({
175
+ id: stakeId,
176
+ owner: '0xaaa',
177
+ stakeType: 'relay',
178
+ balanceMist: stakeId === '0x124' ? 150_000_000_000n : 100_000_000_000n,
179
+ stakedAt: 1,
180
+ deactivatedAt: 0,
181
+ slashedAmount: 0n,
182
+ isActive: true,
183
+ meetsMinium: true,
184
+ meetsMinimum: true,
185
+ }));
186
+ const client = new RelayRegistryClient(
187
+ {
188
+ executeTransaction: vi.fn(),
189
+ queryEvents: vi.fn().mockResolvedValue({
190
+ events: [
191
+ { type: '0x1::relay_registry::RelayRegistered', parsedJson: { relay_id: '0x111' } },
192
+ { type: '0x1::relay_registry::RelayRegistered', parsedJson: { relay_id: '0x112' } },
193
+ ],
194
+ nextCursor: null,
195
+ hasMore: false,
196
+ }),
197
+ getObject: vi.fn().mockImplementation(async (relayId: string) => ({
198
+ objectId: relayId,
199
+ operator: relayId === '0x112' ? '0xaab' : '0xaaa',
200
+ endpoint: `wss://${relayId}.mesh.example/ws`,
201
+ stake_position_id: relayId === '0x112' ? '0x124' : '0x123',
202
+ capabilities: relayId === '0x112' ? ['routing', 'streaming'] : ['routing'],
203
+ region: relayId === '0x112' ? 'us-west' : 'us-east',
204
+ status: relayId === '0x112' ? 1 : 0,
205
+ registered_at: 1_000,
206
+ last_heartbeat: relayId === '0x112' ? 9_000 : 9_500,
207
+ routing_fee_bps: relayId === '0x112' ? 25 : 50,
208
+ total_routed: 0,
209
+ total_fees_earned: '0',
210
+ })),
211
+ } as unknown as MeshSuiClient,
212
+ contractConfig,
213
+ );
214
+
215
+ const activeRelays = await client.listRelays();
216
+ const westRelays = await client.listRelays({ activeOnly: false, region: 'us-west' });
217
+
218
+ expect(activeRelays.map((relay) => relay.id)).toEqual(['0x111']);
219
+ expect(westRelays.map((relay) => relay.id)).toEqual(['0x112']);
220
+ await expect(client.getRelaysByRegion('us-east')).resolves.toHaveLength(1);
221
+ });
222
+ });
223
+
224
+ describe('RelayDiscovery', () => {
225
+ it('selects the best relay and caches relay lists', async () => {
226
+ let now = 10_000;
227
+ const listRelays = vi.fn().mockResolvedValue([
228
+ createRelay({ id: '0xrelay-east', region: 'us-east', routingFeeBps: 50, stakeAmountMist: 150_000_000_000n, heartbeatAgeMs: 100 }),
229
+ createRelay({
230
+ id: '0xrelay-west',
231
+ region: 'us-west',
232
+ routingFeeBps: 10,
233
+ stakeAmountMist: 80_000_000_000n,
234
+ heartbeatAgeMs: 100,
235
+ }),
236
+ createRelay({
237
+ id: '0xrelay-stale',
238
+ region: 'us-east',
239
+ routingFeeBps: 20,
240
+ stakeAmountMist: 500_000_000_000n,
241
+ heartbeatAgeMs: 40_000,
242
+ isHeartbeatFresh: false,
243
+ }),
244
+ ]);
245
+ const discovery = new RelayDiscovery({ listRelays } as Pick<RelayRegistryClient, 'listRelays'>, {
246
+ cacheTtlMs: 1_000,
247
+ heartbeatFreshnessMs: 5_000,
248
+ now: () => now,
249
+ });
250
+
251
+ const first = await discovery.findBestRelay('routing', 'us-east');
252
+ const second = await discovery.findBestRelay('routing', 'us-east');
253
+ now = 12_000;
254
+ const third = await discovery.findBestRelay('routing', 'us-east');
255
+
256
+ expect(first?.id).toBe('0xrelay-east');
257
+ expect(second?.id).toBe('0xrelay-east');
258
+ expect(third?.id).toBe('0xrelay-east');
259
+ expect(listRelays).toHaveBeenCalledTimes(2);
260
+ });
261
+ });
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+
5
+ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
6
+ import { afterEach, describe, expect, it, vi } from 'vitest';
7
+
8
+ import { Ed25519AuthProvider, ReputationEventPublisher } from '../../src/index.js';
9
+ import { serializeReputationEventPayload } from '../../src/reputation/serialization.js';
10
+
11
+ const createdPaths: string[] = [];
12
+
13
+ afterEach(async () => {
14
+ await Promise.all(createdPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })));
15
+ });
16
+
17
+ async function createTempDir(): Promise<string> {
18
+ const dir = resolve(process.cwd(), '.test-data', randomUUID());
19
+ createdPaths.push(dir);
20
+ await mkdir(dir, { recursive: true });
21
+ return dir;
22
+ }
23
+
24
+ describe('ReputationEventPublisher', () => {
25
+ it('rejects self-authored reputation events', async () => {
26
+ await createTempDir();
27
+ const keypair = Ed25519Keypair.generate();
28
+ const identity = new Ed25519AuthProvider(keypair);
29
+ const publisher = new ReputationEventPublisher({ store: vi.fn() } as never, identity);
30
+
31
+ await expect(publisher.createEvent({
32
+ type: 'task_completion',
33
+ subject: identity.getDID(),
34
+ taskId: 'task-1',
35
+ outcome: 'success',
36
+ capability: 'echo',
37
+ })).rejects.toThrow('Reputation event subject must differ from author.');
38
+ });
39
+
40
+ it('creates signed reputation events and publishes them', async () => {
41
+ await createTempDir();
42
+ const keypair = Ed25519Keypair.generate();
43
+ const identity = new Ed25519AuthProvider(keypair);
44
+ const blobStore = {
45
+ store: vi.fn(async () => ({ blobId: 'blob-1', hash: 'hash', checksum: 'hash', contentHash: 'hash', size: 1, storedAt: Date.now() })),
46
+ } as never;
47
+ const publisher = new ReputationEventPublisher(blobStore, identity);
48
+
49
+ const event = await publisher.createEvent({
50
+ type: 'task_completion',
51
+ subject: 'did:mesh:provider',
52
+ taskId: 'task-1',
53
+ outcome: 'success',
54
+ capability: 'echo',
55
+ rating: 5,
56
+ latencyMs: 123,
57
+ paymentAmount: { amount: '42', currency: 'MIST' },
58
+ });
59
+
60
+ expect(event.author).toBe(identity.getDID());
61
+ expect(event.signature.length).toBeGreaterThan(0);
62
+ const { signature, ...unsignedEvent } = event;
63
+ await expect(
64
+ keypair.getPublicKey().verifyPersonalMessage(serializeReputationEventPayload(unsignedEvent), signature),
65
+ ).resolves.toBe(true);
66
+
67
+ await publisher.publishEvent(event);
68
+ expect(blobStore.store).toHaveBeenCalledOnce();
69
+ });
70
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { ReputationEvent } from '@hivemind-os/collective-types';
4
+
5
+ import { buildMerkleTree, verifyMerkleProof } from '../../src/index.js';
6
+
7
+ function createEvent(index: number): ReputationEvent {
8
+ return {
9
+ eventId: `event-${index}`,
10
+ type: 'task_completion',
11
+ subject: 'did:mesh:provider',
12
+ author: 'did:mesh:requester',
13
+ taskId: `task-${index}`,
14
+ outcome: 'success',
15
+ capability: 'echo',
16
+ timestamp: new Date(1_700_000_000_000 + index * 1_000).toISOString(),
17
+ nonce: `nonce-${index}`,
18
+ signature: `signature-${index}`,
19
+ };
20
+ }
21
+
22
+ describe('Merkle tree', () => {
23
+ it('builds proofs for multiple leaves', () => {
24
+ const events = [createEvent(1), createEvent(2), createEvent(3), createEvent(4)];
25
+ const tree = buildMerkleTree(events);
26
+
27
+ expect(verifyMerkleProof(events[2]!, tree.proof(2), tree.root, 2)).toBe(true);
28
+ });
29
+
30
+ it('supports odd numbers of leaves', () => {
31
+ const events = [createEvent(1), createEvent(2), createEvent(3)];
32
+ const tree = buildMerkleTree(events);
33
+
34
+ expect(verifyMerkleProof(events[2]!, tree.proof(2), tree.root, 2)).toBe(true);
35
+ });
36
+
37
+ it('supports a single leaf', () => {
38
+ const events = [createEvent(1)];
39
+ const tree = buildMerkleTree(events);
40
+
41
+ expect(tree.proof(0)).toEqual([]);
42
+ expect(verifyMerkleProof(events[0]!, tree.proof(0), tree.root, 0)).toBe(true);
43
+ });
44
+ });