@solana/keychain-cdp 0.0.0 → 1.0.1
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 +100 -0
- package/dist/__tests__/cdp-signer.integration.test.d.ts +2 -0
- package/dist/__tests__/cdp-signer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/cdp-signer.integration.test.js +17 -0
- package/dist/__tests__/cdp-signer.integration.test.js.map +1 -0
- package/dist/__tests__/cdp-signer.test.d.ts +2 -0
- package/dist/__tests__/cdp-signer.test.d.ts.map +1 -0
- package/dist/__tests__/cdp-signer.test.js +269 -0
- package/dist/__tests__/cdp-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 +20 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/cdp-signer.d.ts +85 -0
- package/dist/cdp-signer.d.ts.map +1 -0
- package/dist/cdp-signer.js +477 -0
- package/dist/cdp-signer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +52 -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 +65 -8
- package/src/__tests__/cdp-signer.integration.test.ts +19 -0
- package/src/__tests__/cdp-signer.test.ts +377 -0
- package/src/__tests__/setup.ts +26 -0
- package/src/cdp-signer.ts +574 -0
- package/src/index.ts +2 -0
- package/src/types.ts +59 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runSignerIntegrationTest } from '@solana/keychain-test-utils';
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { describe, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { getConfig } from './setup.js';
|
|
6
|
+
|
|
7
|
+
config();
|
|
8
|
+
|
|
9
|
+
describe('CdpSigner Integration', () => {
|
|
10
|
+
it.skipIf(!process.env.CDP_API_KEY_ID)('signs transactions with real API', async () => {
|
|
11
|
+
await runSignerIntegrationTest(await getConfig(['signTransaction']));
|
|
12
|
+
});
|
|
13
|
+
it.skipIf(!process.env.CDP_API_KEY_ID)('signs messages with real API', async () => {
|
|
14
|
+
await runSignerIntegrationTest(await getConfig(['signMessage']));
|
|
15
|
+
});
|
|
16
|
+
it.skipIf(!process.env.CDP_API_KEY_ID)('simulates transactions with real API', async () => {
|
|
17
|
+
await runSignerIntegrationTest(await getConfig(['simulateTransaction']));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as nodeCrypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { Address } from '@solana/addresses';
|
|
4
|
+
import { assertIsSolanaSigner } from '@solana/keychain-core';
|
|
5
|
+
import { generateKeyPairSigner } from '@solana/signers';
|
|
6
|
+
import {
|
|
7
|
+
type Base64EncodedWireTransaction,
|
|
8
|
+
type Transaction,
|
|
9
|
+
type TransactionWithinSizeLimit,
|
|
10
|
+
type TransactionWithLifetime,
|
|
11
|
+
} from '@solana/transactions';
|
|
12
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
|
|
14
|
+
vi.mock('@solana/keychain-core', async importOriginal => {
|
|
15
|
+
const mod = await importOriginal<typeof import('@solana/keychain-core')>();
|
|
16
|
+
return {
|
|
17
|
+
...mod,
|
|
18
|
+
assertSignatureValid: vi.fn(),
|
|
19
|
+
sanitizeRemoteErrorResponse:
|
|
20
|
+
mod.sanitizeRemoteErrorResponse ??
|
|
21
|
+
((text: string) =>
|
|
22
|
+
`${text
|
|
23
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ' ')
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim()
|
|
26
|
+
.slice(0, 256)} [truncated]`),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
import { CdpSigner } from '../cdp-signer.js';
|
|
31
|
+
import type { CdpSignerConfig } from '../types.js';
|
|
32
|
+
|
|
33
|
+
// --- Valid test credentials ---
|
|
34
|
+
|
|
35
|
+
// Generate a real Ed25519 keypair so that createKeyPairFromBytes seed↔pubkey validation passes.
|
|
36
|
+
// Ed25519 PKCS#8 DER: 16-byte header + 32-byte seed
|
|
37
|
+
// Ed25519 SPKI DER: 12-byte header + 32-byte public key
|
|
38
|
+
function generateTestApiKeySecret(): string {
|
|
39
|
+
const { privateKey, publicKey } = nodeCrypto.generateKeyPairSync('ed25519');
|
|
40
|
+
const pkcs8 = privateKey.export({ format: 'der', type: 'pkcs8' }) as Buffer;
|
|
41
|
+
const seed = pkcs8.subarray(16, 48);
|
|
42
|
+
const spki = publicKey.export({ format: 'der', type: 'spki' }) as Buffer;
|
|
43
|
+
const pubKeyBytes = spki.subarray(12, 44);
|
|
44
|
+
return Buffer.concat([seed, pubKeyBytes]).toString('base64');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const TEST_CDP_API_KEY_SECRET = generateTestApiKeySecret();
|
|
48
|
+
|
|
49
|
+
// P-256 PKCS#8 DER (67 bytes)
|
|
50
|
+
const TEST_CDP_WALLET_SECRET = Buffer.from([
|
|
51
|
+
// outer SEQUENCE (65 bytes)
|
|
52
|
+
0x30, 0x41,
|
|
53
|
+
// version INTEGER 0
|
|
54
|
+
0x02, 0x01, 0x00,
|
|
55
|
+
// AlgorithmIdentifier SEQUENCE (19 bytes)
|
|
56
|
+
0x30, 0x13,
|
|
57
|
+
// OID ecPublicKey (1.2.840.10045.2.1)
|
|
58
|
+
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
|
|
59
|
+
// OID prime256v1 (1.2.840.10045.3.1.7)
|
|
60
|
+
0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
|
|
61
|
+
// privateKey OCTET STRING (39 bytes)
|
|
62
|
+
0x04, 0x27,
|
|
63
|
+
// ECPrivateKey SEQUENCE (37 bytes)
|
|
64
|
+
0x30, 0x25,
|
|
65
|
+
// version INTEGER 1
|
|
66
|
+
0x02, 0x01, 0x01,
|
|
67
|
+
// privateKey OCTET STRING (32 bytes) — scalar 0x01...01 is in [1, n-1] for P-256
|
|
68
|
+
0x04, 0x20, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
|
69
|
+
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
|
70
|
+
]).toString('base64');
|
|
71
|
+
|
|
72
|
+
// Mock global fetch
|
|
73
|
+
const mockFetch = vi.fn();
|
|
74
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
75
|
+
|
|
76
|
+
// Mock wire transaction (same real-structure tx used across keychain tests)
|
|
77
|
+
const MOCK_B64_WIRE_TX =
|
|
78
|
+
'Af1fCRSrZ9ASprap8D3ZLPsbzeCs6uihvj/jfjm3UrAY72by5zKMRd7YAIbJCl9gyRHQbw+xdklET2ZNmZi3iA2AAQABAurnRuGN5bfL2osZZMdGlvL1qz8k0GbdLhiP1fICgkmsBUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI1NhzEgE0w/YfwaeZi2Ns/mLoZvq2Sx5NVQg7Am7wrjGwEBAAxIZWxsbywgUHJpdnkA' as Base64EncodedWireTransaction;
|
|
79
|
+
|
|
80
|
+
vi.mock('@solana/transactions', async () => {
|
|
81
|
+
const actual = await vi.importActual<typeof import('@solana/transactions')>('@solana/transactions');
|
|
82
|
+
return {
|
|
83
|
+
...actual,
|
|
84
|
+
getBase64EncodedWireTransaction: vi.fn(() => MOCK_B64_WIRE_TX),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const createMockTransaction = (): Transaction & TransactionWithinSizeLimit & TransactionWithLifetime => {
|
|
89
|
+
return {} as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// A valid base58 Solana address for tests
|
|
93
|
+
const TEST_ADDRESS = '7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV';
|
|
94
|
+
|
|
95
|
+
function makeConfig(overrides?: Partial<CdpSignerConfig>): CdpSignerConfig {
|
|
96
|
+
return {
|
|
97
|
+
cdpApiKeyId: 'test-api-key-name',
|
|
98
|
+
cdpApiKeySecret: TEST_CDP_API_KEY_SECRET,
|
|
99
|
+
cdpWalletSecret: TEST_CDP_WALLET_SECRET,
|
|
100
|
+
address: TEST_ADDRESS,
|
|
101
|
+
...overrides,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('CdpSigner', () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
vi.resetAllMocks();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('create()', () => {
|
|
111
|
+
it('creates a CdpSigner with valid config', async () => {
|
|
112
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
113
|
+
|
|
114
|
+
expect(signer.address).toBe(TEST_ADDRESS);
|
|
115
|
+
assertIsSolanaSigner(signer);
|
|
116
|
+
expect(signer.signMessages).toBeDefined();
|
|
117
|
+
expect(signer.signTransactions).toBeDefined();
|
|
118
|
+
expect(signer.isAvailable).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws CONFIG_ERROR for missing cdpApiKeyId', async () => {
|
|
122
|
+
await expect(CdpSigner.create(makeConfig({ cdpApiKeyId: '' }))).rejects.toThrow(
|
|
123
|
+
'Missing required cdpApiKeyId field',
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('throws CONFIG_ERROR for missing cdpApiKeySecret', async () => {
|
|
128
|
+
await expect(CdpSigner.create(makeConfig({ cdpApiKeySecret: '' }))).rejects.toThrow(
|
|
129
|
+
'Missing required cdpApiKeySecret field',
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('throws CONFIG_ERROR for missing cdpWalletSecret', async () => {
|
|
134
|
+
await expect(CdpSigner.create(makeConfig({ cdpWalletSecret: '' }))).rejects.toThrow(
|
|
135
|
+
'Missing required cdpWalletSecret field',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws CONFIG_ERROR for missing address', async () => {
|
|
140
|
+
await expect(CdpSigner.create(makeConfig({ address: '' }))).rejects.toThrow(
|
|
141
|
+
'Missing required address field',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('throws CONFIG_ERROR for invalid address', async () => {
|
|
146
|
+
await expect(CdpSigner.create(makeConfig({ address: 'not-a-valid-address' }))).rejects.toThrow(
|
|
147
|
+
'Invalid Solana address format',
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('throws CONFIG_ERROR for negative requestDelayMs', async () => {
|
|
152
|
+
await expect(CdpSigner.create(makeConfig({ requestDelayMs: -1 }))).rejects.toThrow(
|
|
153
|
+
'requestDelayMs must not be negative',
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('warns for high requestDelayMs', async () => {
|
|
158
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
159
|
+
await CdpSigner.create(makeConfig({ requestDelayMs: 5000 }));
|
|
160
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('requestDelayMs is greater than 3000ms'));
|
|
161
|
+
warnSpy.mockRestore();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('accepts custom baseUrl', async () => {
|
|
165
|
+
const signer = await CdpSigner.create(makeConfig({ baseUrl: 'https://custom.example.com' }));
|
|
166
|
+
expect(signer).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('throws CONFIG_ERROR when baseUrl is not a valid URL', async () => {
|
|
170
|
+
await expect(CdpSigner.create(makeConfig({ baseUrl: 'not-a-url' }))).rejects.toMatchObject({
|
|
171
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
172
|
+
message: expect.stringContaining('baseUrl is not a valid URL'),
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('throws CONFIG_ERROR when baseUrl does not use HTTPS', async () => {
|
|
177
|
+
await expect(
|
|
178
|
+
CdpSigner.create(makeConfig({ baseUrl: 'http://api.cdp.coinbase.com' })),
|
|
179
|
+
).rejects.toMatchObject({
|
|
180
|
+
code: 'SIGNER_CONFIG_ERROR',
|
|
181
|
+
message: expect.stringContaining('baseUrl must use HTTPS'),
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('accepts requestDelayMs of 0', async () => {
|
|
186
|
+
const signer = await CdpSigner.create(makeConfig({ requestDelayMs: 0 }));
|
|
187
|
+
expect(signer).toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('throws CONFIG_ERROR when Ed25519 pubkey does not match seed', async () => {
|
|
191
|
+
// 64 bytes where seed and pubkey are mismatched (all 0x42)
|
|
192
|
+
const mismatchedKey = Buffer.alloc(64, 0x42).toString('base64');
|
|
193
|
+
await expect(CdpSigner.create(makeConfig({ cdpApiKeySecret: mismatchedKey }))).rejects.toThrow(
|
|
194
|
+
'Invalid cdpApiKeySecret',
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('signMessages', () => {
|
|
200
|
+
it('signs a message and returns a signature dictionary', async () => {
|
|
201
|
+
// Base58-encoded 64-byte signature
|
|
202
|
+
const base58Sig = '5LfnqEfGPFBaHHeQBiNkgQ2EPy4FkVLKE7cjMYc7gv6EjE8Vs5gqaXcZHjpxr3yj5TMt7j3JdJPkXfnwXxXiNAh';
|
|
203
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify({ signature: base58Sig }), { status: 200 }));
|
|
204
|
+
|
|
205
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
206
|
+
const message = { content: new TextEncoder().encode('hello'), signatures: {} };
|
|
207
|
+
const result = await signer.signMessages([message]);
|
|
208
|
+
|
|
209
|
+
expect(result).toHaveLength(1);
|
|
210
|
+
expect(result[0]?.[TEST_ADDRESS as Address]).toBeDefined();
|
|
211
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
212
|
+
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
213
|
+
expect(url).toContain('/sign/message');
|
|
214
|
+
expect(JSON.parse(init.body as string)).toMatchObject({ message: 'hello' });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('handles multiple messages with requestDelayMs', async () => {
|
|
218
|
+
const base58Sig = '5LfnqEfGPFBaHHeQBiNkgQ2EPy4FkVLKE7cjMYc7gv6EjE8Vs5gqaXcZHjpxr3yj5TMt7j3JdJPkXfnwXxXiNAh';
|
|
219
|
+
// Use mockImplementation so each concurrent call gets a fresh Response (body can only be read once)
|
|
220
|
+
mockFetch.mockImplementation(() =>
|
|
221
|
+
Promise.resolve(new Response(JSON.stringify({ signature: base58Sig }), { status: 200 })),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const signer = await CdpSigner.create(makeConfig({ requestDelayMs: 10 }));
|
|
225
|
+
const messages = [
|
|
226
|
+
{ content: new TextEncoder().encode('one'), signatures: {} },
|
|
227
|
+
{ content: new TextEncoder().encode('two'), signatures: {} },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
const result = await signer.signMessages(messages);
|
|
232
|
+
const elapsed = Date.now() - startTime;
|
|
233
|
+
|
|
234
|
+
expect(result).toHaveLength(2);
|
|
235
|
+
expect(elapsed).toBeGreaterThanOrEqual(8); // at least one 10ms delay
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('throws HTTP_ERROR on network failure', async () => {
|
|
239
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
240
|
+
|
|
241
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
242
|
+
const message = { content: new TextEncoder().encode('hello'), signatures: {} };
|
|
243
|
+
|
|
244
|
+
await expect(signer.signMessages([message])).rejects.toThrow('CDP signMessage network request failed');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('throws REMOTE_API_ERROR on non-2xx response', async () => {
|
|
248
|
+
mockFetch.mockResolvedValue(new Response('{"error":"Unauthorized"}', { status: 401 }));
|
|
249
|
+
|
|
250
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
251
|
+
const message = { content: new TextEncoder().encode('hello'), signatures: {} };
|
|
252
|
+
|
|
253
|
+
await expect(signer.signMessages([message])).rejects.toThrow('CDP signMessage API error: 401');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('throws SIGNING_FAILED for invalid signature length', async () => {
|
|
257
|
+
// Return a base58 string that decodes to != 64 bytes (small value)
|
|
258
|
+
mockFetch.mockResolvedValue(
|
|
259
|
+
new Response(JSON.stringify({ signature: '1' }), { status: 200 }), // '1' decodes to 1 byte
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
263
|
+
const message = { content: new TextEncoder().encode('hello'), signatures: {} };
|
|
264
|
+
|
|
265
|
+
await expect(signer.signMessages([message])).rejects.toThrow('Invalid signature length');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('throws SERIALIZATION_ERROR for invalid UTF-8 message', async () => {
|
|
269
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
270
|
+
const message = { content: new Uint8Array([0xff]), signatures: {} };
|
|
271
|
+
|
|
272
|
+
await expect(signer.signMessages([message])).rejects.toThrow(
|
|
273
|
+
'CDP signMessage requires a valid UTF-8 message',
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('signTransactions', () => {
|
|
279
|
+
it('accepts a key pair address as the signer address', async () => {
|
|
280
|
+
const keyPair = await generateKeyPairSigner();
|
|
281
|
+
const signer = await CdpSigner.create(makeConfig({ address: keyPair.address }));
|
|
282
|
+
expect(signer.address).toBe(keyPair.address);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('calls CDP signTransaction with the correct address and wire transaction', async () => {
|
|
286
|
+
mockFetch.mockResolvedValue(
|
|
287
|
+
new Response(JSON.stringify({ signedTransaction: MOCK_B64_WIRE_TX }), { status: 200 }),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
291
|
+
const mockTx = createMockTransaction();
|
|
292
|
+
|
|
293
|
+
// The CDP call succeeds; extractSignatureFromWireTransaction throws because
|
|
294
|
+
// MOCK_B64_WIRE_TX was not signed by TEST_ADDRESS (integration tests cover success path)
|
|
295
|
+
await expect(signer.signTransactions([mockTx])).rejects.toThrow();
|
|
296
|
+
|
|
297
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
298
|
+
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
299
|
+
expect(url).toContain('/sign/transaction');
|
|
300
|
+
expect(JSON.parse(init.body as string)).toMatchObject({ transaction: MOCK_B64_WIRE_TX });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('throws HTTP_ERROR on network failure', async () => {
|
|
304
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
305
|
+
|
|
306
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
307
|
+
const mockTx = createMockTransaction();
|
|
308
|
+
|
|
309
|
+
await expect(signer.signTransactions([mockTx])).rejects.toThrow(
|
|
310
|
+
'CDP signTransaction network request failed',
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('throws REMOTE_API_ERROR on non-2xx response', async () => {
|
|
315
|
+
mockFetch.mockResolvedValue(new Response('{"error":"Forbidden"}', { status: 403 }));
|
|
316
|
+
|
|
317
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
318
|
+
const mockTx = createMockTransaction();
|
|
319
|
+
|
|
320
|
+
await expect(signer.signTransactions([mockTx])).rejects.toThrow('CDP signTransaction API error: 403');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('isAvailable', () => {
|
|
325
|
+
it('returns true when the account is accessible', async () => {
|
|
326
|
+
mockFetch.mockResolvedValue(new Response(JSON.stringify({ address: TEST_ADDRESS }), { status: 200 }));
|
|
327
|
+
|
|
328
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
329
|
+
const available = await signer.isAvailable();
|
|
330
|
+
|
|
331
|
+
expect(available).toBe(true);
|
|
332
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
333
|
+
const [url] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
334
|
+
expect(url).toContain(TEST_ADDRESS);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('returns false when the account is not found', async () => {
|
|
338
|
+
mockFetch.mockResolvedValue(new Response('', { status: 404 }));
|
|
339
|
+
|
|
340
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
341
|
+
const available = await signer.isAvailable();
|
|
342
|
+
|
|
343
|
+
expect(available).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('returns false when the CDP API is unreachable', async () => {
|
|
347
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
348
|
+
|
|
349
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
350
|
+
const available = await signer.isAvailable();
|
|
351
|
+
|
|
352
|
+
expect(available).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('returns false on 401 unauthorized', async () => {
|
|
356
|
+
mockFetch.mockResolvedValue(new Response('', { status: 401 }));
|
|
357
|
+
|
|
358
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
359
|
+
const available = await signer.isAvailable();
|
|
360
|
+
|
|
361
|
+
expect(available).toBe(false);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('returns false when auth header generation fails', async () => {
|
|
365
|
+
const signer = await CdpSigner.create(makeConfig());
|
|
366
|
+
const subtleSignSpy = vi
|
|
367
|
+
.spyOn(globalThis.crypto.subtle, 'sign')
|
|
368
|
+
.mockRejectedValueOnce(new Error('sign failed'));
|
|
369
|
+
|
|
370
|
+
const available = await signer.isAvailable();
|
|
371
|
+
|
|
372
|
+
expect(available).toBe(false);
|
|
373
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
374
|
+
subtleSignSpy.mockRestore();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SolanaSigner } from '@solana/keychain-core';
|
|
2
|
+
import type { SignerTestConfig, TestScenario } from '@solana/keychain-test-utils';
|
|
3
|
+
|
|
4
|
+
import { createCdpSigner } from '../cdp-signer.js';
|
|
5
|
+
|
|
6
|
+
const SIGNER_TYPE = 'cdp';
|
|
7
|
+
const REQUIRED_ENV_VARS = ['CDP_API_KEY_ID', 'CDP_API_KEY_SECRET', 'CDP_WALLET_SECRET', 'CDP_SOLANA_ADDRESS'];
|
|
8
|
+
|
|
9
|
+
const CONFIG: SignerTestConfig<SolanaSigner> = {
|
|
10
|
+
createSigner: () =>
|
|
11
|
+
createCdpSigner({
|
|
12
|
+
cdpApiKeyId: process.env.CDP_API_KEY_ID!,
|
|
13
|
+
cdpApiKeySecret: process.env.CDP_API_KEY_SECRET!,
|
|
14
|
+
cdpWalletSecret: process.env.CDP_WALLET_SECRET!,
|
|
15
|
+
address: process.env.CDP_SOLANA_ADDRESS!,
|
|
16
|
+
}),
|
|
17
|
+
requiredEnvVars: REQUIRED_ENV_VARS,
|
|
18
|
+
signerType: SIGNER_TYPE,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function getConfig(scenarios: TestScenario[]): Promise<SignerTestConfig<SolanaSigner>> {
|
|
22
|
+
return {
|
|
23
|
+
...CONFIG,
|
|
24
|
+
testScenarios: scenarios,
|
|
25
|
+
};
|
|
26
|
+
}
|