solana-kms-signer 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/LICENSE +21 -0
- package/README.md +610 -0
- package/dist/errors/index.d.ts +54 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +63 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/kms/client.d.ts +60 -0
- package/dist/kms/client.d.ts.map +1 -0
- package/dist/kms/client.js +108 -0
- package/dist/kms/client.js.map +1 -0
- package/dist/kms/signer.d.ts +170 -0
- package/dist/kms/signer.d.ts.map +1 -0
- package/dist/kms/signer.js +230 -0
- package/dist/kms/signer.js.map +1 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/publicKey.d.ts +28 -0
- package/dist/utils/publicKey.d.ts.map +1 -0
- package/dist/utils/publicKey.js +56 -0
- package/dist/utils/publicKey.js.map +1 -0
- package/package.json +73 -0
- package/src/errors/index.test.ts +173 -0
- package/src/errors/index.ts +61 -0
- package/src/index.ts +27 -0
- package/src/kms/client.test.ts +285 -0
- package/src/kms/client.ts +132 -0
- package/src/kms/signer.test.ts +446 -0
- package/src/kms/signer.ts +274 -0
- package/src/types/index.ts +44 -0
- package/src/utils/publicKey.test.ts +135 -0
- package/src/utils/publicKey.ts +70 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { vi, beforeEach, describe, it, expect, Mock } from 'vitest';
|
|
2
|
+
import nacl from 'tweetnacl';
|
|
3
|
+
import {
|
|
4
|
+
PublicKey,
|
|
5
|
+
Transaction,
|
|
6
|
+
VersionedTransaction,
|
|
7
|
+
SystemProgram,
|
|
8
|
+
TransactionMessage,
|
|
9
|
+
Blockhash,
|
|
10
|
+
} from '@solana/web3.js';
|
|
11
|
+
import { SignatureVerificationError, KmsClientError } from '../errors/index.js';
|
|
12
|
+
import type { KmsConfig } from '../types/index.js';
|
|
13
|
+
|
|
14
|
+
// Mock KmsClient before importing SolanaKmsSigner
|
|
15
|
+
const mockGetPublicKey = vi.fn();
|
|
16
|
+
const mockSign = vi.fn();
|
|
17
|
+
|
|
18
|
+
vi.mock('./client.js', () => ({
|
|
19
|
+
KmsClient: class {
|
|
20
|
+
getPublicKey = mockGetPublicKey;
|
|
21
|
+
sign = mockSign;
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Import after mocking
|
|
26
|
+
import { SolanaKmsSigner } from './signer.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Generate cryptographically valid test data using tweetnacl.
|
|
30
|
+
* This ensures our mock signatures can pass nacl.sign.detached.verify().
|
|
31
|
+
*/
|
|
32
|
+
const testKeyPair = nacl.sign.keyPair();
|
|
33
|
+
const TEST_PUBLIC_KEY = testKeyPair.publicKey; // 32 bytes
|
|
34
|
+
const TEST_SECRET_KEY = testKeyPair.secretKey; // 64 bytes
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create valid DER-encoded public key matching AWS KMS format.
|
|
38
|
+
* Uses actual test public key for signature verification.
|
|
39
|
+
*/
|
|
40
|
+
const createDerPublicKey = (publicKey: Uint8Array): Uint8Array => {
|
|
41
|
+
return new Uint8Array([
|
|
42
|
+
0x30, 0x2a, // SEQUENCE, 42 bytes
|
|
43
|
+
0x30, 0x05, // AlgorithmIdentifier SEQUENCE, 5 bytes
|
|
44
|
+
0x06, 0x03, 0x2b, 0x65, 0x70, // OID: 1.3.101.112 (Ed25519)
|
|
45
|
+
0x03, 0x21, 0x00, // BIT STRING, 33 bytes (32 + 1 padding)
|
|
46
|
+
...publicKey, // 32-byte ED25519 public key
|
|
47
|
+
]);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MOCK_DER_PUBLIC_KEY = createDerPublicKey(TEST_PUBLIC_KEY);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mock KMS configuration
|
|
54
|
+
*/
|
|
55
|
+
const MOCK_KMS_CONFIG: KmsConfig = {
|
|
56
|
+
region: 'us-east-1',
|
|
57
|
+
keyId: 'arn:aws:kms:us-east-1:123456789012:key/test-key-id',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create cryptographically valid signature for a message.
|
|
62
|
+
* Uses the test secret key to generate signatures that will pass verification.
|
|
63
|
+
*/
|
|
64
|
+
const createValidSignature = (message: Uint8Array): Uint8Array => {
|
|
65
|
+
return nacl.sign.detached(message, TEST_SECRET_KEY);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Mock recentBlockhash for transaction testing
|
|
70
|
+
*/
|
|
71
|
+
const MOCK_RECENT_BLOCKHASH: Blockhash = 'GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC';
|
|
72
|
+
|
|
73
|
+
describe('SolanaKmsSigner', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// Reset mock state before each test
|
|
76
|
+
mockGetPublicKey.mockReset();
|
|
77
|
+
mockSign.mockReset();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Happy Path', () => {
|
|
81
|
+
it('should successfully retrieve PublicKey object and convert to Base58', async () => {
|
|
82
|
+
// given: SolanaKmsSigner with mocked KmsClient
|
|
83
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
84
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
85
|
+
|
|
86
|
+
// when: Getting public key
|
|
87
|
+
const publicKey = await signer.getPublicKey();
|
|
88
|
+
|
|
89
|
+
// then: Should return valid PublicKey object
|
|
90
|
+
expect(publicKey).toBeInstanceOf(PublicKey);
|
|
91
|
+
expect(publicKey.toBytes()).toEqual(TEST_PUBLIC_KEY);
|
|
92
|
+
|
|
93
|
+
// then: Should be able to convert to Base58 format
|
|
94
|
+
const base58 = publicKey.toBase58();
|
|
95
|
+
expect(typeof base58).toBe('string');
|
|
96
|
+
expect(base58.length).toBeGreaterThan(0);
|
|
97
|
+
|
|
98
|
+
// then: Should have called KMS getPublicKey once
|
|
99
|
+
expect(mockGetPublicKey).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should cache public key and not call KMS API on second call', async () => {
|
|
103
|
+
// given: SolanaKmsSigner with mocked KmsClient
|
|
104
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
105
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
106
|
+
|
|
107
|
+
// when: Getting public key twice
|
|
108
|
+
const publicKey1 = await signer.getPublicKey();
|
|
109
|
+
const publicKey2 = await signer.getPublicKey();
|
|
110
|
+
|
|
111
|
+
// then: Should return same PublicKey instance
|
|
112
|
+
expect(publicKey1).toBe(publicKey2);
|
|
113
|
+
|
|
114
|
+
// then: KMS API should be called only once (cached after first call)
|
|
115
|
+
expect(mockGetPublicKey).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should successfully retrieve raw 32-byte public key', async () => {
|
|
119
|
+
// given: SolanaKmsSigner with mocked KmsClient
|
|
120
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
121
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
122
|
+
|
|
123
|
+
// when: Getting raw public key
|
|
124
|
+
const rawPublicKey = await signer.getRawPublicKey();
|
|
125
|
+
|
|
126
|
+
// then: Should return 32-byte Uint8Array
|
|
127
|
+
expect(rawPublicKey).toBeInstanceOf(Uint8Array);
|
|
128
|
+
expect(rawPublicKey.length).toBe(32);
|
|
129
|
+
expect(rawPublicKey).toEqual(TEST_PUBLIC_KEY);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should successfully sign message and return 64-byte signature', async () => {
|
|
133
|
+
// given: SolanaKmsSigner and message to sign
|
|
134
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
135
|
+
const message = new TextEncoder().encode('Hello, Solana!');
|
|
136
|
+
const validSignature = createValidSignature(message);
|
|
137
|
+
mockSign.mockResolvedValueOnce(validSignature);
|
|
138
|
+
|
|
139
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
140
|
+
|
|
141
|
+
// when: Signing message
|
|
142
|
+
const signature = await signer.signMessage(message);
|
|
143
|
+
|
|
144
|
+
// then: Should return 64-byte signature
|
|
145
|
+
expect(signature).toBeInstanceOf(Uint8Array);
|
|
146
|
+
expect(signature.length).toBe(64);
|
|
147
|
+
|
|
148
|
+
// then: Signature should be cryptographically valid
|
|
149
|
+
const isValid = nacl.sign.detached.verify(message, signature, TEST_PUBLIC_KEY);
|
|
150
|
+
expect(isValid).toBe(true);
|
|
151
|
+
|
|
152
|
+
// then: Should have called KMS sign
|
|
153
|
+
expect(mockSign).toHaveBeenCalledWith(message);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should successfully sign Transaction and validate signatures array', async () => {
|
|
157
|
+
// given: SolanaKmsSigner and valid Transaction
|
|
158
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
159
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
160
|
+
|
|
161
|
+
// Create valid transaction
|
|
162
|
+
const publicKey = await signer.getPublicKey();
|
|
163
|
+
const transaction = new Transaction().add(
|
|
164
|
+
SystemProgram.transfer({
|
|
165
|
+
fromPubkey: publicKey,
|
|
166
|
+
toPubkey: publicKey,
|
|
167
|
+
lamports: 1000,
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
transaction.recentBlockhash = MOCK_RECENT_BLOCKHASH;
|
|
171
|
+
transaction.feePayer = publicKey;
|
|
172
|
+
|
|
173
|
+
// Mock signature generation
|
|
174
|
+
const messageBytes = transaction.serializeMessage();
|
|
175
|
+
const validSignature = createValidSignature(messageBytes);
|
|
176
|
+
mockSign.mockResolvedValueOnce(validSignature);
|
|
177
|
+
|
|
178
|
+
// when: Signing transaction
|
|
179
|
+
const signedTx = await signer.signTransaction(transaction);
|
|
180
|
+
|
|
181
|
+
// then: Should return signed Transaction
|
|
182
|
+
expect(signedTx).toBeInstanceOf(Transaction);
|
|
183
|
+
|
|
184
|
+
// then: Signatures array should contain valid signature
|
|
185
|
+
expect(signedTx.signatures).toHaveLength(1);
|
|
186
|
+
expect(signedTx.signatures[0].publicKey.equals(publicKey)).toBe(true);
|
|
187
|
+
expect(signedTx.signatures[0].signature).not.toBeNull();
|
|
188
|
+
expect(signedTx.signatures[0].signature?.length).toBe(64);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should successfully sign VersionedTransaction', async () => {
|
|
192
|
+
// given: SolanaKmsSigner and valid VersionedTransaction
|
|
193
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
194
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
195
|
+
|
|
196
|
+
// Create valid versioned transaction
|
|
197
|
+
const publicKey = await signer.getPublicKey();
|
|
198
|
+
const instruction = SystemProgram.transfer({
|
|
199
|
+
fromPubkey: publicKey,
|
|
200
|
+
toPubkey: publicKey,
|
|
201
|
+
lamports: 1000,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const message = new TransactionMessage({
|
|
205
|
+
payerKey: publicKey,
|
|
206
|
+
recentBlockhash: MOCK_RECENT_BLOCKHASH,
|
|
207
|
+
instructions: [instruction],
|
|
208
|
+
}).compileToV0Message();
|
|
209
|
+
|
|
210
|
+
const transaction = new VersionedTransaction(message);
|
|
211
|
+
|
|
212
|
+
// Mock signature generation
|
|
213
|
+
const messageBytes = transaction.message.serialize();
|
|
214
|
+
const validSignature = createValidSignature(messageBytes);
|
|
215
|
+
mockSign.mockResolvedValueOnce(validSignature);
|
|
216
|
+
|
|
217
|
+
// when: Signing versioned transaction
|
|
218
|
+
const signedTx = await signer.signVersionedTransaction(transaction);
|
|
219
|
+
|
|
220
|
+
// then: Should return signed VersionedTransaction
|
|
221
|
+
expect(signedTx).toBeInstanceOf(VersionedTransaction);
|
|
222
|
+
|
|
223
|
+
// then: Signatures array should contain valid signature
|
|
224
|
+
expect(signedTx.signatures).toHaveLength(1);
|
|
225
|
+
expect(signedTx.signatures[0].length).toBe(64);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should successfully sign all transactions using Promise.all', async () => {
|
|
229
|
+
// given: SolanaKmsSigner and multiple transactions
|
|
230
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
231
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
232
|
+
|
|
233
|
+
// Create multiple valid transactions
|
|
234
|
+
const publicKey = await signer.getPublicKey();
|
|
235
|
+
const transactions = [1, 2, 3].map(() => {
|
|
236
|
+
const tx = new Transaction().add(
|
|
237
|
+
SystemProgram.transfer({
|
|
238
|
+
fromPubkey: publicKey,
|
|
239
|
+
toPubkey: publicKey,
|
|
240
|
+
lamports: 1000,
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
tx.recentBlockhash = MOCK_RECENT_BLOCKHASH;
|
|
244
|
+
tx.feePayer = publicKey;
|
|
245
|
+
return tx;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Mock signature generation for all transactions
|
|
249
|
+
transactions.forEach((tx) => {
|
|
250
|
+
const messageBytes = tx.serializeMessage();
|
|
251
|
+
const validSignature = createValidSignature(messageBytes);
|
|
252
|
+
mockSign.mockResolvedValueOnce(validSignature);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// when: Signing all transactions
|
|
256
|
+
const signedTxs = await signer.signAllTransactions(transactions);
|
|
257
|
+
|
|
258
|
+
// then: Should return array of signed transactions
|
|
259
|
+
expect(signedTxs).toHaveLength(3);
|
|
260
|
+
signedTxs.forEach((signedTx) => {
|
|
261
|
+
expect(signedTx).toBeInstanceOf(Transaction);
|
|
262
|
+
expect(signedTx.signatures).toHaveLength(1);
|
|
263
|
+
expect(signedTx.signatures[0].signature).not.toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// then: Should have called sign for each transaction
|
|
267
|
+
expect(mockSign).toHaveBeenCalledTimes(3);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('Failure Paths', () => {
|
|
272
|
+
it('should throw SignatureVerificationError when signature is invalid', async () => {
|
|
273
|
+
// given: SolanaKmsSigner and invalid signature from KMS
|
|
274
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
275
|
+
const message = new TextEncoder().encode('Hello, Solana!');
|
|
276
|
+
|
|
277
|
+
// Create invalid signature (random bytes that won't verify)
|
|
278
|
+
const invalidSignature = new Uint8Array(64).fill(0xff);
|
|
279
|
+
mockSign.mockResolvedValueOnce(invalidSignature);
|
|
280
|
+
|
|
281
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
282
|
+
|
|
283
|
+
// when: Signing message with invalid signature
|
|
284
|
+
const signPromise = signer.signMessage(message);
|
|
285
|
+
|
|
286
|
+
// then: Should throw SignatureVerificationError
|
|
287
|
+
await expect(signPromise).rejects.toThrow(SignatureVerificationError);
|
|
288
|
+
await expect(signPromise).rejects.toThrow('Signature verification failed');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should throw error when signature length is not 64 bytes', async () => {
|
|
292
|
+
// given: SolanaKmsSigner and wrong length signature from KMS
|
|
293
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
294
|
+
const message = new TextEncoder().encode('Hello, Solana!');
|
|
295
|
+
|
|
296
|
+
// Create wrong length signature (32 bytes instead of 64)
|
|
297
|
+
const wrongLengthSignature = new Uint8Array(32).fill(0xaa);
|
|
298
|
+
mockSign.mockResolvedValueOnce(wrongLengthSignature);
|
|
299
|
+
|
|
300
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
301
|
+
|
|
302
|
+
// when: Signing message with wrong length signature
|
|
303
|
+
const signPromise = signer.signMessage(message);
|
|
304
|
+
|
|
305
|
+
// then: Should throw error (nacl throws "bad signature size" Error)
|
|
306
|
+
await expect(signPromise).rejects.toThrow('bad signature size');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should throw error when signTransaction receives transaction without recentBlockhash', async () => {
|
|
310
|
+
// given: SolanaKmsSigner and transaction without recentBlockhash
|
|
311
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
312
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
313
|
+
|
|
314
|
+
const publicKey = await signer.getPublicKey();
|
|
315
|
+
const transaction = new Transaction().add(
|
|
316
|
+
SystemProgram.transfer({
|
|
317
|
+
fromPubkey: publicKey,
|
|
318
|
+
toPubkey: publicKey,
|
|
319
|
+
lamports: 1000,
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
// Note: not setting recentBlockhash or feePayer
|
|
323
|
+
|
|
324
|
+
// when: Attempting to sign invalid transaction
|
|
325
|
+
const signPromise = signer.signTransaction(transaction);
|
|
326
|
+
|
|
327
|
+
// then: Should throw error during serializeMessage()
|
|
328
|
+
await expect(signPromise).rejects.toThrow();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should propagate error when signTransaction message serialization fails', async () => {
|
|
332
|
+
// given: SolanaKmsSigner and transaction that will fail serialization
|
|
333
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
334
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
335
|
+
|
|
336
|
+
const publicKey = await signer.getPublicKey();
|
|
337
|
+
const transaction = new Transaction();
|
|
338
|
+
transaction.recentBlockhash = MOCK_RECENT_BLOCKHASH;
|
|
339
|
+
transaction.feePayer = publicKey;
|
|
340
|
+
// Empty transaction with no instructions
|
|
341
|
+
|
|
342
|
+
// Mock sign to throw error
|
|
343
|
+
mockSign.mockRejectedValueOnce(new Error('Message serialization error'));
|
|
344
|
+
|
|
345
|
+
// when: Attempting to sign with serialization error
|
|
346
|
+
const signPromise = signer.signTransaction(transaction);
|
|
347
|
+
|
|
348
|
+
// then: Error should propagate to caller
|
|
349
|
+
await expect(signPromise).rejects.toThrow();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should propagate KmsClientError when KMS getPublicKey fails', async () => {
|
|
353
|
+
// given: SolanaKmsSigner with KMS that fails getPublicKey
|
|
354
|
+
const kmsError = new KmsClientError('Failed to get public key from KMS');
|
|
355
|
+
mockGetPublicKey.mockRejectedValueOnce(kmsError);
|
|
356
|
+
|
|
357
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
358
|
+
|
|
359
|
+
// when: Getting public key from failing KMS
|
|
360
|
+
const getPublicKeyPromise = signer.getPublicKey();
|
|
361
|
+
|
|
362
|
+
// then: KmsClientError should propagate to caller
|
|
363
|
+
await expect(getPublicKeyPromise).rejects.toThrow(KmsClientError);
|
|
364
|
+
await expect(getPublicKeyPromise).rejects.toThrow('Failed to get public key from KMS');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should propagate KmsClientError when KMS sign fails', async () => {
|
|
368
|
+
// given: SolanaKmsSigner with KMS that fails sign
|
|
369
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
370
|
+
const kmsError = new KmsClientError('Failed to sign message with KMS');
|
|
371
|
+
mockSign.mockRejectedValueOnce(kmsError);
|
|
372
|
+
|
|
373
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
374
|
+
const message = new TextEncoder().encode('Hello, Solana!');
|
|
375
|
+
|
|
376
|
+
// when: Signing message with failing KMS
|
|
377
|
+
const signPromise = signer.signMessage(message);
|
|
378
|
+
|
|
379
|
+
// then: KmsClientError should propagate to caller
|
|
380
|
+
await expect(signPromise).rejects.toThrow(KmsClientError);
|
|
381
|
+
await expect(signPromise).rejects.toThrow('Failed to sign message with KMS');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should successfully create SolanaKmsSigner with KmsConfig', () => {
|
|
385
|
+
// given: Valid KmsConfig
|
|
386
|
+
const config: KmsConfig = MOCK_KMS_CONFIG;
|
|
387
|
+
|
|
388
|
+
// when: Creating signer with KmsConfig
|
|
389
|
+
const signer = new SolanaKmsSigner(config);
|
|
390
|
+
|
|
391
|
+
// then: Should create signer successfully
|
|
392
|
+
expect(signer).toBeInstanceOf(SolanaKmsSigner);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should return empty array when signAllTransactions receives empty array', async () => {
|
|
396
|
+
// given: SolanaKmsSigner and empty transaction array
|
|
397
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
398
|
+
|
|
399
|
+
// when: Signing empty array
|
|
400
|
+
const signedTxs = await signer.signAllTransactions([]);
|
|
401
|
+
|
|
402
|
+
// then: Should return empty array
|
|
403
|
+
expect(signedTxs).toEqual([]);
|
|
404
|
+
expect(signedTxs).toHaveLength(0);
|
|
405
|
+
|
|
406
|
+
// then: Should not call any KMS methods
|
|
407
|
+
expect(mockSign).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should reject Promise.all when one transaction fails in signAllTransactions', async () => {
|
|
411
|
+
// given: SolanaKmsSigner and multiple transactions where one will fail
|
|
412
|
+
mockGetPublicKey.mockResolvedValueOnce(MOCK_DER_PUBLIC_KEY);
|
|
413
|
+
const signer = new SolanaKmsSigner(MOCK_KMS_CONFIG);
|
|
414
|
+
|
|
415
|
+
const publicKey = await signer.getPublicKey();
|
|
416
|
+
const transactions = [1, 2, 3].map(() => {
|
|
417
|
+
const tx = new Transaction().add(
|
|
418
|
+
SystemProgram.transfer({
|
|
419
|
+
fromPubkey: publicKey,
|
|
420
|
+
toPubkey: publicKey,
|
|
421
|
+
lamports: 1000,
|
|
422
|
+
})
|
|
423
|
+
);
|
|
424
|
+
tx.recentBlockhash = MOCK_RECENT_BLOCKHASH;
|
|
425
|
+
tx.feePayer = publicKey;
|
|
426
|
+
return tx;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Mock first signature to succeed, second to fail
|
|
430
|
+
const firstMessage = transactions[0].serializeMessage();
|
|
431
|
+
const validSignature = createValidSignature(firstMessage);
|
|
432
|
+
mockSign.mockResolvedValueOnce(validSignature);
|
|
433
|
+
|
|
434
|
+
// Second call fails
|
|
435
|
+
const signError = new KmsClientError('KMS service unavailable');
|
|
436
|
+
mockSign.mockRejectedValueOnce(signError);
|
|
437
|
+
|
|
438
|
+
// when: Signing all transactions with one failure
|
|
439
|
+
const signAllPromise = signer.signAllTransactions(transactions);
|
|
440
|
+
|
|
441
|
+
// then: Promise.all should reject with the first error
|
|
442
|
+
await expect(signAllPromise).rejects.toThrow(KmsClientError);
|
|
443
|
+
await expect(signAllPromise).rejects.toThrow('KMS service unavailable');
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
});
|