@ophirai/sdk 0.1.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/README.md +139 -0
- package/dist/__tests__/buyer.test.d.ts +1 -0
- package/dist/__tests__/buyer.test.js +664 -0
- package/dist/__tests__/discovery.test.d.ts +1 -0
- package/dist/__tests__/discovery.test.js +188 -0
- package/dist/__tests__/escrow.test.d.ts +1 -0
- package/dist/__tests__/escrow.test.js +385 -0
- package/dist/__tests__/identity.test.d.ts +1 -0
- package/dist/__tests__/identity.test.js +222 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +681 -0
- package/dist/__tests__/lockstep.test.d.ts +1 -0
- package/dist/__tests__/lockstep.test.js +320 -0
- package/dist/__tests__/messages.test.d.ts +1 -0
- package/dist/__tests__/messages.test.js +976 -0
- package/dist/__tests__/negotiation.test.d.ts +1 -0
- package/dist/__tests__/negotiation.test.js +667 -0
- package/dist/__tests__/seller.test.d.ts +1 -0
- package/dist/__tests__/seller.test.js +767 -0
- package/dist/__tests__/server.test.d.ts +1 -0
- package/dist/__tests__/server.test.js +239 -0
- package/dist/__tests__/signing.test.d.ts +1 -0
- package/dist/__tests__/signing.test.js +713 -0
- package/dist/__tests__/sla.test.d.ts +1 -0
- package/dist/__tests__/sla.test.js +342 -0
- package/dist/__tests__/transport.test.d.ts +1 -0
- package/dist/__tests__/transport.test.js +197 -0
- package/dist/__tests__/x402.test.d.ts +1 -0
- package/dist/__tests__/x402.test.js +141 -0
- package/dist/buyer.d.ts +190 -0
- package/dist/buyer.js +555 -0
- package/dist/discovery.d.ts +47 -0
- package/dist/discovery.js +51 -0
- package/dist/escrow.d.ts +177 -0
- package/dist/escrow.js +434 -0
- package/dist/identity.d.ts +60 -0
- package/dist/identity.js +108 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +43 -0
- package/dist/lockstep.d.ts +94 -0
- package/dist/lockstep.js +127 -0
- package/dist/messages.d.ts +172 -0
- package/dist/messages.js +262 -0
- package/dist/negotiation.d.ts +113 -0
- package/dist/negotiation.js +214 -0
- package/dist/seller.d.ts +127 -0
- package/dist/seller.js +395 -0
- package/dist/server.d.ts +52 -0
- package/dist/server.js +149 -0
- package/dist/signing.d.ts +98 -0
- package/dist/signing.js +165 -0
- package/dist/sla.d.ts +95 -0
- package/dist/sla.js +187 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.js +127 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +1 -0
- package/dist/x402.d.ts +25 -0
- package/dist/x402.js +54 -0
- package/package.json +40 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { discoverAgents, parseAgentCard } from '../discovery.js';
|
|
3
|
+
function makeCard(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
name: 'Test Agent',
|
|
6
|
+
description: 'A test seller agent',
|
|
7
|
+
url: 'https://agent.example.com',
|
|
8
|
+
capabilities: {
|
|
9
|
+
negotiation: {
|
|
10
|
+
supported: true,
|
|
11
|
+
endpoint: 'https://agent.example.com/negotiate',
|
|
12
|
+
protocols: ['ophir/1.0'],
|
|
13
|
+
acceptedPayments: [{ network: 'solana', token: 'USDC' }],
|
|
14
|
+
negotiationStyles: ['rfq'],
|
|
15
|
+
maxNegotiationRounds: 5,
|
|
16
|
+
services: [
|
|
17
|
+
{
|
|
18
|
+
category: 'inference',
|
|
19
|
+
description: 'LLM inference',
|
|
20
|
+
base_price: '0.005',
|
|
21
|
+
currency: 'USDC',
|
|
22
|
+
unit: 'request',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('parseAgentCard', () => {
|
|
31
|
+
it('extracts SellerInfo from a card with negotiation capability', () => {
|
|
32
|
+
const card = makeCard();
|
|
33
|
+
const info = parseAgentCard(card);
|
|
34
|
+
expect(info).not.toBeNull();
|
|
35
|
+
expect(info.agentId).toBe('https://agent.example.com');
|
|
36
|
+
expect(info.endpoint).toBe('https://agent.example.com/negotiate');
|
|
37
|
+
expect(info.services).toHaveLength(1);
|
|
38
|
+
expect(info.services[0]).toEqual({
|
|
39
|
+
category: 'inference',
|
|
40
|
+
description: 'LLM inference',
|
|
41
|
+
base_price: '0.005',
|
|
42
|
+
currency: 'USDC',
|
|
43
|
+
unit: 'request',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it('returns null for cards without negotiation capability', () => {
|
|
47
|
+
const card = makeCard({
|
|
48
|
+
capabilities: { streaming: { supported: true } },
|
|
49
|
+
});
|
|
50
|
+
const info = parseAgentCard(card);
|
|
51
|
+
expect(info).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it('returns null when negotiation.supported is false', () => {
|
|
54
|
+
const card = makeCard();
|
|
55
|
+
card.capabilities.negotiation.supported = false;
|
|
56
|
+
const info = parseAgentCard(card);
|
|
57
|
+
expect(info).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
it('returns null when services array is empty', () => {
|
|
60
|
+
const card = makeCard();
|
|
61
|
+
card.capabilities.negotiation.services = [];
|
|
62
|
+
const info = parseAgentCard(card);
|
|
63
|
+
expect(info).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('handles multiple services', () => {
|
|
66
|
+
const card = makeCard();
|
|
67
|
+
card.capabilities.negotiation.services = [
|
|
68
|
+
{
|
|
69
|
+
category: 'inference',
|
|
70
|
+
description: 'LLM inference',
|
|
71
|
+
base_price: '0.005',
|
|
72
|
+
currency: 'USDC',
|
|
73
|
+
unit: 'request',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
category: 'translation',
|
|
77
|
+
description: 'Text translation',
|
|
78
|
+
base_price: '0.002',
|
|
79
|
+
currency: 'USDC',
|
|
80
|
+
unit: 'request',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
const info = parseAgentCard(card);
|
|
84
|
+
expect(info).not.toBeNull();
|
|
85
|
+
expect(info.services).toHaveLength(2);
|
|
86
|
+
expect(info.services[0].category).toBe('inference');
|
|
87
|
+
expect(info.services[1].category).toBe('translation');
|
|
88
|
+
});
|
|
89
|
+
it('returns null when capabilities is undefined', () => {
|
|
90
|
+
const card = { name: 'X', description: 'X', url: 'http://x', capabilities: {} };
|
|
91
|
+
const info = parseAgentCard(card);
|
|
92
|
+
expect(info).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('discoverAgents', () => {
|
|
96
|
+
const originalFetch = globalThis.fetch;
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
99
|
+
});
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
globalThis.fetch = originalFetch;
|
|
102
|
+
vi.restoreAllMocks();
|
|
103
|
+
});
|
|
104
|
+
it('fetches /.well-known/agent.json from each endpoint', async () => {
|
|
105
|
+
const card = makeCard();
|
|
106
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
107
|
+
ok: true,
|
|
108
|
+
json: () => Promise.resolve(card),
|
|
109
|
+
});
|
|
110
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
111
|
+
const agents = await discoverAgents(['https://a.com', 'https://b.com']);
|
|
112
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
113
|
+
expect(mockFetch).toHaveBeenCalledWith('https://a.com/.well-known/agent.json');
|
|
114
|
+
expect(mockFetch).toHaveBeenCalledWith('https://b.com/.well-known/agent.json');
|
|
115
|
+
expect(agents).toHaveLength(2);
|
|
116
|
+
});
|
|
117
|
+
it('strips trailing slash from endpoints', async () => {
|
|
118
|
+
const card = makeCard();
|
|
119
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
120
|
+
ok: true,
|
|
121
|
+
json: () => Promise.resolve(card),
|
|
122
|
+
});
|
|
123
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
124
|
+
await discoverAgents(['https://a.com/']);
|
|
125
|
+
expect(mockFetch).toHaveBeenCalledWith('https://a.com/.well-known/agent.json');
|
|
126
|
+
});
|
|
127
|
+
it('skips unreachable endpoints gracefully', async () => {
|
|
128
|
+
const card = makeCard();
|
|
129
|
+
const mockFetch = vi.fn()
|
|
130
|
+
.mockRejectedValueOnce(new Error('ECONNREFUSED'))
|
|
131
|
+
.mockResolvedValueOnce({
|
|
132
|
+
ok: true,
|
|
133
|
+
json: () => Promise.resolve(card),
|
|
134
|
+
});
|
|
135
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
136
|
+
const agents = await discoverAgents(['https://down.com', 'https://up.com']);
|
|
137
|
+
expect(agents).toHaveLength(1);
|
|
138
|
+
expect(agents[0].name).toBe('Test Agent');
|
|
139
|
+
});
|
|
140
|
+
it('skips endpoints that return non-200 status', async () => {
|
|
141
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: false, status: 404 });
|
|
142
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
143
|
+
const agents = await discoverAgents(['https://a.com']);
|
|
144
|
+
expect(agents).toHaveLength(0);
|
|
145
|
+
});
|
|
146
|
+
it('filters out cards without negotiation support', async () => {
|
|
147
|
+
const noNegCard = {
|
|
148
|
+
name: 'No Neg',
|
|
149
|
+
description: 'No negotiation',
|
|
150
|
+
url: 'https://a.com',
|
|
151
|
+
capabilities: {},
|
|
152
|
+
};
|
|
153
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
154
|
+
ok: true,
|
|
155
|
+
json: () => Promise.resolve(noNegCard),
|
|
156
|
+
});
|
|
157
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
158
|
+
const agents = await discoverAgents(['https://a.com']);
|
|
159
|
+
expect(agents).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
it('returns empty array for empty endpoints list', async () => {
|
|
162
|
+
const agents = await discoverAgents([]);
|
|
163
|
+
expect(agents).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
it('handles mix of successful, failed, and unsupported endpoints', async () => {
|
|
166
|
+
const goodCard = makeCard({ name: 'Good' });
|
|
167
|
+
const noNegCard = {
|
|
168
|
+
name: 'NoNeg',
|
|
169
|
+
description: 'x',
|
|
170
|
+
url: 'https://noneg.com',
|
|
171
|
+
capabilities: {},
|
|
172
|
+
};
|
|
173
|
+
const mockFetch = vi.fn()
|
|
174
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(goodCard) })
|
|
175
|
+
.mockRejectedValueOnce(new Error('timeout'))
|
|
176
|
+
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(noNegCard) })
|
|
177
|
+
.mockResolvedValueOnce({ ok: false, status: 500 });
|
|
178
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
179
|
+
const agents = await discoverAgents([
|
|
180
|
+
'https://good.com',
|
|
181
|
+
'https://down.com',
|
|
182
|
+
'https://noneg.com',
|
|
183
|
+
'https://error.com',
|
|
184
|
+
]);
|
|
185
|
+
expect(agents).toHaveLength(1);
|
|
186
|
+
expect(agents[0].name).toBe('Good');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { EscrowManager } from '../escrow.js';
|
|
3
|
+
import { PublicKey } from '@solana/web3.js';
|
|
4
|
+
import nacl from 'tweetnacl';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
describe('EscrowManager', () => {
|
|
7
|
+
describe('constructor defaults', () => {
|
|
8
|
+
it('uses devnet RPC and default program ID when no config provided', () => {
|
|
9
|
+
const mgr = new EscrowManager();
|
|
10
|
+
expect(mgr).toBeInstanceOf(EscrowManager);
|
|
11
|
+
});
|
|
12
|
+
it('accepts custom rpcUrl and programId', () => {
|
|
13
|
+
const mgr = new EscrowManager({
|
|
14
|
+
rpcUrl: 'https://api.mainnet-beta.solana.com',
|
|
15
|
+
programId: '11111111111111111111111111111111',
|
|
16
|
+
});
|
|
17
|
+
expect(mgr).toBeInstanceOf(EscrowManager);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('PDA derivation', () => {
|
|
21
|
+
it('deriveEscrowAddress is deterministic', () => {
|
|
22
|
+
const mgr = new EscrowManager();
|
|
23
|
+
const kp = nacl.sign.keyPair();
|
|
24
|
+
const hash = createHash('sha256').update('test-agreement').digest();
|
|
25
|
+
const addr1 = mgr.deriveEscrowAddress(kp.publicKey, hash);
|
|
26
|
+
const addr2 = mgr.deriveEscrowAddress(kp.publicKey, hash);
|
|
27
|
+
expect(addr1.address).toBe(addr2.address);
|
|
28
|
+
expect(addr1.bump).toBe(addr2.bump);
|
|
29
|
+
expect(typeof addr1.address).toBe('string');
|
|
30
|
+
expect(addr1.address.length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
it('deriveVaultAddress is deterministic', () => {
|
|
33
|
+
const mgr = new EscrowManager();
|
|
34
|
+
const kp = nacl.sign.keyPair();
|
|
35
|
+
const hash = createHash('sha256').update('test-agreement').digest();
|
|
36
|
+
const { address: escrowAddr } = mgr.deriveEscrowAddress(kp.publicKey, hash);
|
|
37
|
+
const escrowBytes = new PublicKey(escrowAddr).toBytes();
|
|
38
|
+
const vault1 = mgr.deriveVaultAddress(escrowBytes);
|
|
39
|
+
const vault2 = mgr.deriveVaultAddress(escrowBytes);
|
|
40
|
+
expect(vault1.address).toBe(vault2.address);
|
|
41
|
+
expect(vault1.bump).toBe(vault2.bump);
|
|
42
|
+
expect(typeof vault1.address).toBe('string');
|
|
43
|
+
});
|
|
44
|
+
it('different buyers produce different PDAs', () => {
|
|
45
|
+
const mgr = new EscrowManager();
|
|
46
|
+
const kp1 = nacl.sign.keyPair();
|
|
47
|
+
const kp2 = nacl.sign.keyPair();
|
|
48
|
+
const hash = createHash('sha256').update('test-agreement').digest();
|
|
49
|
+
const addr1 = mgr.deriveEscrowAddress(kp1.publicKey, hash);
|
|
50
|
+
const addr2 = mgr.deriveEscrowAddress(kp2.publicKey, hash);
|
|
51
|
+
expect(addr1.address).not.toBe(addr2.address);
|
|
52
|
+
});
|
|
53
|
+
it('different agreement hashes produce different PDAs', () => {
|
|
54
|
+
const mgr = new EscrowManager();
|
|
55
|
+
const kp = nacl.sign.keyPair();
|
|
56
|
+
const hash1 = createHash('sha256').update('agreement-1').digest();
|
|
57
|
+
const hash2 = createHash('sha256').update('agreement-2').digest();
|
|
58
|
+
const addr1 = mgr.deriveEscrowAddress(kp.publicKey, hash1);
|
|
59
|
+
const addr2 = mgr.deriveEscrowAddress(kp.publicKey, hash2);
|
|
60
|
+
expect(addr1.address).not.toBe(addr2.address);
|
|
61
|
+
});
|
|
62
|
+
it('escrow and vault addresses differ for same keypair', () => {
|
|
63
|
+
const mgr = new EscrowManager();
|
|
64
|
+
const kp = nacl.sign.keyPair();
|
|
65
|
+
const hash = createHash('sha256').update('test').digest();
|
|
66
|
+
const { address: escrowAddr } = mgr.deriveEscrowAddress(kp.publicKey, hash);
|
|
67
|
+
const { address: vaultAddr } = mgr.deriveVaultAddress(new PublicKey(escrowAddr).toBytes());
|
|
68
|
+
expect(escrowAddr).not.toBe(vaultAddr);
|
|
69
|
+
});
|
|
70
|
+
it('PDA address is a valid base58 Solana address', () => {
|
|
71
|
+
const mgr = new EscrowManager();
|
|
72
|
+
const kp = nacl.sign.keyPair();
|
|
73
|
+
const hash = createHash('sha256').update('validity-test').digest();
|
|
74
|
+
const { address } = mgr.deriveEscrowAddress(kp.publicKey, hash);
|
|
75
|
+
// Valid Solana address is 32-44 characters of base58
|
|
76
|
+
expect(address).toMatch(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
|
|
77
|
+
// Must be parseable as a PublicKey
|
|
78
|
+
expect(() => new PublicKey(address)).not.toThrow();
|
|
79
|
+
});
|
|
80
|
+
it('bump is a valid byte (0-255)', () => {
|
|
81
|
+
const mgr = new EscrowManager();
|
|
82
|
+
const kp = nacl.sign.keyPair();
|
|
83
|
+
const hash = createHash('sha256').update('bump-test').digest();
|
|
84
|
+
const { bump } = mgr.deriveEscrowAddress(kp.publicKey, hash);
|
|
85
|
+
expect(bump).toBeGreaterThanOrEqual(0);
|
|
86
|
+
expect(bump).toBeLessThanOrEqual(255);
|
|
87
|
+
expect(Number.isInteger(bump)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
it('same program ID produces consistent PDA across manager instances', () => {
|
|
90
|
+
const config = { programId: '11111111111111111111111111111111' };
|
|
91
|
+
const mgr1 = new EscrowManager(config);
|
|
92
|
+
const mgr2 = new EscrowManager(config);
|
|
93
|
+
const kp = nacl.sign.keyPair();
|
|
94
|
+
const hash = createHash('sha256').update('consistency').digest();
|
|
95
|
+
const addr1 = mgr1.deriveEscrowAddress(kp.publicKey, hash);
|
|
96
|
+
const addr2 = mgr2.deriveEscrowAddress(kp.publicKey, hash);
|
|
97
|
+
expect(addr1.address).toBe(addr2.address);
|
|
98
|
+
expect(addr1.bump).toBe(addr2.bump);
|
|
99
|
+
});
|
|
100
|
+
it('different program IDs produce different PDAs for same inputs', () => {
|
|
101
|
+
const mgr1 = new EscrowManager({ programId: 'CHwqh23SpWSM6WLsd15iQcP4KSkB351S9eGcN4fQSVqy' });
|
|
102
|
+
const mgr2 = new EscrowManager({ programId: '11111111111111111111111111111111' });
|
|
103
|
+
const kp = nacl.sign.keyPair();
|
|
104
|
+
const hash = createHash('sha256').update('program-diff').digest();
|
|
105
|
+
const addr1 = mgr1.deriveEscrowAddress(kp.publicKey, hash);
|
|
106
|
+
const addr2 = mgr2.deriveEscrowAddress(kp.publicKey, hash);
|
|
107
|
+
expect(addr1.address).not.toBe(addr2.address);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('input validation', () => {
|
|
111
|
+
it('throws on invalid buyer public key length (too short)', () => {
|
|
112
|
+
const mgr = new EscrowManager();
|
|
113
|
+
const shortKey = new Uint8Array(16);
|
|
114
|
+
const hash = createHash('sha256').update('test').digest();
|
|
115
|
+
expect(() => mgr.deriveEscrowAddress(shortKey, hash)).toThrow('Invalid buyer public key length: expected 32, got 16');
|
|
116
|
+
});
|
|
117
|
+
it('throws on invalid buyer public key length (too long)', () => {
|
|
118
|
+
const mgr = new EscrowManager();
|
|
119
|
+
const longKey = new Uint8Array(64);
|
|
120
|
+
const hash = createHash('sha256').update('test').digest();
|
|
121
|
+
expect(() => mgr.deriveEscrowAddress(longKey, hash)).toThrow('Invalid buyer public key length: expected 32, got 64');
|
|
122
|
+
});
|
|
123
|
+
it('throws on invalid agreement hash length (too short)', () => {
|
|
124
|
+
const mgr = new EscrowManager();
|
|
125
|
+
const kp = nacl.sign.keyPair();
|
|
126
|
+
const shortHash = new Uint8Array(16);
|
|
127
|
+
expect(() => mgr.deriveEscrowAddress(kp.publicKey, shortHash)).toThrow('Invalid agreement hash length: expected 32, got 16');
|
|
128
|
+
});
|
|
129
|
+
it('throws on invalid agreement hash length (too long)', () => {
|
|
130
|
+
const mgr = new EscrowManager();
|
|
131
|
+
const kp = nacl.sign.keyPair();
|
|
132
|
+
const longHash = new Uint8Array(64);
|
|
133
|
+
expect(() => mgr.deriveEscrowAddress(kp.publicKey, longHash)).toThrow('Invalid agreement hash length: expected 32, got 64');
|
|
134
|
+
});
|
|
135
|
+
it('throws on empty buyer public key', () => {
|
|
136
|
+
const mgr = new EscrowManager();
|
|
137
|
+
const emptyKey = new Uint8Array(0);
|
|
138
|
+
const hash = createHash('sha256').update('test').digest();
|
|
139
|
+
expect(() => mgr.deriveEscrowAddress(emptyKey, hash)).toThrow('Invalid buyer public key length: expected 32, got 0');
|
|
140
|
+
});
|
|
141
|
+
it('throws on empty agreement hash', () => {
|
|
142
|
+
const mgr = new EscrowManager();
|
|
143
|
+
const kp = nacl.sign.keyPair();
|
|
144
|
+
const emptyHash = new Uint8Array(0);
|
|
145
|
+
expect(() => mgr.deriveEscrowAddress(kp.publicKey, emptyHash)).toThrow('Invalid agreement hash length: expected 32, got 0');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('createEscrow input validation', () => {
|
|
149
|
+
it('rejects zero deposit amount', async () => {
|
|
150
|
+
const mgr = new EscrowManager();
|
|
151
|
+
const buyerKp = nacl.sign.keyPair();
|
|
152
|
+
const sellerKp = nacl.sign.keyPair();
|
|
153
|
+
await expect(mgr.createEscrow({
|
|
154
|
+
agreement: {
|
|
155
|
+
agreement_id: 'test-id',
|
|
156
|
+
rfq_id: 'rfq-id',
|
|
157
|
+
accepting_message_id: 'quote-id',
|
|
158
|
+
final_terms: { price_per_unit: '1', currency: 'USDC', unit: 'req' },
|
|
159
|
+
agreement_hash: createHash('sha256').update('test').digest('hex'),
|
|
160
|
+
buyer_signature: 'sig1',
|
|
161
|
+
seller_signature: 'sig2',
|
|
162
|
+
},
|
|
163
|
+
buyerKeypair: buyerKp,
|
|
164
|
+
sellerPublicKey: sellerKp.publicKey,
|
|
165
|
+
depositAmount: 0n,
|
|
166
|
+
mintAddress: 'So11111111111111111111111111111111111111112',
|
|
167
|
+
buyerTokenAccount: 'So11111111111111111111111111111111111111112',
|
|
168
|
+
})).rejects.toThrow('Deposit amount must be greater than zero');
|
|
169
|
+
});
|
|
170
|
+
it('rejects invalid seller public key length', async () => {
|
|
171
|
+
const mgr = new EscrowManager();
|
|
172
|
+
const buyerKp = nacl.sign.keyPair();
|
|
173
|
+
await expect(mgr.createEscrow({
|
|
174
|
+
agreement: {
|
|
175
|
+
agreement_id: 'test-id',
|
|
176
|
+
rfq_id: 'rfq-id',
|
|
177
|
+
accepting_message_id: 'quote-id',
|
|
178
|
+
final_terms: { price_per_unit: '1', currency: 'USDC', unit: 'req' },
|
|
179
|
+
agreement_hash: createHash('sha256').update('test').digest('hex'),
|
|
180
|
+
buyer_signature: 'sig1',
|
|
181
|
+
seller_signature: 'sig2',
|
|
182
|
+
},
|
|
183
|
+
buyerKeypair: buyerKp,
|
|
184
|
+
sellerPublicKey: new Uint8Array(16),
|
|
185
|
+
depositAmount: 1000n,
|
|
186
|
+
mintAddress: 'So11111111111111111111111111111111111111112',
|
|
187
|
+
buyerTokenAccount: 'So11111111111111111111111111111111111111112',
|
|
188
|
+
})).rejects.toThrow('Invalid seller public key length');
|
|
189
|
+
});
|
|
190
|
+
it('rejects penalty rate exceeding 10000 basis points', async () => {
|
|
191
|
+
const mgr = new EscrowManager();
|
|
192
|
+
const buyerKp = nacl.sign.keyPair();
|
|
193
|
+
const sellerKp = nacl.sign.keyPair();
|
|
194
|
+
await expect(mgr.createEscrow({
|
|
195
|
+
agreement: {
|
|
196
|
+
agreement_id: 'test-id',
|
|
197
|
+
rfq_id: 'rfq-id',
|
|
198
|
+
accepting_message_id: 'quote-id',
|
|
199
|
+
final_terms: { price_per_unit: '1', currency: 'USDC', unit: 'req' },
|
|
200
|
+
agreement_hash: createHash('sha256').update('test').digest('hex'),
|
|
201
|
+
buyer_signature: 'sig1',
|
|
202
|
+
seller_signature: 'sig2',
|
|
203
|
+
},
|
|
204
|
+
buyerKeypair: buyerKp,
|
|
205
|
+
sellerPublicKey: sellerKp.publicKey,
|
|
206
|
+
depositAmount: 1000n,
|
|
207
|
+
mintAddress: 'So11111111111111111111111111111111111111112',
|
|
208
|
+
buyerTokenAccount: 'So11111111111111111111111111111111111111112',
|
|
209
|
+
penaltyRateBps: 15000,
|
|
210
|
+
})).rejects.toThrow('exceeds maximum 10000 basis points');
|
|
211
|
+
});
|
|
212
|
+
it('rejects invalid agreement hash (not 64 hex chars)', async () => {
|
|
213
|
+
const mgr = new EscrowManager();
|
|
214
|
+
const buyerKp = nacl.sign.keyPair();
|
|
215
|
+
const sellerKp = nacl.sign.keyPair();
|
|
216
|
+
await expect(mgr.createEscrow({
|
|
217
|
+
agreement: {
|
|
218
|
+
agreement_id: 'test-id',
|
|
219
|
+
rfq_id: 'rfq-id',
|
|
220
|
+
accepting_message_id: 'quote-id',
|
|
221
|
+
final_terms: { price_per_unit: '1', currency: 'USDC', unit: 'req' },
|
|
222
|
+
agreement_hash: 'tooshort',
|
|
223
|
+
buyer_signature: 'sig1',
|
|
224
|
+
seller_signature: 'sig2',
|
|
225
|
+
},
|
|
226
|
+
buyerKeypair: buyerKp,
|
|
227
|
+
sellerPublicKey: sellerKp.publicKey,
|
|
228
|
+
depositAmount: 1000n,
|
|
229
|
+
mintAddress: 'So11111111111111111111111111111111111111112',
|
|
230
|
+
buyerTokenAccount: 'So11111111111111111111111111111111111111112',
|
|
231
|
+
})).rejects.toThrow('Invalid agreement hash');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('releaseEscrow input validation', () => {
|
|
235
|
+
it('rejects invalid verification hash length', async () => {
|
|
236
|
+
const mgr = new EscrowManager();
|
|
237
|
+
const sellerKp = nacl.sign.keyPair();
|
|
238
|
+
await expect(mgr.releaseEscrow({
|
|
239
|
+
escrowAddress: '11111111111111111111111111111111',
|
|
240
|
+
sellerKeypair: sellerKp,
|
|
241
|
+
sellerTokenAccount: '11111111111111111111111111111111',
|
|
242
|
+
verificationHash: new Uint8Array(16), // wrong length
|
|
243
|
+
})).rejects.toThrow('Invalid verification hash length');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('disputeEscrow input validation', () => {
|
|
247
|
+
it('rejects invalid evidence hash length', async () => {
|
|
248
|
+
const mgr = new EscrowManager();
|
|
249
|
+
const buyerKp = nacl.sign.keyPair();
|
|
250
|
+
await expect(mgr.disputeEscrow({
|
|
251
|
+
escrowAddress: '11111111111111111111111111111111',
|
|
252
|
+
buyerKeypair: buyerKp,
|
|
253
|
+
buyerTokenAccount: '11111111111111111111111111111111',
|
|
254
|
+
sellerTokenAccount: '11111111111111111111111111111111',
|
|
255
|
+
evidenceHash: new Uint8Array(16), // wrong length
|
|
256
|
+
penaltyAmount: 100n,
|
|
257
|
+
})).rejects.toThrow('Invalid evidence hash length');
|
|
258
|
+
});
|
|
259
|
+
it('rejects negative penalty amount', async () => {
|
|
260
|
+
const mgr = new EscrowManager();
|
|
261
|
+
const buyerKp = nacl.sign.keyPair();
|
|
262
|
+
await expect(mgr.disputeEscrow({
|
|
263
|
+
escrowAddress: '11111111111111111111111111111111',
|
|
264
|
+
buyerKeypair: buyerKp,
|
|
265
|
+
buyerTokenAccount: '11111111111111111111111111111111',
|
|
266
|
+
sellerTokenAccount: '11111111111111111111111111111111',
|
|
267
|
+
evidenceHash: createHash('sha256').update('evidence').digest(),
|
|
268
|
+
penaltyAmount: -1n,
|
|
269
|
+
})).rejects.toThrow('Penalty amount cannot be negative');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe('EscrowManager additional coverage', () => {
|
|
274
|
+
describe('PDA determinism with known inputs', () => {
|
|
275
|
+
it('same inputs always produce same escrow address', () => {
|
|
276
|
+
const manager = new EscrowManager();
|
|
277
|
+
const buyerKey = new Uint8Array(32).fill(1);
|
|
278
|
+
const hash = new Uint8Array(32).fill(2);
|
|
279
|
+
const result1 = manager.deriveEscrowAddress(buyerKey, hash);
|
|
280
|
+
const result2 = manager.deriveEscrowAddress(buyerKey, hash);
|
|
281
|
+
expect(result1.address).toBe(result2.address);
|
|
282
|
+
expect(result1.bump).toBe(result2.bump);
|
|
283
|
+
});
|
|
284
|
+
it('same inputs always produce same vault address', () => {
|
|
285
|
+
const manager = new EscrowManager();
|
|
286
|
+
const escrowKey = new Uint8Array(32).fill(3);
|
|
287
|
+
const result1 = manager.deriveVaultAddress(escrowKey);
|
|
288
|
+
const result2 = manager.deriveVaultAddress(escrowKey);
|
|
289
|
+
expect(result1.address).toBe(result2.address);
|
|
290
|
+
expect(result1.bump).toBe(result2.bump);
|
|
291
|
+
});
|
|
292
|
+
it('all-zero buyer key produces valid PDA', () => {
|
|
293
|
+
const manager = new EscrowManager();
|
|
294
|
+
const result = manager.deriveEscrowAddress(new Uint8Array(32), new Uint8Array(32));
|
|
295
|
+
expect(result.address).toBeTruthy();
|
|
296
|
+
expect(typeof result.bump).toBe('number');
|
|
297
|
+
});
|
|
298
|
+
it('all-FF buyer key produces valid PDA', () => {
|
|
299
|
+
const manager = new EscrowManager();
|
|
300
|
+
const result = manager.deriveEscrowAddress(new Uint8Array(32).fill(0xFF), new Uint8Array(32).fill(0xFF));
|
|
301
|
+
expect(result.address).toBeTruthy();
|
|
302
|
+
expect(typeof result.bump).toBe('number');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
describe('escrow address uniqueness', () => {
|
|
306
|
+
it('100 different buyer keys produce 100 different escrow addresses', () => {
|
|
307
|
+
const manager = new EscrowManager();
|
|
308
|
+
const hash = new Uint8Array(32).fill(0);
|
|
309
|
+
const addresses = new Set();
|
|
310
|
+
for (let i = 0; i < 100; i++) {
|
|
311
|
+
const key = new Uint8Array(32);
|
|
312
|
+
key[0] = i;
|
|
313
|
+
addresses.add(manager.deriveEscrowAddress(key, hash).address);
|
|
314
|
+
}
|
|
315
|
+
expect(addresses.size).toBe(100);
|
|
316
|
+
});
|
|
317
|
+
it('100 different hashes produce 100 different escrow addresses', () => {
|
|
318
|
+
const manager = new EscrowManager();
|
|
319
|
+
const key = new Uint8Array(32).fill(0);
|
|
320
|
+
const addresses = new Set();
|
|
321
|
+
for (let i = 0; i < 100; i++) {
|
|
322
|
+
const hash = new Uint8Array(32);
|
|
323
|
+
hash[0] = i;
|
|
324
|
+
addresses.add(manager.deriveEscrowAddress(key, hash).address);
|
|
325
|
+
}
|
|
326
|
+
expect(addresses.size).toBe(100);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe('createEscrow input validation completeness', () => {
|
|
330
|
+
const validParams = {
|
|
331
|
+
agreement: {
|
|
332
|
+
agreement_id: 'test-agreement',
|
|
333
|
+
rfq_id: 'test-rfq',
|
|
334
|
+
accepting_message_id: 'quote-id',
|
|
335
|
+
final_terms: { price_per_unit: '0.01', currency: 'USDC', unit: 'request' },
|
|
336
|
+
agreement_hash: createHash('sha256').update('test').digest('hex'),
|
|
337
|
+
buyer_signature: 'sig',
|
|
338
|
+
seller_signature: 'sig',
|
|
339
|
+
},
|
|
340
|
+
buyerKeypair: nacl.sign.keyPair(),
|
|
341
|
+
sellerPublicKey: nacl.sign.keyPair().publicKey,
|
|
342
|
+
depositAmount: 1000000n,
|
|
343
|
+
mintAddress: 'So11111111111111111111111111111111111111112',
|
|
344
|
+
buyerTokenAccount: 'So11111111111111111111111111111111111111112',
|
|
345
|
+
};
|
|
346
|
+
it('rejects penaltyRateBps of exactly 10001', async () => {
|
|
347
|
+
const manager = new EscrowManager();
|
|
348
|
+
await expect(manager.createEscrow({ ...validParams, penaltyRateBps: 10001 })).rejects.toThrow('10000');
|
|
349
|
+
});
|
|
350
|
+
it('rejects agreement_hash that is not 64 hex chars', async () => {
|
|
351
|
+
const manager = new EscrowManager();
|
|
352
|
+
const badParams = {
|
|
353
|
+
...validParams,
|
|
354
|
+
agreement: { ...validParams.agreement, agreement_hash: 'short' },
|
|
355
|
+
};
|
|
356
|
+
await expect(manager.createEscrow(badParams)).rejects.toThrow('Invalid agreement hash');
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
describe('disputeEscrow validation', () => {
|
|
360
|
+
it('rejects zero-length evidence hash', async () => {
|
|
361
|
+
const manager = new EscrowManager();
|
|
362
|
+
const buyerKp = nacl.sign.keyPair();
|
|
363
|
+
await expect(manager.disputeEscrow({
|
|
364
|
+
escrowAddress: '11111111111111111111111111111111',
|
|
365
|
+
buyerKeypair: buyerKp,
|
|
366
|
+
buyerTokenAccount: '11111111111111111111111111111111',
|
|
367
|
+
sellerTokenAccount: '11111111111111111111111111111111',
|
|
368
|
+
evidenceHash: new Uint8Array(0),
|
|
369
|
+
penaltyAmount: 100n,
|
|
370
|
+
})).rejects.toThrow('evidence hash');
|
|
371
|
+
});
|
|
372
|
+
it('rejects 64-byte evidence hash (too long)', async () => {
|
|
373
|
+
const manager = new EscrowManager();
|
|
374
|
+
const buyerKp = nacl.sign.keyPair();
|
|
375
|
+
await expect(manager.disputeEscrow({
|
|
376
|
+
escrowAddress: '11111111111111111111111111111111',
|
|
377
|
+
buyerKeypair: buyerKp,
|
|
378
|
+
buyerTokenAccount: '11111111111111111111111111111111',
|
|
379
|
+
sellerTokenAccount: '11111111111111111111111111111111',
|
|
380
|
+
evidenceHash: new Uint8Array(64),
|
|
381
|
+
penaltyAmount: 100n,
|
|
382
|
+
})).rejects.toThrow('evidence hash');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|