@agirails/sdk 2.0.0-beta
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 +183 -0
- package/dist/ACTPClient.d.ts +52 -0
- package/dist/ACTPClient.d.ts.map +1 -0
- package/dist/ACTPClient.js +120 -0
- package/dist/ACTPClient.js.map +1 -0
- package/dist/abi/ACTPKernel.json +1340 -0
- package/dist/abi/ERC20.json +38 -0
- package/dist/abi/EscrowVault.json +64 -0
- package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
- package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
- package/dist/builders/DeliveryProofBuilder.js +165 -0
- package/dist/builders/DeliveryProofBuilder.js.map +1 -0
- package/dist/builders/QuoteBuilder.d.ts +68 -0
- package/dist/builders/QuoteBuilder.d.ts.map +1 -0
- package/dist/builders/QuoteBuilder.js +255 -0
- package/dist/builders/QuoteBuilder.js.map +1 -0
- package/dist/builders/index.d.ts +3 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +10 -0
- package/dist/builders/index.js.map +1 -0
- package/dist/config/networks.d.ts +27 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +103 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/errors/index.d.ts +38 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +87 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/ACTPKernel.d.ts +30 -0
- package/dist/protocol/ACTPKernel.d.ts.map +1 -0
- package/dist/protocol/ACTPKernel.js +261 -0
- package/dist/protocol/ACTPKernel.js.map +1 -0
- package/dist/protocol/EASHelper.d.ts +23 -0
- package/dist/protocol/EASHelper.d.ts.map +1 -0
- package/dist/protocol/EASHelper.js +106 -0
- package/dist/protocol/EASHelper.js.map +1 -0
- package/dist/protocol/EscrowVault.d.ts +24 -0
- package/dist/protocol/EscrowVault.d.ts.map +1 -0
- package/dist/protocol/EscrowVault.js +114 -0
- package/dist/protocol/EscrowVault.js.map +1 -0
- package/dist/protocol/EventMonitor.d.ts +18 -0
- package/dist/protocol/EventMonitor.d.ts.map +1 -0
- package/dist/protocol/EventMonitor.js +92 -0
- package/dist/protocol/EventMonitor.js.map +1 -0
- package/dist/protocol/MessageSigner.d.ts +23 -0
- package/dist/protocol/MessageSigner.d.ts.map +1 -0
- package/dist/protocol/MessageSigner.js +178 -0
- package/dist/protocol/MessageSigner.js.map +1 -0
- package/dist/protocol/ProofGenerator.d.ts +22 -0
- package/dist/protocol/ProofGenerator.d.ts.map +1 -0
- package/dist/protocol/ProofGenerator.js +64 -0
- package/dist/protocol/ProofGenerator.js.map +1 -0
- package/dist/protocol/QuoteBuilder.d.ts +2 -0
- package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
- package/dist/protocol/QuoteBuilder.js +7 -0
- package/dist/protocol/QuoteBuilder.js.map +1 -0
- package/dist/types/eip712.d.ts +106 -0
- package/dist/types/eip712.d.ts.map +1 -0
- package/dist/types/eip712.js +84 -0
- package/dist/types/eip712.js.map +1 -0
- package/dist/types/escrow.d.ts +18 -0
- package/dist/types/escrow.d.ts.map +1 -0
- package/dist/types/escrow.js +3 -0
- package/dist/types/escrow.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/message.d.ts +109 -0
- package/dist/types/message.d.ts.map +1 -0
- package/dist/types/message.js +3 -0
- package/dist/types/message.js.map +1 -0
- package/dist/types/state.d.ts +19 -0
- package/dist/types/state.d.ts.map +1 -0
- package/dist/types/state.js +49 -0
- package/dist/types/state.js.map +1 -0
- package/dist/types/transaction.d.ts +36 -0
- package/dist/types/transaction.d.ts.map +1 -0
- package/dist/types/transaction.js +3 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/utils/IPFSClient.d.ts +37 -0
- package/dist/utils/IPFSClient.d.ts.map +1 -0
- package/dist/utils/IPFSClient.js +128 -0
- package/dist/utils/IPFSClient.js.map +1 -0
- package/dist/utils/NonceManager.d.ts +34 -0
- package/dist/utils/NonceManager.d.ts.map +1 -0
- package/dist/utils/NonceManager.js +114 -0
- package/dist/utils/NonceManager.js.map +1 -0
- package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
- package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
- package/dist/utils/ReceivedNonceTracker.js +196 -0
- package/dist/utils/ReceivedNonceTracker.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +4 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +21 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/computeTypeHash.d.ts +3 -0
- package/dist/utils/computeTypeHash.d.ts.map +1 -0
- package/dist/utils/computeTypeHash.js +30 -0
- package/dist/utils/computeTypeHash.js.map +1 -0
- package/dist/utils/validation.d.ts +6 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +46 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +73 -0
- package/src/ACTPClient.ts +276 -0
- package/src/__tests__/ProofGenerator.test.ts +124 -0
- package/src/__tests__/QuoteBuilder.test.ts +516 -0
- package/src/__tests__/StateMachine.test.ts +82 -0
- package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
- package/src/__tests__/integration/ACTPClient.test.ts +263 -0
- package/src/__tests__/integration.test.ts +289 -0
- package/src/__tests__/protocol/EASHelper.test.ts +472 -0
- package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
- package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
- package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
- package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
- package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
- package/src/__tests__/security/validation.security.test.ts +376 -0
- package/src/__tests__/utils/IPFSClient.test.ts +262 -0
- package/src/__tests__/utils/NonceManager.test.ts +205 -0
- package/src/__tests__/utils/canonicalJson.test.ts +153 -0
- package/src/abi/ACTPKernel.json +1340 -0
- package/src/abi/ERC20.json +40 -0
- package/src/abi/EscrowVault.json +66 -0
- package/src/builders/DeliveryProofBuilder.ts +326 -0
- package/src/builders/QuoteBuilder.ts +483 -0
- package/src/builders/index.ts +17 -0
- package/src/config/networks.ts +165 -0
- package/src/errors/index.ts +130 -0
- package/src/index.ts +108 -0
- package/src/protocol/ACTPKernel.ts +625 -0
- package/src/protocol/EASHelper.ts +197 -0
- package/src/protocol/EscrowVault.ts +237 -0
- package/src/protocol/EventMonitor.ts +161 -0
- package/src/protocol/MessageSigner.ts +336 -0
- package/src/protocol/ProofGenerator.ts +119 -0
- package/src/protocol/QuoteBuilder.ts +15 -0
- package/src/types/eip712.ts +175 -0
- package/src/types/escrow.ts +26 -0
- package/src/types/index.ts +10 -0
- package/src/types/message.ts +145 -0
- package/src/types/state.ts +77 -0
- package/src/types/transaction.ts +54 -0
- package/src/utils/IPFSClient.ts +248 -0
- package/src/utils/NonceManager.ts +293 -0
- package/src/utils/ReceivedNonceTracker.ts +397 -0
- package/src/utils/canonicalJson.ts +38 -0
- package/src/utils/computeTypeHash.ts +50 -0
- package/src/utils/validation.ts +82 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoteBuilder Tests
|
|
3
|
+
* Reference: AIP-2 §9 (Test Vectors), §10.2 (Validation Checklist)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Wallet, parseUnits, HDNodeWallet } from 'ethers';
|
|
7
|
+
import { QuoteBuilder, QuoteParams, QuoteMessage } from '../builders/QuoteBuilder';
|
|
8
|
+
import { NonceManager, InMemoryNonceManager } from '../utils/NonceManager';
|
|
9
|
+
import { IPFSClient } from '../utils/IPFSClient';
|
|
10
|
+
|
|
11
|
+
describe('QuoteBuilder', () => {
|
|
12
|
+
let quoteBuilder: QuoteBuilder;
|
|
13
|
+
let signer: HDNodeWallet;
|
|
14
|
+
let nonceManager: NonceManager;
|
|
15
|
+
let mockIPFS: jest.Mocked<IPFSClient>;
|
|
16
|
+
|
|
17
|
+
// Test constants
|
|
18
|
+
const TEST_CHAIN_ID = 84532; // Base Sepolia
|
|
19
|
+
const TEST_KERNEL_ADDRESS = '0x1234567890123456789012345678901234567890';
|
|
20
|
+
const TEST_TX_ID = '0x7d87c3b8e23a5c9d1f4e6b2a8c5d9e3f1a7b4c6d8e2f5a3b9c1d7e4f6a8b2c5d';
|
|
21
|
+
const PROVIDER_WALLET = Wallet.createRandom();
|
|
22
|
+
const CONSUMER_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678';
|
|
23
|
+
|
|
24
|
+
const PROVIDER_DID = `did:ethr:${TEST_CHAIN_ID}:${PROVIDER_WALLET.address}`;
|
|
25
|
+
const CONSUMER_DID = `did:ethr:${TEST_CHAIN_ID}:${CONSUMER_ADDRESS}`;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
signer = PROVIDER_WALLET;
|
|
29
|
+
nonceManager = new InMemoryNonceManager();
|
|
30
|
+
|
|
31
|
+
// Mock IPFS client
|
|
32
|
+
mockIPFS = {
|
|
33
|
+
add: jest.fn().mockResolvedValue('bafybeigtest123'),
|
|
34
|
+
pin: jest.fn().mockResolvedValue(undefined),
|
|
35
|
+
get: jest.fn(),
|
|
36
|
+
unpin: jest.fn()
|
|
37
|
+
} as any;
|
|
38
|
+
|
|
39
|
+
quoteBuilder = new QuoteBuilder(signer, nonceManager, mockIPFS);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('build()', () => {
|
|
43
|
+
it('should build valid quote with all required fields', async () => {
|
|
44
|
+
const params: QuoteParams = {
|
|
45
|
+
txId: TEST_TX_ID,
|
|
46
|
+
provider: PROVIDER_DID,
|
|
47
|
+
consumer: CONSUMER_DID,
|
|
48
|
+
quotedAmount: parseUnits('7.5', 6).toString(), // $7.50
|
|
49
|
+
originalAmount: parseUnits('5.0', 6).toString(), // $5.00
|
|
50
|
+
maxPrice: parseUnits('10.0', 6).toString(), // $10.00
|
|
51
|
+
chainId: TEST_CHAIN_ID,
|
|
52
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const quote = await quoteBuilder.build(params);
|
|
56
|
+
|
|
57
|
+
// Verify structure
|
|
58
|
+
expect(quote.type).toBe('agirails.quote.v1');
|
|
59
|
+
expect(quote.version).toBe('1.0.0');
|
|
60
|
+
expect(quote.txId).toBe(TEST_TX_ID);
|
|
61
|
+
expect(quote.provider).toBe(PROVIDER_DID);
|
|
62
|
+
expect(quote.consumer).toBe(CONSUMER_DID);
|
|
63
|
+
expect(quote.quotedAmount).toBe(params.quotedAmount);
|
|
64
|
+
expect(quote.originalAmount).toBe(params.originalAmount);
|
|
65
|
+
expect(quote.maxPrice).toBe(params.maxPrice);
|
|
66
|
+
expect(quote.currency).toBe('USDC');
|
|
67
|
+
expect(quote.decimals).toBe(6);
|
|
68
|
+
expect(quote.chainId).toBe(TEST_CHAIN_ID);
|
|
69
|
+
expect(quote.nonce).toBe(1);
|
|
70
|
+
expect(quote.signature).toMatch(/^0x[a-fA-F0-9]{130}$/);
|
|
71
|
+
|
|
72
|
+
// Verify timestamps
|
|
73
|
+
const now = Math.floor(Date.now() / 1000);
|
|
74
|
+
expect(quote.quotedAt).toBeGreaterThanOrEqual(now - 5);
|
|
75
|
+
expect(quote.quotedAt).toBeLessThanOrEqual(now + 5);
|
|
76
|
+
expect(quote.expiresAt).toBe(quote.quotedAt + 3600); // Default 1 hour
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should build quote with custom expiry', async () => {
|
|
80
|
+
const customExpiry = Math.floor(Date.now() / 1000) + 7200; // 2 hours
|
|
81
|
+
|
|
82
|
+
const params: QuoteParams = {
|
|
83
|
+
txId: TEST_TX_ID,
|
|
84
|
+
provider: PROVIDER_DID,
|
|
85
|
+
consumer: CONSUMER_DID,
|
|
86
|
+
quotedAmount: '7500000',
|
|
87
|
+
originalAmount: '5000000',
|
|
88
|
+
maxPrice: '10000000',
|
|
89
|
+
expiresAt: customExpiry,
|
|
90
|
+
chainId: TEST_CHAIN_ID,
|
|
91
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const quote = await quoteBuilder.build(params);
|
|
95
|
+
expect(quote.expiresAt).toBe(customExpiry);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should build quote with justification', async () => {
|
|
99
|
+
const params: QuoteParams = {
|
|
100
|
+
txId: TEST_TX_ID,
|
|
101
|
+
provider: PROVIDER_DID,
|
|
102
|
+
consumer: CONSUMER_DID,
|
|
103
|
+
quotedAmount: '7500000',
|
|
104
|
+
originalAmount: '5000000',
|
|
105
|
+
maxPrice: '10000000',
|
|
106
|
+
justification: {
|
|
107
|
+
reason: 'Dataset size requires additional compute',
|
|
108
|
+
estimatedTime: 300,
|
|
109
|
+
computeCost: 2.5,
|
|
110
|
+
breakdown: {
|
|
111
|
+
basePrice: 5.0,
|
|
112
|
+
additionalCompute: 2.5
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
chainId: TEST_CHAIN_ID,
|
|
116
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const quote = await quoteBuilder.build(params);
|
|
120
|
+
expect(quote.justification).toEqual(params.justification);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should increment nonce for subsequent quotes', async () => {
|
|
124
|
+
const params: QuoteParams = {
|
|
125
|
+
txId: TEST_TX_ID,
|
|
126
|
+
provider: PROVIDER_DID,
|
|
127
|
+
consumer: CONSUMER_DID,
|
|
128
|
+
quotedAmount: '7500000',
|
|
129
|
+
originalAmount: '5000000',
|
|
130
|
+
maxPrice: '10000000',
|
|
131
|
+
chainId: TEST_CHAIN_ID,
|
|
132
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const quote1 = await quoteBuilder.build(params);
|
|
136
|
+
const quote2 = await quoteBuilder.build({ ...params, txId: TEST_TX_ID.replace('5d', '6e') });
|
|
137
|
+
|
|
138
|
+
expect(quote1.nonce).toBe(1);
|
|
139
|
+
expect(quote2.nonce).toBe(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('build() - validation errors', () => {
|
|
144
|
+
it('should reject quote below originalAmount', async () => {
|
|
145
|
+
const params: QuoteParams = {
|
|
146
|
+
txId: TEST_TX_ID,
|
|
147
|
+
provider: PROVIDER_DID,
|
|
148
|
+
consumer: CONSUMER_DID,
|
|
149
|
+
quotedAmount: '4000000', // $4.00 (below $5.00 original)
|
|
150
|
+
originalAmount: '5000000',
|
|
151
|
+
maxPrice: '10000000',
|
|
152
|
+
chainId: TEST_CHAIN_ID,
|
|
153
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('quotedAmount must be >= originalAmount');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should reject quote above maxPrice', async () => {
|
|
160
|
+
const params: QuoteParams = {
|
|
161
|
+
txId: TEST_TX_ID,
|
|
162
|
+
provider: PROVIDER_DID,
|
|
163
|
+
consumer: CONSUMER_DID,
|
|
164
|
+
quotedAmount: '11000000', // $11.00 (above $10.00 max)
|
|
165
|
+
originalAmount: '5000000',
|
|
166
|
+
maxPrice: '10000000',
|
|
167
|
+
chainId: TEST_CHAIN_ID,
|
|
168
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('quotedAmount must be <= maxPrice');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should reject quote below platform minimum ($0.05)', async () => {
|
|
175
|
+
const params: QuoteParams = {
|
|
176
|
+
txId: TEST_TX_ID,
|
|
177
|
+
provider: PROVIDER_DID,
|
|
178
|
+
consumer: CONSUMER_DID,
|
|
179
|
+
quotedAmount: '40000', // $0.04 (below $0.05 minimum)
|
|
180
|
+
originalAmount: '30000',
|
|
181
|
+
maxPrice: '100000',
|
|
182
|
+
chainId: TEST_CHAIN_ID,
|
|
183
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('quotedAmount must be >= $0.05');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should reject expiry in the past', async () => {
|
|
190
|
+
const pastExpiry = Math.floor(Date.now() / 1000) - 3600;
|
|
191
|
+
|
|
192
|
+
const params: QuoteParams = {
|
|
193
|
+
txId: TEST_TX_ID,
|
|
194
|
+
provider: PROVIDER_DID,
|
|
195
|
+
consumer: CONSUMER_DID,
|
|
196
|
+
quotedAmount: '7500000',
|
|
197
|
+
originalAmount: '5000000',
|
|
198
|
+
maxPrice: '10000000',
|
|
199
|
+
expiresAt: pastExpiry,
|
|
200
|
+
chainId: TEST_CHAIN_ID,
|
|
201
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('expiresAt must be in the future');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should reject expiry beyond 24 hours', async () => {
|
|
208
|
+
const farFutureExpiry = Math.floor(Date.now() / 1000) + 86401; // 24h + 1 second
|
|
209
|
+
|
|
210
|
+
const params: QuoteParams = {
|
|
211
|
+
txId: TEST_TX_ID,
|
|
212
|
+
provider: PROVIDER_DID,
|
|
213
|
+
consumer: CONSUMER_DID,
|
|
214
|
+
quotedAmount: '7500000',
|
|
215
|
+
originalAmount: '5000000',
|
|
216
|
+
maxPrice: '10000000',
|
|
217
|
+
expiresAt: farFutureExpiry,
|
|
218
|
+
chainId: TEST_CHAIN_ID,
|
|
219
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('expiresAt cannot be more than 24 hours');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should reject invalid provider DID format', async () => {
|
|
226
|
+
const params: QuoteParams = {
|
|
227
|
+
txId: TEST_TX_ID,
|
|
228
|
+
provider: 'invalid-did',
|
|
229
|
+
consumer: CONSUMER_DID,
|
|
230
|
+
quotedAmount: '7500000',
|
|
231
|
+
originalAmount: '5000000',
|
|
232
|
+
maxPrice: '10000000',
|
|
233
|
+
chainId: TEST_CHAIN_ID,
|
|
234
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('provider must be valid did:ethr format');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should reject invalid transaction ID format', async () => {
|
|
241
|
+
const params: QuoteParams = {
|
|
242
|
+
txId: '0xinvalid',
|
|
243
|
+
provider: PROVIDER_DID,
|
|
244
|
+
consumer: CONSUMER_DID,
|
|
245
|
+
quotedAmount: '7500000',
|
|
246
|
+
originalAmount: '5000000',
|
|
247
|
+
maxPrice: '10000000',
|
|
248
|
+
chainId: TEST_CHAIN_ID,
|
|
249
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('txId must be valid bytes32 hex string');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should reject invalid chainId', async () => {
|
|
256
|
+
const params: QuoteParams = {
|
|
257
|
+
txId: TEST_TX_ID,
|
|
258
|
+
provider: PROVIDER_DID,
|
|
259
|
+
consumer: CONSUMER_DID,
|
|
260
|
+
quotedAmount: '7500000',
|
|
261
|
+
originalAmount: '5000000',
|
|
262
|
+
maxPrice: '10000000',
|
|
263
|
+
chainId: 1, // Ethereum mainnet (not supported)
|
|
264
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
await expect(quoteBuilder.build(params)).rejects.toThrow('chainId must be 84532 (Base Sepolia) or 8453 (Base Mainnet)');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('verify()', () => {
|
|
272
|
+
let validQuote: QuoteMessage;
|
|
273
|
+
|
|
274
|
+
beforeEach(async () => {
|
|
275
|
+
const params: QuoteParams = {
|
|
276
|
+
txId: TEST_TX_ID,
|
|
277
|
+
provider: PROVIDER_DID,
|
|
278
|
+
consumer: CONSUMER_DID,
|
|
279
|
+
quotedAmount: '7500000',
|
|
280
|
+
originalAmount: '5000000',
|
|
281
|
+
maxPrice: '10000000',
|
|
282
|
+
chainId: TEST_CHAIN_ID,
|
|
283
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
validQuote = await quoteBuilder.build(params);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should verify valid quote', async () => {
|
|
290
|
+
const result = await quoteBuilder.verify(validQuote, TEST_KERNEL_ADDRESS);
|
|
291
|
+
expect(result).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should reject quote with tampered quotedAmount', async () => {
|
|
295
|
+
const tamperedQuote = { ...validQuote, quotedAmount: '99999999' };
|
|
296
|
+
|
|
297
|
+
await expect(quoteBuilder.verify(tamperedQuote, TEST_KERNEL_ADDRESS)).rejects.toThrow('Invalid signature');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should reject quote with invalid signature', async () => {
|
|
301
|
+
const invalidQuote = {
|
|
302
|
+
...validQuote,
|
|
303
|
+
signature: '0x' + '0'.repeat(130)
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
await expect(quoteBuilder.verify(invalidQuote, TEST_KERNEL_ADDRESS)).rejects.toThrow('Signature verification failed');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should reject expired quote', async () => {
|
|
310
|
+
const expiredQuote = {
|
|
311
|
+
...validQuote,
|
|
312
|
+
expiresAt: Math.floor(Date.now() / 1000) - 3600 // 1 hour ago
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Need to rebuild with new expiry for valid signature
|
|
316
|
+
const params: QuoteParams = {
|
|
317
|
+
txId: TEST_TX_ID,
|
|
318
|
+
provider: PROVIDER_DID,
|
|
319
|
+
consumer: CONSUMER_DID,
|
|
320
|
+
quotedAmount: '7500000',
|
|
321
|
+
originalAmount: '5000000',
|
|
322
|
+
maxPrice: '10000000',
|
|
323
|
+
expiresAt: expiredQuote.expiresAt,
|
|
324
|
+
chainId: TEST_CHAIN_ID,
|
|
325
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Mock Date.now to make quote appear valid at build time
|
|
329
|
+
const realDateNow = Date.now;
|
|
330
|
+
Date.now = jest.fn(() => (expiredQuote.expiresAt - 1800) * 1000);
|
|
331
|
+
|
|
332
|
+
const expiredButSignedQuote = await quoteBuilder.build(params);
|
|
333
|
+
|
|
334
|
+
// Restore Date.now
|
|
335
|
+
Date.now = realDateNow;
|
|
336
|
+
|
|
337
|
+
await expect(quoteBuilder.verify(expiredButSignedQuote, TEST_KERNEL_ADDRESS)).rejects.toThrow('Quote expired');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('computeHash()', () => {
|
|
342
|
+
it('should compute deterministic hash', async () => {
|
|
343
|
+
const params: QuoteParams = {
|
|
344
|
+
txId: TEST_TX_ID,
|
|
345
|
+
provider: PROVIDER_DID,
|
|
346
|
+
consumer: CONSUMER_DID,
|
|
347
|
+
quotedAmount: '7500000',
|
|
348
|
+
originalAmount: '5000000',
|
|
349
|
+
maxPrice: '10000000',
|
|
350
|
+
chainId: TEST_CHAIN_ID,
|
|
351
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const quote = await quoteBuilder.build(params);
|
|
355
|
+
const hash1 = quoteBuilder.computeHash(quote);
|
|
356
|
+
const hash2 = quoteBuilder.computeHash(quote);
|
|
357
|
+
|
|
358
|
+
expect(hash1).toBe(hash2);
|
|
359
|
+
expect(hash1).toMatch(/^0x[a-fA-F0-9]{64}$/);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should produce different hashes for different quotes', async () => {
|
|
363
|
+
const params1: QuoteParams = {
|
|
364
|
+
txId: TEST_TX_ID,
|
|
365
|
+
provider: PROVIDER_DID,
|
|
366
|
+
consumer: CONSUMER_DID,
|
|
367
|
+
quotedAmount: '7500000',
|
|
368
|
+
originalAmount: '5000000',
|
|
369
|
+
maxPrice: '10000000',
|
|
370
|
+
chainId: TEST_CHAIN_ID,
|
|
371
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const params2 = { ...params1, quotedAmount: '8000000' };
|
|
375
|
+
|
|
376
|
+
const quote1 = await quoteBuilder.build(params1);
|
|
377
|
+
const quote2 = await quoteBuilder.build(params2);
|
|
378
|
+
|
|
379
|
+
const hash1 = quoteBuilder.computeHash(quote1);
|
|
380
|
+
const hash2 = quoteBuilder.computeHash(quote2);
|
|
381
|
+
|
|
382
|
+
expect(hash1).not.toBe(hash2);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('uploadToIPFS()', () => {
|
|
387
|
+
it('should upload quote to IPFS and return CID', async () => {
|
|
388
|
+
const params: QuoteParams = {
|
|
389
|
+
txId: TEST_TX_ID,
|
|
390
|
+
provider: PROVIDER_DID,
|
|
391
|
+
consumer: CONSUMER_DID,
|
|
392
|
+
quotedAmount: '7500000',
|
|
393
|
+
originalAmount: '5000000',
|
|
394
|
+
maxPrice: '10000000',
|
|
395
|
+
chainId: TEST_CHAIN_ID,
|
|
396
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const quote = await quoteBuilder.build(params);
|
|
400
|
+
const cid = await quoteBuilder.uploadToIPFS(quote);
|
|
401
|
+
|
|
402
|
+
expect(cid).toBe('bafybeigtest123');
|
|
403
|
+
expect(mockIPFS.add).toHaveBeenCalledWith(JSON.stringify(quote));
|
|
404
|
+
expect(mockIPFS.pin).toHaveBeenCalledWith(cid);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should throw error if IPFS client not configured', async () => {
|
|
408
|
+
const builderWithoutIPFS = new QuoteBuilder(signer, nonceManager);
|
|
409
|
+
|
|
410
|
+
const params: QuoteParams = {
|
|
411
|
+
txId: TEST_TX_ID,
|
|
412
|
+
provider: PROVIDER_DID,
|
|
413
|
+
consumer: CONSUMER_DID,
|
|
414
|
+
quotedAmount: '7500000',
|
|
415
|
+
originalAmount: '5000000',
|
|
416
|
+
maxPrice: '10000000',
|
|
417
|
+
chainId: TEST_CHAIN_ID,
|
|
418
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const quote = await builderWithoutIPFS.build(params);
|
|
422
|
+
|
|
423
|
+
await expect(builderWithoutIPFS.uploadToIPFS(quote)).rejects.toThrow('IPFS client not configured');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('Edge cases (AIP-2 §9.2)', () => {
|
|
428
|
+
it('should accept quote at maxPrice boundary', async () => {
|
|
429
|
+
const params: QuoteParams = {
|
|
430
|
+
txId: TEST_TX_ID,
|
|
431
|
+
provider: PROVIDER_DID,
|
|
432
|
+
consumer: CONSUMER_DID,
|
|
433
|
+
quotedAmount: '10000000', // Exactly at maxPrice
|
|
434
|
+
originalAmount: '5000000',
|
|
435
|
+
maxPrice: '10000000',
|
|
436
|
+
chainId: TEST_CHAIN_ID,
|
|
437
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const quote = await quoteBuilder.build(params);
|
|
441
|
+
expect(quote.quotedAmount).toBe('10000000');
|
|
442
|
+
|
|
443
|
+
const result = await quoteBuilder.verify(quote, TEST_KERNEL_ADDRESS);
|
|
444
|
+
expect(result).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should accept quote at platform minimum boundary ($0.05)', async () => {
|
|
448
|
+
const params: QuoteParams = {
|
|
449
|
+
txId: TEST_TX_ID,
|
|
450
|
+
provider: PROVIDER_DID,
|
|
451
|
+
consumer: CONSUMER_DID,
|
|
452
|
+
quotedAmount: '50000', // Exactly $0.05
|
|
453
|
+
originalAmount: '50000',
|
|
454
|
+
maxPrice: '100000',
|
|
455
|
+
chainId: TEST_CHAIN_ID,
|
|
456
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const quote = await quoteBuilder.build(params);
|
|
460
|
+
const result = await quoteBuilder.verify(quote, TEST_KERNEL_ADDRESS);
|
|
461
|
+
expect(result).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should accept quote expiring exactly at 24-hour limit', async () => {
|
|
465
|
+
const exactExpiry = Math.floor(Date.now() / 1000) + 86400;
|
|
466
|
+
|
|
467
|
+
const params: QuoteParams = {
|
|
468
|
+
txId: TEST_TX_ID,
|
|
469
|
+
provider: PROVIDER_DID,
|
|
470
|
+
consumer: CONSUMER_DID,
|
|
471
|
+
quotedAmount: '7500000',
|
|
472
|
+
originalAmount: '5000000',
|
|
473
|
+
maxPrice: '10000000',
|
|
474
|
+
expiresAt: exactExpiry,
|
|
475
|
+
chainId: TEST_CHAIN_ID,
|
|
476
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const quote = await quoteBuilder.build(params);
|
|
480
|
+
expect(quote.expiresAt).toBe(exactExpiry);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should handle quote with empty justification', async () => {
|
|
484
|
+
const params: QuoteParams = {
|
|
485
|
+
txId: TEST_TX_ID,
|
|
486
|
+
provider: PROVIDER_DID,
|
|
487
|
+
consumer: CONSUMER_DID,
|
|
488
|
+
quotedAmount: '7500000',
|
|
489
|
+
originalAmount: '5000000',
|
|
490
|
+
maxPrice: '10000000',
|
|
491
|
+
justification: {},
|
|
492
|
+
chainId: TEST_CHAIN_ID,
|
|
493
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const quote = await quoteBuilder.build(params);
|
|
497
|
+
expect(quote.justification).toEqual({});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should handle quote with undefined justification', async () => {
|
|
501
|
+
const params: QuoteParams = {
|
|
502
|
+
txId: TEST_TX_ID,
|
|
503
|
+
provider: PROVIDER_DID,
|
|
504
|
+
consumer: CONSUMER_DID,
|
|
505
|
+
quotedAmount: '7500000',
|
|
506
|
+
originalAmount: '5000000',
|
|
507
|
+
maxPrice: '10000000',
|
|
508
|
+
chainId: TEST_CHAIN_ID,
|
|
509
|
+
kernelAddress: TEST_KERNEL_ADDRESS
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const quote = await quoteBuilder.build(params);
|
|
513
|
+
expect(quote.justification).toBeUndefined();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { State, StateMachine } from '../types/state';
|
|
2
|
+
|
|
3
|
+
describe('StateMachine', () => {
|
|
4
|
+
describe('isValidTransition', () => {
|
|
5
|
+
it('should allow INITIATED → QUOTED', () => {
|
|
6
|
+
expect(StateMachine.isValidTransition(State.INITIATED, State.QUOTED)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should allow INITIATED → CANCELLED', () => {
|
|
10
|
+
expect(StateMachine.isValidTransition(State.INITIATED, State.CANCELLED)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should reject INITIATED → SETTLED', () => {
|
|
14
|
+
expect(StateMachine.isValidTransition(State.INITIATED, State.SETTLED)).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should allow DELIVERED → SETTLED', () => {
|
|
18
|
+
expect(StateMachine.isValidTransition(State.DELIVERED, State.SETTLED)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should allow DELIVERED → DISPUTED', () => {
|
|
22
|
+
expect(StateMachine.isValidTransition(State.DELIVERED, State.DISPUTED)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should reject SETTLED → any state (terminal)', () => {
|
|
26
|
+
expect(StateMachine.isValidTransition(State.SETTLED, State.INITIATED)).toBe(false);
|
|
27
|
+
expect(StateMachine.isValidTransition(State.SETTLED, State.DISPUTED)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('isTerminalState', () => {
|
|
32
|
+
it('should identify SETTLED as terminal', () => {
|
|
33
|
+
expect(StateMachine.isTerminalState(State.SETTLED)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should identify CANCELLED as terminal', () => {
|
|
37
|
+
expect(StateMachine.isTerminalState(State.CANCELLED)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should identify IN_PROGRESS as not terminal', () => {
|
|
41
|
+
expect(StateMachine.isTerminalState(State.IN_PROGRESS)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('getStateName', () => {
|
|
46
|
+
it('should return correct state names', () => {
|
|
47
|
+
expect(StateMachine.getStateName(State.INITIATED)).toBe('INITIATED');
|
|
48
|
+
expect(StateMachine.getStateName(State.SETTLED)).toBe('SETTLED');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('getNextValidStates', () => {
|
|
53
|
+
it('should return valid next states for INITIATED', () => {
|
|
54
|
+
const next = StateMachine.getNextValidStates(State.INITIATED);
|
|
55
|
+
expect(next).toContain(State.QUOTED);
|
|
56
|
+
expect(next).toContain(State.COMMITTED);
|
|
57
|
+
expect(next).toContain(State.CANCELLED);
|
|
58
|
+
expect(next).toHaveLength(3); // INITIATED can transition to QUOTED, COMMITTED, or CANCELLED
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return empty array for terminal states', () => {
|
|
62
|
+
expect(StateMachine.getNextValidStates(State.SETTLED)).toEqual([]);
|
|
63
|
+
expect(StateMachine.getNextValidStates(State.CANCELLED)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('validateTransition', () => {
|
|
68
|
+
it('should not throw for valid transitions', () => {
|
|
69
|
+
expect(() => {
|
|
70
|
+
StateMachine.validateTransition(State.INITIATED, State.QUOTED);
|
|
71
|
+
}).not.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should throw for invalid transitions', () => {
|
|
75
|
+
expect(() => {
|
|
76
|
+
StateMachine.validateTransition(State.INITIATED, State.SETTLED);
|
|
77
|
+
}).toThrow('Invalid state transition');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
|