@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,286 @@
|
|
|
1
|
+
import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PaymentRail, RegistryClient, StakingClient, type MeshSuiClient, type AgentCard } from '../../src/index.js';
|
|
5
|
+
|
|
6
|
+
const contractConfig = { packageId: '0x1' };
|
|
7
|
+
const networkConfig = {
|
|
8
|
+
rpcUrl: 'http://127.0.0.1:9000',
|
|
9
|
+
faucetUrl: 'http://127.0.0.1:9123',
|
|
10
|
+
packageId: '0x1',
|
|
11
|
+
registryId: '0x2',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getMoveTargets(tx: { getData: () => { commands: Array<Record<string, unknown>> } }): string[] {
|
|
15
|
+
return tx
|
|
16
|
+
.getData()
|
|
17
|
+
.commands.map((command) => {
|
|
18
|
+
if ('MoveCall' in command && typeof command.MoveCall === 'object' && command.MoveCall) {
|
|
19
|
+
const moveCall = command.MoveCall as {
|
|
20
|
+
package: string;
|
|
21
|
+
module: string;
|
|
22
|
+
function: string;
|
|
23
|
+
};
|
|
24
|
+
return `${moveCall.package}::${moveCall.module}::${moveCall.function}`;
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
})
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createAgent(overrides: Partial<AgentCard> = {}): AgentCard {
|
|
32
|
+
return {
|
|
33
|
+
id: '0xagent-1',
|
|
34
|
+
owner: '0xowner-1',
|
|
35
|
+
did: 'did:mesh:agent-1' as AgentCard['did'],
|
|
36
|
+
name: 'Summarizer',
|
|
37
|
+
description: 'Summarizes text',
|
|
38
|
+
capabilities: [
|
|
39
|
+
{
|
|
40
|
+
name: 'summarize',
|
|
41
|
+
description: 'Summarize documents',
|
|
42
|
+
version: '1.0.0',
|
|
43
|
+
pricing: {
|
|
44
|
+
rail: PaymentRail.SUI_ESCROW,
|
|
45
|
+
amount: 100n,
|
|
46
|
+
currency: 'MIST',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
endpoint: 'mesh://agent/did:mesh:agent-1',
|
|
51
|
+
active: true,
|
|
52
|
+
version: 1,
|
|
53
|
+
registeredAt: 1_000,
|
|
54
|
+
updatedAt: 1_000,
|
|
55
|
+
totalTasksCompleted: 1,
|
|
56
|
+
totalTasksFailed: 0,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('StakingClient', () => {
|
|
62
|
+
it('builds a deposit stake transaction and returns the new stake id', async () => {
|
|
63
|
+
const executeTransaction = vi.fn().mockResolvedValue({
|
|
64
|
+
digest: '0xtx',
|
|
65
|
+
objectChanges: [],
|
|
66
|
+
events: [
|
|
67
|
+
{
|
|
68
|
+
type: '0x1::staking::StakeDeposited',
|
|
69
|
+
parsedJson: { stake_id: '0xstake-1' },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
const client = new StakingClient(
|
|
74
|
+
{
|
|
75
|
+
executeTransaction,
|
|
76
|
+
queryEvents: vi.fn(),
|
|
77
|
+
getObject: vi.fn(),
|
|
78
|
+
} as unknown as MeshSuiClient,
|
|
79
|
+
contractConfig,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const result = await client.depositStake({
|
|
83
|
+
amountMist: 10_000_000_000n,
|
|
84
|
+
stakeType: 'agent',
|
|
85
|
+
signer: {} as Ed25519Keypair,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const tx = executeTransaction.mock.calls[0]?.[0];
|
|
89
|
+
expect(getMoveTargets(tx).some((target) => target.endsWith('::staking::deposit_stake'))).toBe(true);
|
|
90
|
+
expect(result).toEqual({ stakeId: '0xstake-1', txDigest: '0xtx' });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('parses stake positions from Move objects', async () => {
|
|
94
|
+
const client = new StakingClient(
|
|
95
|
+
{
|
|
96
|
+
executeTransaction: vi.fn(),
|
|
97
|
+
queryEvents: vi.fn(),
|
|
98
|
+
getObject: vi.fn().mockResolvedValue({
|
|
99
|
+
objectId: '0xstake-1',
|
|
100
|
+
owner: '0xowner-1',
|
|
101
|
+
stake_type: 0,
|
|
102
|
+
balance: { value: '10000000000' },
|
|
103
|
+
staked_at: 123,
|
|
104
|
+
deactivated_at: 0,
|
|
105
|
+
slashed_amount: '50',
|
|
106
|
+
}),
|
|
107
|
+
} as unknown as MeshSuiClient,
|
|
108
|
+
contractConfig,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const position = await client.getStakePosition('0xstake-1');
|
|
112
|
+
|
|
113
|
+
expect(position).toEqual({
|
|
114
|
+
id: '0xstake-1',
|
|
115
|
+
owner: '0xowner-1',
|
|
116
|
+
stakeType: 'agent',
|
|
117
|
+
balanceMist: 10_000_000_000n,
|
|
118
|
+
stakedAt: 123,
|
|
119
|
+
deactivatedAt: 0,
|
|
120
|
+
slashedAmount: 50n,
|
|
121
|
+
isActive: true,
|
|
122
|
+
meetsMinium: true,
|
|
123
|
+
meetsMinimum: true,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns the best active stake for an owner', async () => {
|
|
128
|
+
const getObject = vi.fn().mockImplementation(async (id: string) => ({
|
|
129
|
+
objectId: id,
|
|
130
|
+
owner: '0xowner-1',
|
|
131
|
+
stake_type: 0,
|
|
132
|
+
balance: { value: id === '0xstake-active' ? '12000000000' : '5000000000' },
|
|
133
|
+
staked_at: id === '0xstake-active' ? 2 : 1,
|
|
134
|
+
deactivated_at: id === '0xstake-active' ? 0 : 3,
|
|
135
|
+
slashed_amount: '0',
|
|
136
|
+
}));
|
|
137
|
+
const client = new StakingClient(
|
|
138
|
+
{
|
|
139
|
+
executeTransaction: vi.fn(),
|
|
140
|
+
queryEvents: vi.fn().mockResolvedValue({
|
|
141
|
+
events: [
|
|
142
|
+
{ type: '0x1::staking::StakeDeposited', parsedJson: { owner: '0xowner-1', stake_id: '0xstake-old' } },
|
|
143
|
+
{ type: '0x1::staking::StakeDeposited', parsedJson: { owner: '0xowner-1', stake_id: '0xstake-active' } },
|
|
144
|
+
],
|
|
145
|
+
nextCursor: null,
|
|
146
|
+
hasMore: false,
|
|
147
|
+
}),
|
|
148
|
+
getObject,
|
|
149
|
+
} as unknown as MeshSuiClient,
|
|
150
|
+
contractConfig,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const position = await client.getStakeByOwner('0xowner-1');
|
|
154
|
+
|
|
155
|
+
expect(position?.id).toBe('0xstake-active');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('fails when the deactivation event omits cooldown metadata', async () => {
|
|
159
|
+
const client = new StakingClient(
|
|
160
|
+
{
|
|
161
|
+
executeTransaction: vi.fn().mockResolvedValue({
|
|
162
|
+
digest: '0xdeactivate',
|
|
163
|
+
events: [{ type: '0x1::staking::DeactivationStarted', parsedJson: {} }],
|
|
164
|
+
objectChanges: [],
|
|
165
|
+
}),
|
|
166
|
+
queryEvents: vi.fn(),
|
|
167
|
+
getObject: vi.fn(),
|
|
168
|
+
} as unknown as MeshSuiClient,
|
|
169
|
+
contractConfig,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await expect(client.startDeactivation({ stakeId: '0x123', signer: {} as Ed25519Keypair })).rejects.toThrow(
|
|
173
|
+
'DeactivationStarted event did not include a valid cooldown_ends_at.',
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('supports add stake, deactivation, withdrawal, and slashing flows', async () => {
|
|
178
|
+
const executeTransaction = vi
|
|
179
|
+
.fn()
|
|
180
|
+
.mockResolvedValueOnce({ digest: '0xadd', events: [], objectChanges: [] })
|
|
181
|
+
.mockResolvedValueOnce({
|
|
182
|
+
digest: '0xdeactivate',
|
|
183
|
+
events: [{ type: '0x1::staking::DeactivationStarted', parsedJson: { cooldown_ends_at: 1234 } }],
|
|
184
|
+
objectChanges: [],
|
|
185
|
+
})
|
|
186
|
+
.mockResolvedValueOnce({
|
|
187
|
+
digest: '0xwithdraw',
|
|
188
|
+
events: [{ type: '0x1::staking::StakeWithdrawn', parsedJson: { amount: '5000' } }],
|
|
189
|
+
objectChanges: [],
|
|
190
|
+
})
|
|
191
|
+
.mockResolvedValueOnce({
|
|
192
|
+
digest: '0xslash',
|
|
193
|
+
events: [{ type: '0x1::staking::StakeSlashed', parsedJson: { amount: '25' } }],
|
|
194
|
+
objectChanges: [],
|
|
195
|
+
});
|
|
196
|
+
const client = new StakingClient(
|
|
197
|
+
{
|
|
198
|
+
executeTransaction,
|
|
199
|
+
queryEvents: vi.fn(),
|
|
200
|
+
getObject: vi.fn(),
|
|
201
|
+
} as unknown as MeshSuiClient,
|
|
202
|
+
contractConfig,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
await expect(client.addStake({ stakeId: '0x123', amountMist: 100n, signer: {} as Ed25519Keypair })).resolves.toEqual({
|
|
206
|
+
txDigest: '0xadd',
|
|
207
|
+
});
|
|
208
|
+
await expect(client.startDeactivation({ stakeId: '0x123', signer: {} as Ed25519Keypair })).resolves.toEqual({
|
|
209
|
+
cooldownEndsAt: 1234,
|
|
210
|
+
txDigest: '0xdeactivate',
|
|
211
|
+
});
|
|
212
|
+
await expect(client.withdrawStake({ stakeId: '0x123', signer: {} as Ed25519Keypair })).resolves.toEqual({
|
|
213
|
+
amountReturned: 5000n,
|
|
214
|
+
txDigest: '0xwithdraw',
|
|
215
|
+
});
|
|
216
|
+
await expect(client.slashExpiredEscrow({ stakeId: '0x123', taskId: '0x456', signer: {} as Ed25519Keypair })).resolves.toEqual({
|
|
217
|
+
slashedAmount: 25n,
|
|
218
|
+
txDigest: '0xslash',
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('RegistryClient staking discovery', () => {
|
|
224
|
+
it('ranks staked agents ahead of unstaked peers', async () => {
|
|
225
|
+
const staked = createAgent({ owner: '0xowner-staked', totalTasksCompleted: 3 });
|
|
226
|
+
const unstaked = createAgent({
|
|
227
|
+
id: '0xagent-2',
|
|
228
|
+
did: 'did:mesh:agent-2' as AgentCard['did'],
|
|
229
|
+
owner: '0xowner-unstaked',
|
|
230
|
+
totalTasksCompleted: 3,
|
|
231
|
+
});
|
|
232
|
+
const getStakeByOwner = vi.spyOn(StakingClient.prototype, 'getStakeByOwner')
|
|
233
|
+
.mockResolvedValueOnce(null)
|
|
234
|
+
.mockResolvedValueOnce({
|
|
235
|
+
id: '0xstake',
|
|
236
|
+
owner: staked.owner,
|
|
237
|
+
stakeType: 'agent',
|
|
238
|
+
balanceMist: 10_000_000_000n,
|
|
239
|
+
stakedAt: 1,
|
|
240
|
+
deactivatedAt: 0,
|
|
241
|
+
slashedAmount: 0n,
|
|
242
|
+
isActive: true,
|
|
243
|
+
meetsMinium: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const client = new RegistryClient(
|
|
247
|
+
{
|
|
248
|
+
executeTransaction: vi.fn(),
|
|
249
|
+
queryEvents: vi.fn().mockResolvedValue({
|
|
250
|
+
events: [
|
|
251
|
+
{
|
|
252
|
+
id: { txDigest: '0x1', eventSeq: '0' },
|
|
253
|
+
type: '0x1::registry::AgentRegistered',
|
|
254
|
+
timestampMs: '1',
|
|
255
|
+
parsedJson: { card_id: unstaked.id, capabilities: unstaked.capabilities },
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: { txDigest: '0x2', eventSeq: '1' },
|
|
259
|
+
type: '0x1::registry::AgentRegistered',
|
|
260
|
+
timestampMs: '2',
|
|
261
|
+
parsedJson: { card_id: staked.id, capabilities: staked.capabilities },
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
nextCursor: null,
|
|
265
|
+
hasMore: false,
|
|
266
|
+
}),
|
|
267
|
+
getObject: vi.fn().mockImplementation(async (id: string) => {
|
|
268
|
+
if (id === staked.id) {
|
|
269
|
+
return staked;
|
|
270
|
+
}
|
|
271
|
+
if (id === unstaked.id) {
|
|
272
|
+
return unstaked;
|
|
273
|
+
}
|
|
274
|
+
throw new Error(`unexpected object ${id}`);
|
|
275
|
+
}),
|
|
276
|
+
} as unknown as MeshSuiClient,
|
|
277
|
+
networkConfig,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const results = await client.discoverByCapability('summarize', 10, { sortByReputation: true });
|
|
281
|
+
|
|
282
|
+
expect(getStakeByOwner).toHaveBeenCalledTimes(2);
|
|
283
|
+
expect(results.map((agent) => agent.did)).toEqual([staked.did, unstaked.did]);
|
|
284
|
+
expect(results[0]?.hasStake).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SuiTransactionBlockResponse } from '@mysten/sui/client';
|
|
2
|
+
import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
3
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { MeshSuiClient, SuiTransactionExecutionError } from '../src/index.js';
|
|
7
|
+
|
|
8
|
+
const networkConfig = {
|
|
9
|
+
rpcUrl: 'http://127.0.0.1:9000',
|
|
10
|
+
faucetUrl: 'http://127.0.0.1:9123',
|
|
11
|
+
packageId: '0x1',
|
|
12
|
+
registryId: '0x2',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function createKeypairStub(): Ed25519Keypair {
|
|
16
|
+
return {
|
|
17
|
+
getPublicKey: () => ({
|
|
18
|
+
toSuiAddress: () => '0x123',
|
|
19
|
+
}),
|
|
20
|
+
} as unknown as Ed25519Keypair;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('MeshSuiClient', () => {
|
|
24
|
+
it('throws informative errors for failed transaction effects', async () => {
|
|
25
|
+
const client = new MeshSuiClient(networkConfig);
|
|
26
|
+
const signAndExecuteTransaction = vi.fn().mockResolvedValue({ digest: '0xabc' });
|
|
27
|
+
const waitForTransaction = vi.fn().mockResolvedValue({
|
|
28
|
+
digest: '0xabc',
|
|
29
|
+
effects: {
|
|
30
|
+
status: {
|
|
31
|
+
status: 'failure',
|
|
32
|
+
error: 'Insufficient gas',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
} satisfies Partial<SuiTransactionBlockResponse>);
|
|
36
|
+
|
|
37
|
+
Reflect.set(client, 'suiClient', {
|
|
38
|
+
signAndExecuteTransaction,
|
|
39
|
+
waitForTransaction,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const execution = client.executeTransaction(new Transaction(), createKeypairStub());
|
|
43
|
+
|
|
44
|
+
await expect(execution).rejects.toThrow(SuiTransactionExecutionError);
|
|
45
|
+
await expect(execution).rejects.toThrow(/insufficient gas/i);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('retries retryable execution failures before succeeding', async () => {
|
|
49
|
+
const client = new MeshSuiClient(networkConfig);
|
|
50
|
+
const signAndExecuteTransaction = vi
|
|
51
|
+
.fn()
|
|
52
|
+
.mockResolvedValueOnce({ digest: '0xretry-1' })
|
|
53
|
+
.mockResolvedValueOnce({ digest: '0xretry-2' });
|
|
54
|
+
const waitForTransaction = vi
|
|
55
|
+
.fn()
|
|
56
|
+
.mockResolvedValueOnce({
|
|
57
|
+
digest: '0xretry-1',
|
|
58
|
+
effects: {
|
|
59
|
+
status: {
|
|
60
|
+
status: 'failure',
|
|
61
|
+
error: 'Object lock conflict',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
} satisfies Partial<SuiTransactionBlockResponse>)
|
|
65
|
+
.mockResolvedValueOnce({
|
|
66
|
+
digest: '0xretry-2',
|
|
67
|
+
effects: {
|
|
68
|
+
status: {
|
|
69
|
+
status: 'success',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} satisfies Partial<SuiTransactionBlockResponse>);
|
|
73
|
+
|
|
74
|
+
Reflect.set(client, 'suiClient', {
|
|
75
|
+
signAndExecuteTransaction,
|
|
76
|
+
waitForTransaction,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const response = await client.executeTransaction(new Transaction(), createKeypairStub());
|
|
80
|
+
|
|
81
|
+
expect(response.digest).toBe('0xretry-2');
|
|
82
|
+
expect(signAndExecuteTransaction).toHaveBeenCalledTimes(2);
|
|
83
|
+
expect(waitForTransaction).toHaveBeenCalledTimes(2);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { PaymentScheme, TaskClient, TaskStatus, 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 getCommands(tx: { getData: () => { commands: Array<Record<string, unknown>> } }): Array<Record<string, unknown>> {
|
|
14
|
+
return tx.getData().commands;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('TaskClient', () => {
|
|
18
|
+
it('builds a post task transaction with split coins', async () => {
|
|
19
|
+
const executeTransaction = vi.fn().mockResolvedValue({
|
|
20
|
+
digest: '0xtx',
|
|
21
|
+
objectChanges: [
|
|
22
|
+
{
|
|
23
|
+
type: 'created',
|
|
24
|
+
objectType: '0x1::task::Task',
|
|
25
|
+
objectId: '0x3',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
const client = new TaskClient(
|
|
30
|
+
{
|
|
31
|
+
executeTransaction,
|
|
32
|
+
getObject: vi.fn(),
|
|
33
|
+
} as unknown as MeshSuiClient,
|
|
34
|
+
networkConfig,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const result = await client.postTask({
|
|
38
|
+
capability: 'summarize',
|
|
39
|
+
category: 'analysis',
|
|
40
|
+
inputBlobId: 'blob-1',
|
|
41
|
+
agreementHash: 'hash-1',
|
|
42
|
+
priceMist: 500n,
|
|
43
|
+
disputeWindowMs: 60_000,
|
|
44
|
+
expiryHours: 24,
|
|
45
|
+
keypair: {} as unknown as Ed25519Keypair,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const commands = getCommands(executeTransaction.mock.calls[0]?.[0]);
|
|
49
|
+
const kinds = commands.map((command) => String(command.$kind));
|
|
50
|
+
|
|
51
|
+
expect(kinds).toContain('SplitCoins');
|
|
52
|
+
expect(kinds).toContain('MoveCall');
|
|
53
|
+
expect(result).toEqual({ txDigest: '0xtx', taskId: '0x3' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('builds an accept task transaction', async () => {
|
|
57
|
+
const executeTransaction = vi.fn().mockResolvedValue({ digest: '0xtx' });
|
|
58
|
+
const client = new TaskClient(
|
|
59
|
+
{
|
|
60
|
+
executeTransaction,
|
|
61
|
+
getObject: vi.fn(),
|
|
62
|
+
} as unknown as MeshSuiClient,
|
|
63
|
+
networkConfig,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await client.acceptTask({
|
|
67
|
+
taskId: '0x3',
|
|
68
|
+
keypair: {} as unknown as Ed25519Keypair,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const commands = getCommands(executeTransaction.mock.calls[0]?.[0]);
|
|
72
|
+
expect(commands[0]?.$kind).toBe('MoveCall');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('builds metered task lifecycle transactions', async () => {
|
|
76
|
+
const executeTransaction = vi.fn()
|
|
77
|
+
.mockResolvedValueOnce({
|
|
78
|
+
digest: '0xpost',
|
|
79
|
+
objectChanges: [
|
|
80
|
+
{
|
|
81
|
+
type: 'created',
|
|
82
|
+
objectType: '0x1::task::Task',
|
|
83
|
+
objectId: '0x9',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
})
|
|
87
|
+
.mockResolvedValueOnce({ digest: '0xcomplete' })
|
|
88
|
+
.mockResolvedValueOnce({ digest: '0xrelease' });
|
|
89
|
+
const client = new TaskClient(
|
|
90
|
+
{
|
|
91
|
+
executeTransaction,
|
|
92
|
+
getObject: vi.fn(),
|
|
93
|
+
} as unknown as MeshSuiClient,
|
|
94
|
+
networkConfig,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const posted = await client.postMeteredTask({
|
|
98
|
+
capability: 'summarize',
|
|
99
|
+
category: 'analysis',
|
|
100
|
+
inputBlobId: 'blob-1',
|
|
101
|
+
agreementHash: 'hash-1',
|
|
102
|
+
maxPriceMist: 900n,
|
|
103
|
+
unitPriceMist: 300n,
|
|
104
|
+
disputeWindowMs: 60_000,
|
|
105
|
+
expiryHours: 24,
|
|
106
|
+
keypair: {} as unknown as Ed25519Keypair,
|
|
107
|
+
});
|
|
108
|
+
await client.completeMeteredTask({
|
|
109
|
+
taskId: '0x9',
|
|
110
|
+
resultBlobId: 'blob-2',
|
|
111
|
+
meteredUnits: 2,
|
|
112
|
+
verificationHash: 'aa'.repeat(32),
|
|
113
|
+
keypair: {} as unknown as Ed25519Keypair,
|
|
114
|
+
});
|
|
115
|
+
await client.releaseMeteredPayment({
|
|
116
|
+
taskId: '0x9',
|
|
117
|
+
keypair: {} as unknown as Ed25519Keypair,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(posted).toEqual({ txDigest: '0xpost', taskId: '0x9' });
|
|
121
|
+
const postCommands = getCommands(executeTransaction.mock.calls[0]?.[0]);
|
|
122
|
+
expect(postCommands.map((command) => String(command.$kind))).toContain('SplitCoins');
|
|
123
|
+
const completeCommands = getCommands(executeTransaction.mock.calls[1]?.[0]);
|
|
124
|
+
expect(completeCommands[0]?.$kind).toBe('MoveCall');
|
|
125
|
+
const releaseCommands = getCommands(executeTransaction.mock.calls[2]?.[0]);
|
|
126
|
+
expect(releaseCommands[0]?.$kind).toBe('MoveCall');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('parses task objects', async () => {
|
|
130
|
+
const client = new TaskClient(
|
|
131
|
+
{
|
|
132
|
+
executeTransaction: vi.fn(),
|
|
133
|
+
getObject: vi.fn().mockResolvedValue({
|
|
134
|
+
id: '0xtask',
|
|
135
|
+
requester: '0xrequester',
|
|
136
|
+
provider: '0x0',
|
|
137
|
+
capability: 'summarize',
|
|
138
|
+
category: 'analysis',
|
|
139
|
+
input_blob_id: 'blob-1',
|
|
140
|
+
price: '500',
|
|
141
|
+
status: 0,
|
|
142
|
+
dispute_window_ms: 60_000,
|
|
143
|
+
created_at: 1_000,
|
|
144
|
+
expires_at: 2_000,
|
|
145
|
+
agreement_hash: 'hash-1',
|
|
146
|
+
}),
|
|
147
|
+
} as unknown as MeshSuiClient,
|
|
148
|
+
networkConfig,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const task = await client.getTask('0xtask');
|
|
152
|
+
|
|
153
|
+
expect(task).toEqual({
|
|
154
|
+
id: '0xtask',
|
|
155
|
+
requester: '0xrequester',
|
|
156
|
+
provider: undefined,
|
|
157
|
+
capability: 'summarize',
|
|
158
|
+
category: 'analysis',
|
|
159
|
+
inputBlobId: 'blob-1',
|
|
160
|
+
resultBlobId: undefined,
|
|
161
|
+
price: 500n,
|
|
162
|
+
paymentScheme: undefined,
|
|
163
|
+
maxPrice: undefined,
|
|
164
|
+
meteredUnits: undefined,
|
|
165
|
+
unitPrice: undefined,
|
|
166
|
+
verificationHash: undefined,
|
|
167
|
+
status: TaskStatus.OPEN,
|
|
168
|
+
disputeWindowMs: 60_000,
|
|
169
|
+
createdAt: 1_000,
|
|
170
|
+
acceptedAt: undefined,
|
|
171
|
+
completedAt: undefined,
|
|
172
|
+
expiresAt: 2_000,
|
|
173
|
+
agreementHash: 'hash-1',
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('parses metered task objects', async () => {
|
|
178
|
+
const client = new TaskClient(
|
|
179
|
+
{
|
|
180
|
+
executeTransaction: vi.fn(),
|
|
181
|
+
getObject: vi.fn().mockResolvedValue({
|
|
182
|
+
id: '0xmetered',
|
|
183
|
+
requester: '0xrequester',
|
|
184
|
+
provider: '0xprovider',
|
|
185
|
+
capability: 'summarize',
|
|
186
|
+
category: 'analysis',
|
|
187
|
+
input_blob_id: 'blob-1',
|
|
188
|
+
result_blob_id: 'blob-2',
|
|
189
|
+
price: '600',
|
|
190
|
+
payment_scheme: 1,
|
|
191
|
+
max_price: '1000',
|
|
192
|
+
metered_units: 2,
|
|
193
|
+
unit_price: '300',
|
|
194
|
+
verification_hash: Array.from(Buffer.from('aa'.repeat(32), 'hex')),
|
|
195
|
+
status: 2,
|
|
196
|
+
dispute_window_ms: 60_000,
|
|
197
|
+
created_at: 1_000,
|
|
198
|
+
accepted_at: 1_100,
|
|
199
|
+
completed_at: 1_200,
|
|
200
|
+
expires_at: 2_000,
|
|
201
|
+
agreement_hash: 'hash-1',
|
|
202
|
+
}),
|
|
203
|
+
} as unknown as MeshSuiClient,
|
|
204
|
+
networkConfig,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await expect(client.getTask('0xmetered')).resolves.toEqual({
|
|
208
|
+
id: '0xmetered',
|
|
209
|
+
requester: '0xrequester',
|
|
210
|
+
provider: '0xprovider',
|
|
211
|
+
capability: 'summarize',
|
|
212
|
+
category: 'analysis',
|
|
213
|
+
inputBlobId: 'blob-1',
|
|
214
|
+
resultBlobId: 'blob-2',
|
|
215
|
+
price: 600n,
|
|
216
|
+
paymentScheme: PaymentScheme.UPTO,
|
|
217
|
+
maxPrice: 1000n,
|
|
218
|
+
meteredUnits: 2,
|
|
219
|
+
unitPrice: 300n,
|
|
220
|
+
verificationHash: 'aa'.repeat(32),
|
|
221
|
+
status: TaskStatus.COMPLETED,
|
|
222
|
+
disputeWindowMs: 60_000,
|
|
223
|
+
createdAt: 1_000,
|
|
224
|
+
acceptedAt: 1_100,
|
|
225
|
+
completedAt: 1_200,
|
|
226
|
+
expiresAt: 2_000,
|
|
227
|
+
agreementHash: 'hash-1',
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('rejects invalid task ids before submitting task mutations', async () => {
|
|
232
|
+
const executeTransaction = vi.fn();
|
|
233
|
+
const client = new TaskClient(
|
|
234
|
+
{
|
|
235
|
+
executeTransaction,
|
|
236
|
+
getObject: vi.fn(),
|
|
237
|
+
} as unknown as MeshSuiClient,
|
|
238
|
+
networkConfig,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
await expect(
|
|
242
|
+
client.acceptTask({
|
|
243
|
+
taskId: 'not-an-object-id',
|
|
244
|
+
keypair: {} as unknown as Ed25519Keypair,
|
|
245
|
+
}),
|
|
246
|
+
).rejects.toThrow('taskId must be a 0x-prefixed hex object id.');
|
|
247
|
+
expect(executeTransaction).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildClaimPaymentTx,
|
|
5
|
+
buildCompleteMeteredTaskTx,
|
|
6
|
+
buildOpenDisputeTx,
|
|
7
|
+
buildPostMeteredTaskTx,
|
|
8
|
+
buildReleaseMeteredPaymentTx,
|
|
9
|
+
} from '../src/sui/tx-helpers.js';
|
|
10
|
+
|
|
11
|
+
describe('tx-helpers object id validation', () => {
|
|
12
|
+
it('rejects all-zero object ids while preserving short test ids', () => {
|
|
13
|
+
expect(() => buildClaimPaymentTx({ packageId: '0x1', taskId: '0x3' })).not.toThrow();
|
|
14
|
+
expect(() => buildClaimPaymentTx({ packageId: '0x1', taskId: '0x0' })).toThrow(
|
|
15
|
+
'taskId must be a 0x-prefixed hex object id.',
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('rejects object ids longer than 32 bytes', () => {
|
|
20
|
+
expect(() => buildClaimPaymentTx({ packageId: `0x${'1'.repeat(65)}`, taskId: '0x3' })).toThrow(
|
|
21
|
+
'packageId must be a 0x-prefixed hex object id.',
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rejects zero arbitrator addresses for dispute opening', () => {
|
|
26
|
+
expect(() => buildOpenDisputeTx({
|
|
27
|
+
packageId: '0x1',
|
|
28
|
+
taskId: '0x3',
|
|
29
|
+
evidenceBlobId: 'walrus:evidence',
|
|
30
|
+
proposedSplitMist: 1n,
|
|
31
|
+
arbitratorAddress: '0x0',
|
|
32
|
+
})).toThrow('arbitratorAddress must be a 0x-prefixed hex object id.');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('builds metered task transactions', () => {
|
|
36
|
+
expect(() => buildPostMeteredTaskTx({
|
|
37
|
+
packageId: '0x1',
|
|
38
|
+
registryId: '0x2',
|
|
39
|
+
capability: 'summarize',
|
|
40
|
+
category: 'analysis',
|
|
41
|
+
inputBlobId: 'blob-1',
|
|
42
|
+
agreementHash: 'hash-1',
|
|
43
|
+
maxPriceMist: 10n,
|
|
44
|
+
unitPriceMist: 2n,
|
|
45
|
+
disputeWindowMs: 60_000,
|
|
46
|
+
expiryHours: 24,
|
|
47
|
+
})).not.toThrow();
|
|
48
|
+
expect(() => buildCompleteMeteredTaskTx({
|
|
49
|
+
packageId: '0x1',
|
|
50
|
+
taskId: '0x3',
|
|
51
|
+
resultBlobId: 'blob-2',
|
|
52
|
+
meteredUnits: 2,
|
|
53
|
+
verificationHash: 'aa'.repeat(32),
|
|
54
|
+
})).not.toThrow();
|
|
55
|
+
expect(() => buildReleaseMeteredPaymentTx({
|
|
56
|
+
packageId: '0x1',
|
|
57
|
+
taskId: '0x3',
|
|
58
|
+
})).not.toThrow();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects invalid metered completion hashes', () => {
|
|
62
|
+
expect(() => buildCompleteMeteredTaskTx({
|
|
63
|
+
packageId: '0x1',
|
|
64
|
+
taskId: '0x3',
|
|
65
|
+
resultBlobId: 'blob-2',
|
|
66
|
+
meteredUnits: 2,
|
|
67
|
+
verificationHash: 'xyz',
|
|
68
|
+
})).toThrow('verificationHash must be an even-length hex string.');
|
|
69
|
+
});
|
|
70
|
+
});
|