easctl 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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/README.md +195 -0
  3. package/dist/index.js +31401 -0
  4. package/dist/index.js.map +1 -0
  5. package/manual-test/package-lock.json +4483 -0
  6. package/manual-test/package.json +15 -0
  7. package/package.json +40 -0
  8. package/src/__tests__/chains.test.ts +82 -0
  9. package/src/__tests__/clear-key.test.ts +40 -0
  10. package/src/__tests__/client.test.ts +168 -0
  11. package/src/__tests__/commands/attest.test.ts +203 -0
  12. package/src/__tests__/commands/get-attestation.test.ts +164 -0
  13. package/src/__tests__/commands/multi-attest.test.ts +166 -0
  14. package/src/__tests__/commands/multi-revoke.test.ts +114 -0
  15. package/src/__tests__/commands/multi-timestamp.test.ts +88 -0
  16. package/src/__tests__/commands/offchain-attest.test.ts +217 -0
  17. package/src/__tests__/commands/query-attestation.test.ts +84 -0
  18. package/src/__tests__/commands/query-attestations.test.ts +156 -0
  19. package/src/__tests__/commands/query-schema.test.ts +62 -0
  20. package/src/__tests__/commands/query-schemas.test.ts +110 -0
  21. package/src/__tests__/commands/revoke.test.ts +86 -0
  22. package/src/__tests__/commands/schema-get.test.ts +66 -0
  23. package/src/__tests__/commands/schema-register.test.ts +94 -0
  24. package/src/__tests__/commands/timestamp.test.ts +78 -0
  25. package/src/__tests__/config.test.ts +103 -0
  26. package/src/__tests__/graphql.test.ts +148 -0
  27. package/src/__tests__/integration/graphql-live.test.ts +103 -0
  28. package/src/__tests__/integration/offchain-signing.test.ts +252 -0
  29. package/src/__tests__/integration/schema-encoder.test.ts +131 -0
  30. package/src/__tests__/output.test.ts +138 -0
  31. package/src/__tests__/set-key.test.ts +58 -0
  32. package/src/__tests__/stdin.test.ts +15 -0
  33. package/src/chains.ts +99 -0
  34. package/src/client.ts +53 -0
  35. package/src/commands/attest.ts +73 -0
  36. package/src/commands/clear-key.ts +15 -0
  37. package/src/commands/get-attestation.ts +58 -0
  38. package/src/commands/multi-attest.ts +75 -0
  39. package/src/commands/multi-revoke.ts +60 -0
  40. package/src/commands/multi-timestamp.ts +43 -0
  41. package/src/commands/offchain-attest.ts +78 -0
  42. package/src/commands/query-attestation.ts +31 -0
  43. package/src/commands/query-attestations.ts +57 -0
  44. package/src/commands/query-schema.ts +24 -0
  45. package/src/commands/query-schemas.ts +35 -0
  46. package/src/commands/revoke.ts +48 -0
  47. package/src/commands/schema-get.ts +30 -0
  48. package/src/commands/schema-register.ts +49 -0
  49. package/src/commands/set-key.ts +19 -0
  50. package/src/commands/timestamp.ts +35 -0
  51. package/src/config.ts +41 -0
  52. package/src/graphql.ts +136 -0
  53. package/src/index.ts +74 -0
  54. package/src/output.ts +50 -0
  55. package/src/stdin.ts +15 -0
  56. package/src/validation.ts +15 -0
  57. package/tsconfig.json +16 -0
  58. package/tsup.config.ts +21 -0
  59. package/vitest.config.ts +7 -0
@@ -0,0 +1,252 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { ethers } from 'ethers';
3
+ import {
4
+ SchemaEncoder,
5
+ Offchain,
6
+ OffchainAttestationVersion,
7
+ createOffchainURL,
8
+ NO_EXPIRATION,
9
+ ZERO_BYTES32,
10
+ EAS,
11
+ } from '@ethereum-attestation-service/eas-sdk';
12
+
13
+ // Use a deterministic test private key (not a real key - for testing only)
14
+ const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
15
+
16
+ describe('off-chain attestation signing integration', () => {
17
+ let signer: ethers.Wallet;
18
+ let offchain: Offchain;
19
+ const EAS_CONTRACT_ADDRESS = '0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587';
20
+
21
+ beforeAll(() => {
22
+ signer = new ethers.Wallet(TEST_PRIVATE_KEY);
23
+
24
+ // Create Offchain instance directly with test config (no network needed)
25
+ offchain = new Offchain(
26
+ {
27
+ address: EAS_CONTRACT_ADDRESS,
28
+ version: '1.3.0',
29
+ chainId: 1n,
30
+ },
31
+ OffchainAttestationVersion.Version2,
32
+ new EAS(EAS_CONTRACT_ADDRESS)
33
+ );
34
+ });
35
+
36
+ it('signs and verifies an off-chain attestation', async () => {
37
+ const schemaEncoder = new SchemaEncoder('uint256 score, string name');
38
+ const encodedData = schemaEncoder.encodeData([
39
+ { name: 'score', type: 'uint256', value: 100 },
40
+ { name: 'name', type: 'string', value: 'Alice' },
41
+ ]);
42
+
43
+ const schemaUID = '0x' + 'ab'.repeat(32);
44
+
45
+ const signedAttestation = await offchain.signOffchainAttestation(
46
+ {
47
+ schema: schemaUID,
48
+ recipient: '0x0000000000000000000000000000000000000001',
49
+ time: BigInt(Math.floor(Date.now() / 1000)),
50
+ expirationTime: NO_EXPIRATION,
51
+ revocable: true,
52
+ refUID: ZERO_BYTES32,
53
+ data: encodedData,
54
+ },
55
+ signer
56
+ );
57
+
58
+ // Verify the attestation has expected fields
59
+ expect(signedAttestation.uid).toMatch(/^0x[0-9a-fA-F]{64}$/);
60
+ expect(signedAttestation.message).toBeDefined();
61
+ expect(signedAttestation.signature).toBeDefined();
62
+ expect(signedAttestation.message.schema).toBe(schemaUID);
63
+ expect(signedAttestation.message.recipient).toBe('0x0000000000000000000000000000000000000001');
64
+ expect(signedAttestation.message.revocable).toBe(true);
65
+
66
+ // Verify the signature is valid
67
+ const isValid = offchain.verifyOffchainAttestationSignature(
68
+ signer.address,
69
+ signedAttestation
70
+ );
71
+ expect(isValid).toBe(true);
72
+ });
73
+
74
+ it('rejects tampered attestation data', async () => {
75
+ const schemaEncoder = new SchemaEncoder('string msg');
76
+ const encodedData = schemaEncoder.encodeData([
77
+ { name: 'msg', type: 'string', value: 'original' },
78
+ ]);
79
+
80
+ const signed = await offchain.signOffchainAttestation(
81
+ {
82
+ schema: '0x' + '00'.repeat(32),
83
+ recipient: '0x0000000000000000000000000000000000000000',
84
+ time: BigInt(Math.floor(Date.now() / 1000)),
85
+ expirationTime: NO_EXPIRATION,
86
+ revocable: true,
87
+ refUID: ZERO_BYTES32,
88
+ data: encodedData,
89
+ },
90
+ signer
91
+ );
92
+
93
+ // Tamper with the message recipient
94
+ const tampered = {
95
+ ...signed,
96
+ message: { ...signed.message, recipient: '0x0000000000000000000000000000000000000099' },
97
+ };
98
+
99
+ // Tampered data should fail verification against the real signer
100
+ const isTamperedValid = offchain.verifyOffchainAttestationSignature(
101
+ signer.address,
102
+ tampered
103
+ );
104
+ expect(isTamperedValid).toBe(false);
105
+
106
+ // Wrong attester on untampered data should also fail
107
+ const isWrongAttesterValid = offchain.verifyOffchainAttestationSignature(
108
+ '0x0000000000000000000000000000000000000099',
109
+ signed
110
+ );
111
+ expect(isWrongAttesterValid).toBe(false);
112
+ });
113
+
114
+ it('different signers produce different signatures', async () => {
115
+ const signer2 = new ethers.Wallet(
116
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
117
+ );
118
+
119
+ const schemaEncoder = new SchemaEncoder('uint8 x');
120
+ const encodedData = schemaEncoder.encodeData([{ name: 'x', type: 'uint8', value: 1 }]);
121
+
122
+ const params = {
123
+ schema: '0x' + '11'.repeat(32),
124
+ recipient: '0x0000000000000000000000000000000000000000',
125
+ time: 1700000000n,
126
+ expirationTime: NO_EXPIRATION,
127
+ revocable: true,
128
+ refUID: ZERO_BYTES32,
129
+ data: encodedData,
130
+ };
131
+
132
+ const signed1 = await offchain.signOffchainAttestation(params, signer);
133
+ const signed2 = await offchain.signOffchainAttestation(params, signer2);
134
+
135
+ // Different signers produce different signatures
136
+ expect(signed1.signature).not.toEqual(signed2.signature);
137
+
138
+ // Each verifies with its own signer
139
+ expect(offchain.verifyOffchainAttestationSignature(signer.address, signed1)).toBe(true);
140
+ expect(offchain.verifyOffchainAttestationSignature(signer2.address, signed2)).toBe(true);
141
+
142
+ // Cross-verification fails
143
+ expect(offchain.verifyOffchainAttestationSignature(signer.address, signed2)).toBe(false);
144
+ expect(offchain.verifyOffchainAttestationSignature(signer2.address, signed1)).toBe(false);
145
+ });
146
+
147
+ it('non-revocable attestation signs correctly', async () => {
148
+ const schemaEncoder = new SchemaEncoder('uint8 x');
149
+ const encodedData = schemaEncoder.encodeData([{ name: 'x', type: 'uint8', value: 1 }]);
150
+
151
+ const signed = await offchain.signOffchainAttestation(
152
+ {
153
+ schema: '0x' + '22'.repeat(32),
154
+ recipient: '0x0000000000000000000000000000000000000000',
155
+ time: 1700000000n,
156
+ expirationTime: NO_EXPIRATION,
157
+ revocable: false,
158
+ refUID: ZERO_BYTES32,
159
+ data: encodedData,
160
+ },
161
+ signer
162
+ );
163
+
164
+ expect(signed.message.revocable).toBe(false);
165
+ expect(offchain.verifyOffchainAttestationSignature(signer.address, signed)).toBe(true);
166
+ });
167
+
168
+ it('attestation with expiration signs correctly', async () => {
169
+ const schemaEncoder = new SchemaEncoder('uint8 x');
170
+ const encodedData = schemaEncoder.encodeData([{ name: 'x', type: 'uint8', value: 1 }]);
171
+
172
+ const expiration = BigInt(Math.floor(Date.now() / 1000) + 86400); // +1 day
173
+
174
+ const signed = await offchain.signOffchainAttestation(
175
+ {
176
+ schema: '0x' + '33'.repeat(32),
177
+ recipient: '0x0000000000000000000000000000000000000000',
178
+ time: 1700000000n,
179
+ expirationTime: expiration,
180
+ revocable: true,
181
+ refUID: ZERO_BYTES32,
182
+ data: encodedData,
183
+ },
184
+ signer
185
+ );
186
+
187
+ expect(signed.message.expirationTime).toBe(expiration);
188
+ expect(offchain.verifyOffchainAttestationSignature(signer.address, signed)).toBe(true);
189
+ });
190
+ });
191
+
192
+ describe('createOffchainURL integration', () => {
193
+ it('produces a URL path from a signed attestation package', async () => {
194
+ const signer = new ethers.Wallet(TEST_PRIVATE_KEY);
195
+ const offchain = new Offchain(
196
+ { address: '0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587', version: '1.3.0', chainId: 1n },
197
+ OffchainAttestationVersion.Version2,
198
+ new EAS('0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587')
199
+ );
200
+
201
+ const encoder = new SchemaEncoder('uint8 x');
202
+ const data = encoder.encodeData([{ name: 'x', type: 'uint8', value: 1 }]);
203
+
204
+ const signed = await offchain.signOffchainAttestation(
205
+ {
206
+ schema: '0x' + '00'.repeat(32),
207
+ recipient: '0x0000000000000000000000000000000000000000',
208
+ time: 1700000000n,
209
+ expirationTime: NO_EXPIRATION,
210
+ revocable: true,
211
+ refUID: ZERO_BYTES32,
212
+ data,
213
+ },
214
+ signer
215
+ );
216
+
217
+ const pkg = { sig: signed, signer: signer.address };
218
+ const urlPath = createOffchainURL(pkg);
219
+
220
+ expect(urlPath).toContain('/offchain/url/#attestation=');
221
+ expect(urlPath.length).toBeGreaterThan(50);
222
+ });
223
+
224
+ it('produces deterministic URLs for same input', async () => {
225
+ const signer = new ethers.Wallet(TEST_PRIVATE_KEY);
226
+ const offchain = new Offchain(
227
+ { address: '0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587', version: '1.3.0', chainId: 1n },
228
+ OffchainAttestationVersion.Version2,
229
+ new EAS('0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587')
230
+ );
231
+
232
+ const encoder = new SchemaEncoder('string msg');
233
+ const data = encoder.encodeData([{ name: 'msg', type: 'string', value: 'test' }]);
234
+
235
+ const params = {
236
+ schema: '0x' + 'ff'.repeat(32),
237
+ recipient: '0x0000000000000000000000000000000000000001',
238
+ time: 1700000000n,
239
+ expirationTime: NO_EXPIRATION,
240
+ revocable: true,
241
+ refUID: ZERO_BYTES32,
242
+ data,
243
+ };
244
+
245
+ const signed = await offchain.signOffchainAttestation(params, signer);
246
+ const pkg = { sig: signed, signer: signer.address };
247
+
248
+ const url1 = createOffchainURL(pkg);
249
+ const url2 = createOffchainURL(pkg);
250
+ expect(url1).toBe(url2);
251
+ });
252
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk';
3
+
4
+ describe('SchemaEncoder integration', () => {
5
+ describe('encode/decode round-trip', () => {
6
+ it('handles uint256', () => {
7
+ const encoder = new SchemaEncoder('uint256 score');
8
+ const encoded = encoder.encodeData([{ name: 'score', type: 'uint256', value: 42 }]);
9
+ const decoded = encoder.decodeData(encoded);
10
+
11
+ expect(decoded).toHaveLength(1);
12
+ expect(decoded[0].name).toBe('score');
13
+ expect(decoded[0].type).toBe('uint256');
14
+ expect(decoded[0].value.value).toBe(42n);
15
+ });
16
+
17
+ it('handles string', () => {
18
+ const encoder = new SchemaEncoder('string name');
19
+ const encoded = encoder.encodeData([{ name: 'name', type: 'string', value: 'hello world' }]);
20
+ const decoded = encoder.decodeData(encoded);
21
+
22
+ expect(decoded[0].value.value).toBe('hello world');
23
+ });
24
+
25
+ it('handles bool', () => {
26
+ const encoder = new SchemaEncoder('bool active');
27
+ const encoded = encoder.encodeData([{ name: 'active', type: 'bool', value: true }]);
28
+ const decoded = encoder.decodeData(encoded);
29
+
30
+ expect(decoded[0].value.value).toBe(true);
31
+ });
32
+
33
+ it('handles address', () => {
34
+ const addr = '0x0000000000000000000000000000000000000001';
35
+ const encoder = new SchemaEncoder('address wallet');
36
+ const encoded = encoder.encodeData([{ name: 'wallet', type: 'address', value: addr }]);
37
+ const decoded = encoder.decodeData(encoded);
38
+
39
+ expect(decoded[0].value.value).toBe(addr);
40
+ });
41
+
42
+ it('handles bytes32', () => {
43
+ const hash = '0x' + 'ab'.repeat(32);
44
+ const encoder = new SchemaEncoder('bytes32 hash');
45
+ const encoded = encoder.encodeData([{ name: 'hash', type: 'bytes32', value: hash }]);
46
+ const decoded = encoder.decodeData(encoded);
47
+
48
+ expect(decoded[0].value.value).toBe(hash);
49
+ });
50
+
51
+ it('handles multiple fields', () => {
52
+ const encoder = new SchemaEncoder('uint256 score, string name, bool active');
53
+ const encoded = encoder.encodeData([
54
+ { name: 'score', type: 'uint256', value: 100 },
55
+ { name: 'name', type: 'string', value: 'Alice' },
56
+ { name: 'active', type: 'bool', value: false },
57
+ ]);
58
+ const decoded = encoder.decodeData(encoded);
59
+
60
+ expect(decoded).toHaveLength(3);
61
+ expect(decoded[0].value.value).toBe(100n);
62
+ expect(decoded[1].value.value).toBe('Alice');
63
+ expect(decoded[2].value.value).toBe(false);
64
+ });
65
+
66
+ it('handles large BigInt values', () => {
67
+ const encoder = new SchemaEncoder('uint256 amount');
68
+ const bigVal = 2n ** 128n - 1n;
69
+ const encoded = encoder.encodeData([{ name: 'amount', type: 'uint256', value: bigVal }]);
70
+ const decoded = encoder.decodeData(encoded);
71
+
72
+ expect(decoded[0].value.value).toBe(bigVal);
73
+ });
74
+
75
+ it('handles zero values', () => {
76
+ const encoder = new SchemaEncoder('uint256 val');
77
+ const encoded = encoder.encodeData([{ name: 'val', type: 'uint256', value: 0 }]);
78
+ const decoded = encoder.decodeData(encoded);
79
+
80
+ expect(decoded[0].value.value).toBe(0n);
81
+ });
82
+
83
+ it('handles empty string', () => {
84
+ const encoder = new SchemaEncoder('string text');
85
+ const encoded = encoder.encodeData([{ name: 'text', type: 'string', value: '' }]);
86
+ const decoded = encoder.decodeData(encoded);
87
+
88
+ expect(decoded[0].value.value).toBe('');
89
+ });
90
+
91
+ it('handles bytes (dynamic)', () => {
92
+ const encoder = new SchemaEncoder('bytes data');
93
+ const encoded = encoder.encodeData([{ name: 'data', type: 'bytes', value: '0xdeadbeef' }]);
94
+ const decoded = encoder.decodeData(encoded);
95
+
96
+ expect(decoded[0].value.value).toBe('0xdeadbeef');
97
+ });
98
+ });
99
+
100
+ describe('schema validation', () => {
101
+ it('throws on invalid schema type', () => {
102
+ expect(() => new SchemaEncoder('invalidtype foo')).toThrow();
103
+ });
104
+
105
+ it('throws on mismatched field count in encodeData', () => {
106
+ const encoder = new SchemaEncoder('uint256 a, string b');
107
+ expect(() =>
108
+ encoder.encodeData([{ name: 'a', type: 'uint256', value: 1 }])
109
+ ).toThrow();
110
+ });
111
+ });
112
+
113
+ describe('encoded output format', () => {
114
+ it('produces a hex string', () => {
115
+ const encoder = new SchemaEncoder('uint256 x');
116
+ const encoded = encoder.encodeData([{ name: 'x', type: 'uint256', value: 1 }]);
117
+ expect(encoded).toMatch(/^0x[0-9a-fA-F]+$/);
118
+ });
119
+
120
+ it('produces deterministic encoding', () => {
121
+ const encoder = new SchemaEncoder('uint256 x, string y');
122
+ const data = [
123
+ { name: 'x', type: 'uint256', value: 42 },
124
+ { name: 'y', type: 'string', value: 'test' },
125
+ ];
126
+ const encoded1 = encoder.encodeData(data);
127
+ const encoded2 = encoder.encodeData(data);
128
+ expect(encoded1).toBe(encoded2);
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { setJsonMode, isJsonMode, output, handleError } from '../output.js';
3
+
4
+ describe('output module', () => {
5
+ let logSpy: ReturnType<typeof vi.spyOn>;
6
+ let errorSpy: ReturnType<typeof vi.spyOn>;
7
+
8
+ beforeEach(() => {
9
+ logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
10
+ errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
11
+ setJsonMode(false);
12
+ });
13
+
14
+ afterEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ describe('setJsonMode / isJsonMode', () => {
19
+ it('defaults to false', () => {
20
+ expect(isJsonMode()).toBe(false);
21
+ });
22
+
23
+ it('sets to true', () => {
24
+ setJsonMode(true);
25
+ expect(isJsonMode()).toBe(true);
26
+ });
27
+
28
+ it('toggles back to false', () => {
29
+ setJsonMode(true);
30
+ setJsonMode(false);
31
+ expect(isJsonMode()).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe('output() in JSON mode', () => {
36
+ beforeEach(() => setJsonMode(true));
37
+
38
+ it('outputs JSON for success result', () => {
39
+ output({ success: true, data: { uid: 'abc' } });
40
+ expect(logSpy).toHaveBeenCalledOnce();
41
+ const parsed = JSON.parse(logSpy.mock.calls[0][0]);
42
+ expect(parsed).toEqual({ success: true, data: { uid: 'abc' } });
43
+ });
44
+
45
+ it('serializes BigInt values as strings', () => {
46
+ output({ success: true, data: { value: BigInt(123) as any } });
47
+ const parsed = JSON.parse(logSpy.mock.calls[0][0]);
48
+ expect(parsed.data.value).toBe('123');
49
+ });
50
+
51
+ it('outputs JSON for error result', () => {
52
+ output({ success: false, error: 'something failed' });
53
+ const parsed = JSON.parse(logSpy.mock.calls[0][0]);
54
+ expect(parsed).toEqual({ success: false, error: 'something failed' });
55
+ });
56
+ });
57
+
58
+ describe('output() in text mode', () => {
59
+ it('prints key-value pairs for success with data', () => {
60
+ output({ success: true, data: { uid: 'abc', chain: 'ethereum' } });
61
+ expect(logSpy).toHaveBeenCalledWith('uid: abc');
62
+ expect(logSpy).toHaveBeenCalledWith('chain: ethereum');
63
+ });
64
+
65
+ it('prints nested objects with indentation', () => {
66
+ output({ success: true, data: { info: { name: 'test', value: '42' } } });
67
+ expect(logSpy).toHaveBeenCalledWith('info:');
68
+ expect(logSpy).toHaveBeenCalledWith(' name: test');
69
+ expect(logSpy).toHaveBeenCalledWith(' value: 42');
70
+ });
71
+
72
+ it('formats BigInt values as strings in text mode', () => {
73
+ output({ success: true, data: { amount: BigInt(999) as any } });
74
+ expect(logSpy).toHaveBeenCalledWith('amount: 999');
75
+ });
76
+
77
+ it('formats array values through nested object path', () => {
78
+ output({ success: true, data: { items: [1, 2, 3] as any } });
79
+ // Arrays are objects, so they go through the nested object path
80
+ expect(logSpy).toHaveBeenCalledWith('items:');
81
+ expect(logSpy).toHaveBeenCalledWith(' 0: 1');
82
+ expect(logSpy).toHaveBeenCalledWith(' 1: 2');
83
+ expect(logSpy).toHaveBeenCalledWith(' 2: 3');
84
+ });
85
+
86
+ it('prints error to stderr', () => {
87
+ output({ success: false, error: 'bad thing happened' });
88
+ expect(errorSpy).toHaveBeenCalledWith('Error: bad thing happened');
89
+ expect(logSpy).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it('does nothing for success with no data', () => {
93
+ output({ success: true });
94
+ expect(logSpy).not.toHaveBeenCalled();
95
+ expect(errorSpy).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it('does nothing for success false with no error', () => {
99
+ output({ success: false });
100
+ expect(logSpy).not.toHaveBeenCalled();
101
+ expect(errorSpy).not.toHaveBeenCalled();
102
+ });
103
+ });
104
+
105
+ describe('handleError', () => {
106
+ let exitSpy: ReturnType<typeof vi.spyOn>;
107
+
108
+ beforeEach(() => {
109
+ exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
110
+ });
111
+
112
+ it('extracts message from Error instance', () => {
113
+ handleError(new Error('test error'));
114
+ expect(errorSpy).toHaveBeenCalledWith('Error: test error');
115
+ expect(exitSpy).toHaveBeenCalledWith(1);
116
+ });
117
+
118
+ it('converts non-Error to string', () => {
119
+ handleError('string error');
120
+ expect(errorSpy).toHaveBeenCalledWith('Error: string error');
121
+ expect(exitSpy).toHaveBeenCalledWith(1);
122
+ });
123
+
124
+ it('converts number to string', () => {
125
+ handleError(42);
126
+ expect(errorSpy).toHaveBeenCalledWith('Error: 42');
127
+ expect(exitSpy).toHaveBeenCalledWith(1);
128
+ });
129
+
130
+ it('outputs JSON error in JSON mode', () => {
131
+ setJsonMode(true);
132
+ handleError(new Error('json error'));
133
+ const parsed = JSON.parse(logSpy.mock.calls[0][0]);
134
+ expect(parsed).toEqual({ success: false, error: 'json error' });
135
+ expect(exitSpy).toHaveBeenCalledWith(1);
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ vi.mock('../config.js', () => ({
4
+ setStoredPrivateKey: vi.fn(),
5
+ }));
6
+
7
+ vi.mock('../output.js', () => ({
8
+ output: vi.fn(),
9
+ handleError: vi.fn(),
10
+ }));
11
+
12
+ const mockWalletAddress = '0xMockWalletAddress';
13
+ vi.mock('ethers', () => ({
14
+ ethers: {
15
+ Wallet: class MockWallet {
16
+ address = mockWalletAddress;
17
+ constructor(key: string) {
18
+ if (key === '0xinvalid') throw new Error('invalid private key');
19
+ }
20
+ },
21
+ },
22
+ }));
23
+
24
+ import { setKeyCommand } from '../commands/set-key.js';
25
+ import { setStoredPrivateKey } from '../config.js';
26
+ import { output, handleError } from '../output.js';
27
+
28
+ describe('set-key command', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ it('stores key and outputs wallet address', async () => {
34
+ await setKeyCommand.parseAsync(['node', 'test', '0xabc123']);
35
+ expect(setStoredPrivateKey).toHaveBeenCalledWith('0xabc123');
36
+ expect(output).toHaveBeenCalledWith({
37
+ success: true,
38
+ data: { address: mockWalletAddress },
39
+ });
40
+ });
41
+
42
+ it('rejects invalid key', async () => {
43
+ await setKeyCommand.parseAsync(['node', 'test', 'invalid']);
44
+ expect(handleError).toHaveBeenCalledWith(expect.any(Error));
45
+ const err = (handleError as any).mock.calls[0][0] as Error;
46
+ expect(err.message).toBe('Invalid private key format');
47
+ });
48
+
49
+ it('normalizes key without 0x prefix', async () => {
50
+ await setKeyCommand.parseAsync(['node', 'test', 'abc123']);
51
+ expect(setStoredPrivateKey).toHaveBeenCalledWith('abc123');
52
+ });
53
+
54
+ it('handles key with 0x prefix', async () => {
55
+ await setKeyCommand.parseAsync(['node', 'test', '0xabc123']);
56
+ expect(setStoredPrivateKey).toHaveBeenCalledWith('0xabc123');
57
+ });
58
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveInput } from '../stdin.js';
3
+
4
+ describe('resolveInput', () => {
5
+ it('returns value as-is when not "-"', async () => {
6
+ const result = await resolveInput('some data');
7
+ expect(result).toBe('some data');
8
+ });
9
+
10
+ it('returns JSON string as-is when not "-"', async () => {
11
+ const json = '[{"name":"score","type":"uint256","value":"100"}]';
12
+ const result = await resolveInput(json);
13
+ expect(result).toBe(json);
14
+ });
15
+ });
package/src/chains.ts ADDED
@@ -0,0 +1,99 @@
1
+ export interface ChainConfig {
2
+ chainId: number;
3
+ eas: string;
4
+ schemaRegistry: string;
5
+ defaultRpc?: string;
6
+ }
7
+
8
+ const SHARED_V1_ADDRESSES = {
9
+ eas: '0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587',
10
+ schemaRegistry: '0xA7b39296258348C78294F95B872b282326A97BDF',
11
+ };
12
+
13
+ const OP_STACK_PREDEPLOY = {
14
+ eas: '0x4200000000000000000000000000000000000021',
15
+ schemaRegistry: '0x4200000000000000000000000000000000000020',
16
+ };
17
+
18
+ export const CHAIN_CONFIGS: Record<string, ChainConfig> = {
19
+ ethereum: {
20
+ chainId: 1,
21
+ ...SHARED_V1_ADDRESSES,
22
+ defaultRpc: 'https://ethereum-rpc.publicnode.com',
23
+ },
24
+ sepolia: {
25
+ chainId: 11155111,
26
+ eas: '0xC2679fBD37d54388Ce493F1DB75320D236e1815e',
27
+ schemaRegistry: '0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0',
28
+ defaultRpc: 'https://ethereum-sepolia-rpc.publicnode.com',
29
+ },
30
+ base: {
31
+ chainId: 8453,
32
+ ...OP_STACK_PREDEPLOY,
33
+ defaultRpc: 'https://base-rpc.publicnode.com',
34
+ },
35
+ 'base-sepolia': {
36
+ chainId: 84532,
37
+ ...OP_STACK_PREDEPLOY,
38
+ defaultRpc: 'https://base-sepolia-rpc.publicnode.com',
39
+ },
40
+ optimism: {
41
+ chainId: 10,
42
+ ...OP_STACK_PREDEPLOY,
43
+ defaultRpc: 'https://optimism-rpc.publicnode.com',
44
+ },
45
+ 'optimism-sepolia': {
46
+ chainId: 11155420,
47
+ ...OP_STACK_PREDEPLOY,
48
+ defaultRpc: 'https://optimism-sepolia-rpc.publicnode.com',
49
+ },
50
+ arbitrum: {
51
+ chainId: 42161,
52
+ eas: '0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458',
53
+ schemaRegistry: '0xA310da9c5B885E7fb3fbA9D66E9Ba6Df512b78eB',
54
+ defaultRpc: 'https://arbitrum-one-rpc.publicnode.com',
55
+ },
56
+ 'arbitrum-sepolia': {
57
+ chainId: 421614,
58
+ eas: '0xaEF4103A04090071165F78D45D83A0C0782c2B2a',
59
+ schemaRegistry: '0x55D26f9ae0203EF95494AE4C170eD35f4Cf77797',
60
+ defaultRpc: 'https://arbitrum-sepolia-rpc.publicnode.com',
61
+ },
62
+ polygon: {
63
+ chainId: 137,
64
+ eas: '0x5E634ef5355f45A855d02D66eCD687b1502AF790',
65
+ schemaRegistry: '0x7876EEF51A891E737AF8ba5A5E0f0Fd29073D5a7',
66
+ defaultRpc: 'https://polygon-bor-rpc.publicnode.com',
67
+ },
68
+ scroll: {
69
+ chainId: 534352,
70
+ eas: '0xC47300428b6AD2c7D03BB76D05A176058b47E6B0',
71
+ schemaRegistry: '0xD2CDF46556543316e7D34e8eDc4624e2bB95e3B6',
72
+ defaultRpc: 'https://scroll-rpc.publicnode.com',
73
+ },
74
+ linea: {
75
+ chainId: 59144,
76
+ eas: '0xaEF4103A04090071165F78D45D83A0C0782c2B2a',
77
+ schemaRegistry: '0x55D26f9ae0203EF95494AE4C170eD35f4Cf77797',
78
+ defaultRpc: 'https://linea-rpc.publicnode.com',
79
+ },
80
+ celo: {
81
+ chainId: 42220,
82
+ eas: '0x72E1d8ccf5a114EA560D5Fd383f1ab3c0C5C1d50',
83
+ schemaRegistry: '0x5ece93bE0d0D52EeD561Fdbb32e240b43C2CAb6e',
84
+ defaultRpc: 'https://celo-rpc.publicnode.com',
85
+ },
86
+ };
87
+
88
+ export function getChainConfig(name: string): ChainConfig {
89
+ const config = CHAIN_CONFIGS[name];
90
+ if (!config) {
91
+ const available = Object.keys(CHAIN_CONFIGS).join(', ');
92
+ throw new Error(`Unknown chain "${name}". Available chains: ${available}`);
93
+ }
94
+ return config;
95
+ }
96
+
97
+ export function listChains(): string[] {
98
+ return Object.keys(CHAIN_CONFIGS);
99
+ }