@solana/keychain-para 0.0.0 → 0.6.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 +123 -0
- package/dist/__tests__/para-signer.integration.test.d.ts +2 -0
- package/dist/__tests__/para-signer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/para-signer.integration.test.js +17 -0
- package/dist/__tests__/para-signer.integration.test.js.map +1 -0
- package/dist/__tests__/para-signer.test.d.ts +2 -0
- package/dist/__tests__/para-signer.test.d.ts.map +1 -0
- package/dist/__tests__/para-signer.test.js +366 -0
- package/dist/__tests__/para-signer.test.js.map +1 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +19 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/para-signer.d.ts +66 -0
- package/dist/para-signer.d.ts.map +1 -0
- package/dist/para-signer.js +278 -0
- package/dist/para-signer.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -8
- package/src/__tests__/para-signer.integration.test.ts +17 -0
- package/src/__tests__/para-signer.test.ts +492 -0
- package/src/__tests__/setup.ts +24 -0
- package/src/index.ts +4 -0
- package/src/para-signer.ts +344 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { assertIsSolanaSigner } from '@solana/keychain-core';
|
|
4
|
+
|
|
5
|
+
vi.mock('@solana/keychain-core', async importOriginal => {
|
|
6
|
+
const mod = await importOriginal<typeof import('@solana/keychain-core')>();
|
|
7
|
+
return { ...mod, assertSignatureValid: vi.fn() };
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
import { ParaSigner } from '../para-signer.js';
|
|
11
|
+
|
|
12
|
+
// Mock fetch globally
|
|
13
|
+
global.fetch = vi.fn();
|
|
14
|
+
|
|
15
|
+
// Valid 64-byte Ed25519 signature as 128 hex chars
|
|
16
|
+
const MOCK_SIGNATURE = 'ab'.repeat(64);
|
|
17
|
+
|
|
18
|
+
const MOCK_ADDRESS = '11111111111111111111111111111111';
|
|
19
|
+
|
|
20
|
+
const mockConfig = {
|
|
21
|
+
apiKey: 'sk_test_api_key',
|
|
22
|
+
apiBaseUrl: 'https://api.test.getpara.com',
|
|
23
|
+
walletId: '00000000-0000-0000-0000-000000000000',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function mockWalletResponse(overrides?: Record<string, unknown>) {
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
address: MOCK_ADDRESS,
|
|
30
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
31
|
+
publicKey: MOCK_ADDRESS,
|
|
32
|
+
status: 'ready',
|
|
33
|
+
type: 'SOLANA',
|
|
34
|
+
...overrides,
|
|
35
|
+
}),
|
|
36
|
+
{ status: 200 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mockSignResponse(signature = MOCK_SIGNATURE) {
|
|
41
|
+
return new Response(JSON.stringify({ signature }), { status: 200 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('ParaSigner', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.resetAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('create', () => {
|
|
50
|
+
it('should create a signer by fetching wallet address', async () => {
|
|
51
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
52
|
+
|
|
53
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
54
|
+
|
|
55
|
+
expect(signer.address).toBe(MOCK_ADDRESS);
|
|
56
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
57
|
+
'https://api.test.getpara.com/v1/wallets/00000000-0000-0000-0000-000000000000',
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
headers: { 'X-API-Key': 'sk_test_api_key' },
|
|
60
|
+
method: 'GET',
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should satisfy the SolanaSigner interface', async () => {
|
|
66
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
67
|
+
|
|
68
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
69
|
+
|
|
70
|
+
assertIsSolanaSigner(signer);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should throw CONFIG_ERROR for missing apiKey', async () => {
|
|
74
|
+
await expect(ParaSigner.create({ ...mockConfig, apiKey: '' })).rejects.toMatchObject({
|
|
75
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
76
|
+
message: expect.stringContaining('Missing required configuration fields'),
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw CONFIG_ERROR for missing walletId', async () => {
|
|
81
|
+
await expect(ParaSigner.create({ ...mockConfig, walletId: '' })).rejects.toMatchObject({
|
|
82
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
83
|
+
message: expect.stringContaining('Missing required configuration fields'),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should throw CONFIG_ERROR for non-sk_ apiKey', async () => {
|
|
88
|
+
await expect(ParaSigner.create({ ...mockConfig, apiKey: 'pk_test_key' })).rejects.toMatchObject({
|
|
89
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
90
|
+
message: expect.stringContaining('apiKey must be a Para secret key'),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw CONFIG_ERROR for non-UUID walletId', async () => {
|
|
95
|
+
await expect(ParaSigner.create({ ...mockConfig, walletId: 'not-a-uuid' })).rejects.toMatchObject({
|
|
96
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
97
|
+
message: expect.stringContaining('walletId must be a valid UUID'),
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw CONFIG_ERROR for non-HTTPS apiBaseUrl', async () => {
|
|
102
|
+
await expect(
|
|
103
|
+
ParaSigner.create({ ...mockConfig, apiBaseUrl: 'http://api.getpara.com' }),
|
|
104
|
+
).rejects.toMatchObject({
|
|
105
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
106
|
+
message: expect.stringContaining('apiBaseUrl must use HTTPS'),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should throw CONFIG_ERROR for invalid apiBaseUrl', async () => {
|
|
111
|
+
await expect(ParaSigner.create({ ...mockConfig, apiBaseUrl: 'not-a-url' })).rejects.toMatchObject({
|
|
112
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
113
|
+
message: expect.stringContaining('apiBaseUrl is not a valid URL'),
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should throw CONFIG_ERROR for non-SOLANA wallet type', async () => {
|
|
118
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse({ type: 'EVM' }));
|
|
119
|
+
|
|
120
|
+
await expect(ParaSigner.create(mockConfig)).rejects.toMatchObject({
|
|
121
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
122
|
+
message: expect.stringContaining('Expected SOLANA wallet but got EVM'),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should throw REMOTE_API_ERROR when wallet has no address', async () => {
|
|
127
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse({ address: null }));
|
|
128
|
+
|
|
129
|
+
await expect(ParaSigner.create(mockConfig)).rejects.toMatchObject({
|
|
130
|
+
code: 'SIGNER_REMOTE_API_ERROR',
|
|
131
|
+
message: expect.stringContaining('does not have an address'),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should throw REMOTE_API_ERROR when API returns error', async () => {
|
|
136
|
+
vi.mocked(fetch).mockResolvedValueOnce(
|
|
137
|
+
new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await expect(ParaSigner.create(mockConfig)).rejects.toMatchObject({
|
|
141
|
+
code: 'SIGNER_REMOTE_API_ERROR',
|
|
142
|
+
message: expect.stringContaining('Failed to fetch wallet'),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should throw HTTP_ERROR when network fails', async () => {
|
|
147
|
+
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
|
|
148
|
+
|
|
149
|
+
await expect(ParaSigner.create(mockConfig)).rejects.toMatchObject({
|
|
150
|
+
code: 'SIGNER_HTTP_ERROR',
|
|
151
|
+
message: expect.stringContaining('Para network request failed'),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should remove trailing slash from apiBaseUrl', async () => {
|
|
156
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
157
|
+
|
|
158
|
+
await ParaSigner.create({ ...mockConfig, apiBaseUrl: 'https://api.test.getpara.com/' });
|
|
159
|
+
|
|
160
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
161
|
+
'https://api.test.getpara.com/v1/wallets/00000000-0000-0000-0000-000000000000',
|
|
162
|
+
expect.anything(),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should throw CONFIG_ERROR for negative requestDelayMs', async () => {
|
|
167
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
168
|
+
|
|
169
|
+
await expect(ParaSigner.create({ ...mockConfig, requestDelayMs: -1 })).rejects.toMatchObject({
|
|
170
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
171
|
+
message: expect.stringContaining('requestDelayMs must not be negative'),
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should warn for high requestDelayMs', async () => {
|
|
176
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
177
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
178
|
+
|
|
179
|
+
await ParaSigner.create({ ...mockConfig, requestDelayMs: 3001 });
|
|
180
|
+
|
|
181
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('requestDelayMs is greater than 3000ms'));
|
|
182
|
+
warnSpy.mockRestore();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('isAvailable', () => {
|
|
187
|
+
it('should return true when wallet is ready', async () => {
|
|
188
|
+
vi.mocked(fetch)
|
|
189
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
190
|
+
.mockResolvedValueOnce(mockWalletResponse({ status: 'ready' })); // isAvailable
|
|
191
|
+
|
|
192
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
193
|
+
const result = await signer.isAvailable();
|
|
194
|
+
|
|
195
|
+
expect(result).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return true when wallet status is ACTIVE', async () => {
|
|
199
|
+
vi.mocked(fetch)
|
|
200
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
201
|
+
.mockResolvedValueOnce(mockWalletResponse({ status: 'ACTIVE' })); // isAvailable
|
|
202
|
+
|
|
203
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
204
|
+
const result = await signer.isAvailable();
|
|
205
|
+
|
|
206
|
+
expect(result).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should return true when wallet status is mixed case', async () => {
|
|
210
|
+
vi.mocked(fetch)
|
|
211
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
212
|
+
.mockResolvedValueOnce(mockWalletResponse({ status: 'Ready' })); // isAvailable
|
|
213
|
+
|
|
214
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
215
|
+
const result = await signer.isAvailable();
|
|
216
|
+
|
|
217
|
+
expect(result).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return false when wallet is not ready', async () => {
|
|
221
|
+
vi.mocked(fetch)
|
|
222
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
223
|
+
.mockResolvedValueOnce(mockWalletResponse({ status: 'creating' })); // isAvailable
|
|
224
|
+
|
|
225
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
226
|
+
const result = await signer.isAvailable();
|
|
227
|
+
|
|
228
|
+
expect(result).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return false when wallet type is not SOLANA', async () => {
|
|
232
|
+
vi.mocked(fetch)
|
|
233
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
234
|
+
.mockResolvedValueOnce(mockWalletResponse({ type: 'EVM', status: 'ready' })); // isAvailable
|
|
235
|
+
|
|
236
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
237
|
+
const result = await signer.isAvailable();
|
|
238
|
+
|
|
239
|
+
expect(result).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return false when API returns error', async () => {
|
|
243
|
+
vi.mocked(fetch)
|
|
244
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
245
|
+
.mockResolvedValueOnce(new Response('', { status: 500 })); // isAvailable
|
|
246
|
+
|
|
247
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
248
|
+
const result = await signer.isAvailable();
|
|
249
|
+
|
|
250
|
+
expect(result).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should return false when network fails', async () => {
|
|
254
|
+
vi.mocked(fetch)
|
|
255
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
256
|
+
.mockRejectedValueOnce(new Error('Network error')); // isAvailable
|
|
257
|
+
|
|
258
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
259
|
+
const result = await signer.isAvailable();
|
|
260
|
+
|
|
261
|
+
expect(result).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('signMessages', () => {
|
|
266
|
+
it('should sign a message successfully', async () => {
|
|
267
|
+
vi.mocked(fetch)
|
|
268
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
269
|
+
.mockResolvedValueOnce(mockSignResponse()); // sign
|
|
270
|
+
|
|
271
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
272
|
+
const message = {
|
|
273
|
+
content: new Uint8Array([1, 2, 3, 4]),
|
|
274
|
+
signatures: {},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const [result] = await signer.signMessages([message]);
|
|
278
|
+
|
|
279
|
+
expect(result).toHaveProperty(MOCK_ADDRESS);
|
|
280
|
+
expect(fetch).toHaveBeenLastCalledWith(
|
|
281
|
+
'https://api.test.getpara.com/v1/wallets/00000000-0000-0000-0000-000000000000/sign-raw',
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
body: JSON.stringify({ data: '01020304', encoding: 'hex' }),
|
|
284
|
+
headers: {
|
|
285
|
+
'Content-Type': 'application/json',
|
|
286
|
+
'X-API-Key': 'sk_test_api_key',
|
|
287
|
+
},
|
|
288
|
+
method: 'POST',
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return empty array for empty input', async () => {
|
|
294
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
295
|
+
|
|
296
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
297
|
+
const result = await signer.signMessages([]);
|
|
298
|
+
|
|
299
|
+
expect(result).toEqual([]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should handle 0x-prefixed signatures', async () => {
|
|
303
|
+
vi.mocked(fetch)
|
|
304
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
305
|
+
.mockResolvedValueOnce(mockSignResponse('0x' + MOCK_SIGNATURE)); // sign
|
|
306
|
+
|
|
307
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
308
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
309
|
+
|
|
310
|
+
const [result] = await signer.signMessages([message]);
|
|
311
|
+
|
|
312
|
+
expect(result).toHaveProperty(MOCK_ADDRESS);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should throw REMOTE_API_ERROR on API error', async () => {
|
|
316
|
+
vi.mocked(fetch)
|
|
317
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
318
|
+
.mockResolvedValueOnce(new Response(JSON.stringify({ message: 'rate limited' }), { status: 429 }));
|
|
319
|
+
|
|
320
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
321
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
322
|
+
|
|
323
|
+
await expect(signer.signMessages([message])).rejects.toMatchObject({
|
|
324
|
+
code: 'SIGNER_REMOTE_API_ERROR',
|
|
325
|
+
message: expect.stringContaining('Para signing failed'),
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should throw HTTP_ERROR on network failure', async () => {
|
|
330
|
+
vi.mocked(fetch)
|
|
331
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
332
|
+
.mockRejectedValueOnce(new Error('Network failure'));
|
|
333
|
+
|
|
334
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
335
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
336
|
+
|
|
337
|
+
await expect(signer.signMessages([message])).rejects.toMatchObject({
|
|
338
|
+
code: 'SIGNER_HTTP_ERROR',
|
|
339
|
+
message: expect.stringContaining('Para network request failed'),
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should throw REMOTE_API_ERROR for missing signature', async () => {
|
|
344
|
+
vi.mocked(fetch)
|
|
345
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
346
|
+
.mockResolvedValueOnce(new Response(JSON.stringify({}), { status: 200 }));
|
|
347
|
+
|
|
348
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
349
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
350
|
+
|
|
351
|
+
await expect(signer.signMessages([message])).rejects.toMatchObject({
|
|
352
|
+
code: 'SIGNER_REMOTE_API_ERROR',
|
|
353
|
+
message: expect.stringContaining('Missing signature in Para response'),
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should throw PARSING_ERROR for invalid signature length', async () => {
|
|
358
|
+
vi.mocked(fetch)
|
|
359
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
360
|
+
.mockResolvedValueOnce(mockSignResponse('aabb')); // too short
|
|
361
|
+
|
|
362
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
363
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
364
|
+
|
|
365
|
+
await expect(signer.signMessages([message])).rejects.toMatchObject({
|
|
366
|
+
code: 'SIGNER_PARSING_ERROR',
|
|
367
|
+
message: expect.stringContaining('Invalid Ed25519 signature length'),
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should throw PARSING_ERROR for non-hex characters in signature', async () => {
|
|
372
|
+
vi.mocked(fetch)
|
|
373
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
374
|
+
.mockResolvedValueOnce(mockSignResponse('zz'.repeat(64))); // invalid hex
|
|
375
|
+
|
|
376
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
377
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
378
|
+
|
|
379
|
+
await expect(signer.signMessages([message])).rejects.toMatchObject({
|
|
380
|
+
code: 'SIGNER_PARSING_ERROR',
|
|
381
|
+
message: expect.stringContaining('Invalid hex characters'),
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should apply request delay for multiple messages', async () => {
|
|
386
|
+
vi.mocked(fetch)
|
|
387
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
388
|
+
.mockResolvedValueOnce(mockSignResponse()) // sign 1
|
|
389
|
+
.mockResolvedValueOnce(mockSignResponse()) // sign 2
|
|
390
|
+
.mockResolvedValueOnce(mockSignResponse()); // sign 3
|
|
391
|
+
|
|
392
|
+
const delaySpy = vi.spyOn(global, 'setTimeout');
|
|
393
|
+
const signer = await ParaSigner.create({ ...mockConfig, requestDelayMs: 100 });
|
|
394
|
+
|
|
395
|
+
const messages = [
|
|
396
|
+
{ content: new Uint8Array([1, 2, 3, 4]), signatures: {} },
|
|
397
|
+
{ content: new Uint8Array([5, 6, 7, 8]), signatures: {} },
|
|
398
|
+
{ content: new Uint8Array([9, 10, 11, 12]), signatures: {} },
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
await signer.signMessages(messages);
|
|
402
|
+
|
|
403
|
+
// First message has no delay, second has 100ms, third has 200ms
|
|
404
|
+
expect(delaySpy).toHaveBeenCalledTimes(2);
|
|
405
|
+
expect(delaySpy).toHaveBeenNthCalledWith(1, expect.any(Function), 100);
|
|
406
|
+
expect(delaySpy).toHaveBeenNthCalledWith(2, expect.any(Function), 200);
|
|
407
|
+
|
|
408
|
+
delaySpy.mockRestore();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('signTransactions', () => {
|
|
413
|
+
it('should sign a transaction successfully', async () => {
|
|
414
|
+
vi.mocked(fetch)
|
|
415
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
416
|
+
.mockResolvedValueOnce(mockSignResponse()); // sign
|
|
417
|
+
|
|
418
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
419
|
+
const mockTransaction = {
|
|
420
|
+
messageBytes: new Uint8Array([1, 2, 3, 4]),
|
|
421
|
+
signatures: {},
|
|
422
|
+
} as any;
|
|
423
|
+
|
|
424
|
+
const [result] = await signer.signTransactions([mockTransaction]);
|
|
425
|
+
|
|
426
|
+
expect(result).toHaveProperty(MOCK_ADDRESS);
|
|
427
|
+
expect(fetch).toHaveBeenLastCalledWith(
|
|
428
|
+
'https://api.test.getpara.com/v1/wallets/00000000-0000-0000-0000-000000000000/sign-raw',
|
|
429
|
+
expect.objectContaining({
|
|
430
|
+
body: JSON.stringify({ data: '01020304', encoding: 'hex' }),
|
|
431
|
+
method: 'POST',
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should return empty array for empty input', async () => {
|
|
437
|
+
vi.mocked(fetch).mockResolvedValueOnce(mockWalletResponse());
|
|
438
|
+
|
|
439
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
440
|
+
const result = await signer.signTransactions([]);
|
|
441
|
+
|
|
442
|
+
expect(result).toEqual([]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should sign multiple transactions', async () => {
|
|
446
|
+
vi.mocked(fetch)
|
|
447
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
448
|
+
.mockResolvedValueOnce(mockSignResponse()) // sign 1
|
|
449
|
+
.mockResolvedValueOnce(mockSignResponse()); // sign 2
|
|
450
|
+
|
|
451
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
452
|
+
const transactions = [
|
|
453
|
+
{ messageBytes: new Uint8Array([1, 2]), signatures: {} } as any,
|
|
454
|
+
{ messageBytes: new Uint8Array([3, 4]), signatures: {} } as any,
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
const results = await signer.signTransactions(transactions);
|
|
458
|
+
|
|
459
|
+
expect(results).toHaveLength(2);
|
|
460
|
+
expect(results[0]).toHaveProperty(MOCK_ADDRESS);
|
|
461
|
+
expect(results[1]).toHaveProperty(MOCK_ADDRESS);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should throw REMOTE_API_ERROR on API error', async () => {
|
|
465
|
+
vi.mocked(fetch)
|
|
466
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
467
|
+
.mockResolvedValueOnce(new Response(JSON.stringify({ message: 'rate limited' }), { status: 429 }));
|
|
468
|
+
|
|
469
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
470
|
+
const mockTransaction = { messageBytes: new Uint8Array([1, 2, 3, 4]), signatures: {} } as any;
|
|
471
|
+
|
|
472
|
+
await expect(signer.signTransactions([mockTransaction])).rejects.toMatchObject({
|
|
473
|
+
code: 'SIGNER_REMOTE_API_ERROR',
|
|
474
|
+
message: expect.stringContaining('Para signing failed'),
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should throw HTTP_ERROR on network failure', async () => {
|
|
479
|
+
vi.mocked(fetch)
|
|
480
|
+
.mockResolvedValueOnce(mockWalletResponse()) // create
|
|
481
|
+
.mockRejectedValueOnce(new Error('Network failure'));
|
|
482
|
+
|
|
483
|
+
const signer = await ParaSigner.create(mockConfig);
|
|
484
|
+
const mockTransaction = { messageBytes: new Uint8Array([1, 2, 3, 4]), signatures: {} } as any;
|
|
485
|
+
|
|
486
|
+
await expect(signer.signTransactions([mockTransaction])).rejects.toMatchObject({
|
|
487
|
+
code: 'SIGNER_HTTP_ERROR',
|
|
488
|
+
message: expect.stringContaining('Para network request failed'),
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SolanaSigner } from '@solana/keychain-core';
|
|
2
|
+
import { SignerTestConfig, TestScenario } from '@solana/keychain-test-utils';
|
|
3
|
+
import { createParaSigner } from '../para-signer';
|
|
4
|
+
|
|
5
|
+
const SIGNER_TYPE = 'para';
|
|
6
|
+
const REQUIRED_ENV_VARS = ['PARA_API_KEY', 'PARA_WALLET_ID'];
|
|
7
|
+
|
|
8
|
+
const CONFIG: SignerTestConfig<SolanaSigner> = {
|
|
9
|
+
signerType: SIGNER_TYPE,
|
|
10
|
+
requiredEnvVars: REQUIRED_ENV_VARS,
|
|
11
|
+
createSigner: () =>
|
|
12
|
+
createParaSigner({
|
|
13
|
+
apiKey: process.env.PARA_API_KEY!,
|
|
14
|
+
apiBaseUrl: process.env.PARA_API_BASE_URL,
|
|
15
|
+
walletId: process.env.PARA_WALLET_ID!,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function getConfig(scenarios: TestScenario[]): Promise<SignerTestConfig<SolanaSigner>> {
|
|
20
|
+
return {
|
|
21
|
+
...CONFIG,
|
|
22
|
+
testScenarios: scenarios,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ParaSigner, createParaSigner } from './para-signer.js';
|
|
2
|
+
export type { ParaSignerConfig } from './para-signer.js';
|
|
3
|
+
export type { ParaErrorResponse, ParaSignRawRequest, ParaSignRawResponse, ParaWalletResponse } from './types.js';
|
|
4
|
+
export { assertIsSolanaSigner, isSolanaSigner } from '@solana/keychain-core';
|