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.
- package/.claude/settings.local.json +9 -0
- package/README.md +195 -0
- package/dist/index.js +31401 -0
- package/dist/index.js.map +1 -0
- package/manual-test/package-lock.json +4483 -0
- package/manual-test/package.json +15 -0
- package/package.json +40 -0
- package/src/__tests__/chains.test.ts +82 -0
- package/src/__tests__/clear-key.test.ts +40 -0
- package/src/__tests__/client.test.ts +168 -0
- package/src/__tests__/commands/attest.test.ts +203 -0
- package/src/__tests__/commands/get-attestation.test.ts +164 -0
- package/src/__tests__/commands/multi-attest.test.ts +166 -0
- package/src/__tests__/commands/multi-revoke.test.ts +114 -0
- package/src/__tests__/commands/multi-timestamp.test.ts +88 -0
- package/src/__tests__/commands/offchain-attest.test.ts +217 -0
- package/src/__tests__/commands/query-attestation.test.ts +84 -0
- package/src/__tests__/commands/query-attestations.test.ts +156 -0
- package/src/__tests__/commands/query-schema.test.ts +62 -0
- package/src/__tests__/commands/query-schemas.test.ts +110 -0
- package/src/__tests__/commands/revoke.test.ts +86 -0
- package/src/__tests__/commands/schema-get.test.ts +66 -0
- package/src/__tests__/commands/schema-register.test.ts +94 -0
- package/src/__tests__/commands/timestamp.test.ts +78 -0
- package/src/__tests__/config.test.ts +103 -0
- package/src/__tests__/graphql.test.ts +148 -0
- package/src/__tests__/integration/graphql-live.test.ts +103 -0
- package/src/__tests__/integration/offchain-signing.test.ts +252 -0
- package/src/__tests__/integration/schema-encoder.test.ts +131 -0
- package/src/__tests__/output.test.ts +138 -0
- package/src/__tests__/set-key.test.ts +58 -0
- package/src/__tests__/stdin.test.ts +15 -0
- package/src/chains.ts +99 -0
- package/src/client.ts +53 -0
- package/src/commands/attest.ts +73 -0
- package/src/commands/clear-key.ts +15 -0
- package/src/commands/get-attestation.ts +58 -0
- package/src/commands/multi-attest.ts +75 -0
- package/src/commands/multi-revoke.ts +60 -0
- package/src/commands/multi-timestamp.ts +43 -0
- package/src/commands/offchain-attest.ts +78 -0
- package/src/commands/query-attestation.ts +31 -0
- package/src/commands/query-attestations.ts +57 -0
- package/src/commands/query-schema.ts +24 -0
- package/src/commands/query-schemas.ts +35 -0
- package/src/commands/revoke.ts +48 -0
- package/src/commands/schema-get.ts +30 -0
- package/src/commands/schema-register.ts +49 -0
- package/src/commands/set-key.ts +19 -0
- package/src/commands/timestamp.ts +35 -0
- package/src/config.ts +41 -0
- package/src/graphql.ts +136 -0
- package/src/index.ts +74 -0
- package/src/output.ts +50 -0
- package/src/stdin.ts +15 -0
- package/src/validation.ts +15 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +21 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockGetAttestation = vi.fn();
|
|
4
|
+
const mockGetSchema = vi.fn();
|
|
5
|
+
const mockClient = {
|
|
6
|
+
eas: { getAttestation: mockGetAttestation },
|
|
7
|
+
schemaRegistry: { getSchema: mockGetSchema },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
vi.mock('../../client.js', () => ({
|
|
11
|
+
createReadOnlyEASClient: vi.fn(() => mockClient),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('../../output.js', () => ({
|
|
15
|
+
output: vi.fn(),
|
|
16
|
+
handleError: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../../validation.js', () => ({
|
|
20
|
+
validateBytes32: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const mockDecodeData = vi.fn();
|
|
24
|
+
|
|
25
|
+
vi.mock('@ethereum-attestation-service/eas-sdk', () => ({
|
|
26
|
+
SchemaEncoder: class MockSchemaEncoder {
|
|
27
|
+
decodeData = mockDecodeData;
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import { getAttestationCommand } from '../../commands/get-attestation.js';
|
|
32
|
+
import { output, handleError } from '../../output.js';
|
|
33
|
+
|
|
34
|
+
const mockAttestation = {
|
|
35
|
+
uid: '0xuid',
|
|
36
|
+
schema: '0xschema',
|
|
37
|
+
attester: '0xattester',
|
|
38
|
+
recipient: '0xrecipient',
|
|
39
|
+
refUID: '0xref',
|
|
40
|
+
revocable: true,
|
|
41
|
+
revocationTime: 0n,
|
|
42
|
+
expirationTime: 0n,
|
|
43
|
+
time: 1700000000n,
|
|
44
|
+
data: '0xdata',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('get-attestation command', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
mockGetAttestation.mockResolvedValue(mockAttestation);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
async function runCommand(args: string[]) {
|
|
54
|
+
await getAttestationCommand.parseAsync(['node', 'test', ...args]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('gets attestation by uid', async () => {
|
|
58
|
+
await runCommand(['-u', '0xuid']);
|
|
59
|
+
|
|
60
|
+
expect(mockGetAttestation).toHaveBeenCalledWith('0xuid');
|
|
61
|
+
expect(output).toHaveBeenCalledWith({
|
|
62
|
+
success: true,
|
|
63
|
+
data: expect.objectContaining({
|
|
64
|
+
uid: '0xuid',
|
|
65
|
+
schema: '0xschema',
|
|
66
|
+
attester: '0xattester',
|
|
67
|
+
recipient: '0xrecipient',
|
|
68
|
+
revocationTime: 0,
|
|
69
|
+
expirationTime: 0,
|
|
70
|
+
time: 1700000000,
|
|
71
|
+
data: '0xdata',
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('decodes data when --decode is provided with schema string', async () => {
|
|
77
|
+
mockDecodeData.mockReturnValue([
|
|
78
|
+
{ name: 'score', type: 'uint256', value: { value: 100n } },
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
await runCommand(['-u', '0xuid', '--decode', 'uint256 score']);
|
|
82
|
+
|
|
83
|
+
expect(mockGetSchema).not.toHaveBeenCalled();
|
|
84
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
85
|
+
expect(outputCall.data.decodedData).toEqual([
|
|
86
|
+
{ name: 'score', type: 'uint256', value: '100' },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('auto-fetches schema when --decode is passed without value', async () => {
|
|
91
|
+
mockGetSchema.mockResolvedValue({ schema: 'uint256 score', uid: '0xschema', resolver: '0x0', revocable: true });
|
|
92
|
+
mockDecodeData.mockReturnValue([
|
|
93
|
+
{ name: 'score', type: 'uint256', value: { value: 42n } },
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
await runCommand(['-u', '0xuid', '--decode']);
|
|
97
|
+
|
|
98
|
+
expect(mockGetSchema).toHaveBeenCalledWith({ uid: '0xschema' });
|
|
99
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
100
|
+
expect(outputCall.data.decodedData).toEqual([
|
|
101
|
+
{ name: 'score', type: 'uint256', value: '42' },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('adds decodeError when auto-fetch schema fails', async () => {
|
|
106
|
+
mockGetSchema.mockRejectedValue(new Error('schema not found'));
|
|
107
|
+
|
|
108
|
+
await runCommand(['-u', '0xuid', '--decode']);
|
|
109
|
+
|
|
110
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
111
|
+
expect(outputCall.data.decodeError).toBe('schema not found');
|
|
112
|
+
expect(outputCall.data.decodedData).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('converts bigint values to strings in decoded data', async () => {
|
|
116
|
+
mockDecodeData.mockReturnValue([
|
|
117
|
+
{ name: 'amount', type: 'uint256', value: { value: 999999999999999999n } },
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
await runCommand(['-u', '0xuid', '--decode', 'uint256 amount']);
|
|
121
|
+
|
|
122
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
123
|
+
expect(outputCall.data.decodedData[0].value).toBe('999999999999999999');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('passes through non-bigint decoded values', async () => {
|
|
127
|
+
mockDecodeData.mockReturnValue([
|
|
128
|
+
{ name: 'label', type: 'string', value: { value: 'hello' } },
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
await runCommand(['-u', '0xuid', '--decode', 'string label']);
|
|
132
|
+
|
|
133
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
134
|
+
expect(outputCall.data.decodedData[0].value).toBe('hello');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('adds decodeError on decode failure instead of throwing', async () => {
|
|
138
|
+
mockDecodeData.mockImplementation(() => {
|
|
139
|
+
throw new Error('decode failed');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await runCommand(['-u', '0xuid', '--decode', 'bad schema']);
|
|
143
|
+
|
|
144
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
145
|
+
expect(outputCall.data.decodeError).toBe('decode failed');
|
|
146
|
+
expect(outputCall.data.decodedData).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('passes SDK errors to handleError', async () => {
|
|
150
|
+
mockGetAttestation.mockRejectedValue(new Error('network error'));
|
|
151
|
+
await runCommand(['-u', '0xuid']);
|
|
152
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
153
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
154
|
+
expect(err.message).toBe('network error');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not include decodedData when --decode not provided', async () => {
|
|
158
|
+
await runCommand(['-u', '0xuid']);
|
|
159
|
+
|
|
160
|
+
const outputCall = (output as any).mock.calls[0][0];
|
|
161
|
+
expect(outputCall.data.decodedData).toBeUndefined();
|
|
162
|
+
expect(outputCall.data.decodeError).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(50000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockMultiAttest = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
eas: { multiAttest: mockMultiAttest },
|
|
9
|
+
address: '0xAttester',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
vi.mock('../../client.js', () => ({
|
|
13
|
+
createEASClient: vi.fn(() => mockClient),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../../output.js', () => ({
|
|
17
|
+
output: vi.fn(),
|
|
18
|
+
handleError: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('../../stdin.js', () => ({
|
|
22
|
+
resolveInput: vi.fn((v: string) => Promise.resolve(v)),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const mockEncodeData = vi.fn().mockReturnValue('0xencoded');
|
|
26
|
+
|
|
27
|
+
vi.mock('@ethereum-attestation-service/eas-sdk', () => ({
|
|
28
|
+
SchemaEncoder: class MockSchemaEncoder {
|
|
29
|
+
encodeData = mockEncodeData;
|
|
30
|
+
},
|
|
31
|
+
NO_EXPIRATION: 0n,
|
|
32
|
+
ZERO_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import { multiAttestCommand } from '../../commands/multi-attest.js';
|
|
36
|
+
import { output, handleError } from '../../output.js';
|
|
37
|
+
|
|
38
|
+
describe('multi-attest command', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
mockWait.mockImplementation(async () => {
|
|
42
|
+
mockTx.receipt = { hash: '0xtxhash456' };
|
|
43
|
+
return ['0xuid1', '0xuid2'];
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function runCommand(args: string[]) {
|
|
48
|
+
await multiAttestCommand.parseAsync(['node', 'test', ...args]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('processes single attestation input', async () => {
|
|
52
|
+
const input = JSON.stringify([
|
|
53
|
+
{ schema: '0xschema1', data: [{ name: 'x', type: 'uint8', value: '1' }] },
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
await runCommand(['-i', input]);
|
|
57
|
+
|
|
58
|
+
expect(mockEncodeData).toHaveBeenCalledWith([
|
|
59
|
+
{ name: 'x', type: 'uint8', value: '1' },
|
|
60
|
+
]);
|
|
61
|
+
expect(mockMultiAttest).toHaveBeenCalledWith([
|
|
62
|
+
{
|
|
63
|
+
schema: '0xschema1',
|
|
64
|
+
data: [
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
recipient: '0x0000000000000000000000000000000000000000',
|
|
67
|
+
expirationTime: 0n,
|
|
68
|
+
revocable: true,
|
|
69
|
+
refUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
70
|
+
data: '0xencoded',
|
|
71
|
+
value: 0n,
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('groups attestations by schema', async () => {
|
|
79
|
+
const input = JSON.stringify([
|
|
80
|
+
{ schema: '0xschemaA', data: [{ name: 'x', type: 'uint8', value: '1' }] },
|
|
81
|
+
{ schema: '0xschemaB', data: [{ name: 'y', type: 'uint8', value: '2' }] },
|
|
82
|
+
{ schema: '0xschemaA', data: [{ name: 'x', type: 'uint8', value: '3' }] },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
await runCommand(['-i', input]);
|
|
86
|
+
|
|
87
|
+
const groups = mockMultiAttest.mock.calls[0][0];
|
|
88
|
+
expect(groups).toHaveLength(2);
|
|
89
|
+
const groupA = groups.find((g: any) => g.schema === '0xschemaA');
|
|
90
|
+
expect(groupA.data).toHaveLength(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles invalid JSON in --input', async () => {
|
|
94
|
+
await runCommand(['-i', 'not-json']);
|
|
95
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
96
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
97
|
+
expect(err.message).toContain('Invalid JSON in --input');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('uses custom values when provided', async () => {
|
|
101
|
+
const input = JSON.stringify([
|
|
102
|
+
{
|
|
103
|
+
schema: '0xschema1',
|
|
104
|
+
recipient: '0xCustomRecipient',
|
|
105
|
+
expirationTime: '1700000000',
|
|
106
|
+
revocable: false,
|
|
107
|
+
refUID: '0xref',
|
|
108
|
+
value: '500',
|
|
109
|
+
data: [{ name: 'x', type: 'uint8', value: '1' }],
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
await runCommand(['-i', input]);
|
|
114
|
+
|
|
115
|
+
const attestData = mockMultiAttest.mock.calls[0][0][0].data[0];
|
|
116
|
+
expect(attestData.recipient).toBe('0xCustomRecipient');
|
|
117
|
+
expect(attestData.expirationTime).toBe(1700000000n);
|
|
118
|
+
expect(attestData.revocable).toBe(false);
|
|
119
|
+
expect(attestData.refUID).toBe('0xref');
|
|
120
|
+
expect(attestData.value).toBe(500n);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('passes SDK errors to handleError', async () => {
|
|
124
|
+
mockMultiAttest.mockRejectedValueOnce(new Error('tx failed'));
|
|
125
|
+
const input = JSON.stringify([
|
|
126
|
+
{ schema: '0xschema1', data: [{ name: 'x', type: 'uint8', value: '1' }] },
|
|
127
|
+
]);
|
|
128
|
+
await runCommand(['-i', input]);
|
|
129
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
130
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
131
|
+
expect(err.message).toBe('tx failed');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('outputs uids, count, and txHash on success', async () => {
|
|
135
|
+
const input = JSON.stringify([
|
|
136
|
+
{ schema: '0xschema1', data: [{ name: 'x', type: 'uint8', value: '1' }] },
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
await runCommand(['-i', input]);
|
|
140
|
+
|
|
141
|
+
expect(output).toHaveBeenCalledWith({
|
|
142
|
+
success: true,
|
|
143
|
+
data: {
|
|
144
|
+
uids: ['0xuid1', '0xuid2'],
|
|
145
|
+
count: 2,
|
|
146
|
+
txHash: '0xtxhash456',
|
|
147
|
+
chain: 'ethereum',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
153
|
+
const input = JSON.stringify([
|
|
154
|
+
{ schema: '0xschema1', data: [{ name: 'x', type: 'uint8', value: '1' }] },
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
await runCommand(['-i', input, '--dry-run']);
|
|
158
|
+
|
|
159
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
160
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
161
|
+
expect(output).toHaveBeenCalledWith({
|
|
162
|
+
success: true,
|
|
163
|
+
data: { dryRun: true, estimatedGas: '50000', chain: 'ethereum' },
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(40000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockMultiRevoke = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
eas: { multiRevoke: mockMultiRevoke },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
vi.mock('../../client.js', () => ({
|
|
12
|
+
createEASClient: vi.fn(() => mockClient),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../../output.js', () => ({
|
|
16
|
+
output: vi.fn(),
|
|
17
|
+
handleError: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../../stdin.js', () => ({
|
|
21
|
+
resolveInput: vi.fn((v: string) => Promise.resolve(v)),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { multiRevokeCommand } from '../../commands/multi-revoke.js';
|
|
25
|
+
import { output, handleError } from '../../output.js';
|
|
26
|
+
|
|
27
|
+
describe('multi-revoke command', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
mockWait.mockImplementation(async () => {
|
|
31
|
+
mockTx.receipt = { hash: '0xmultirevokehash' };
|
|
32
|
+
return undefined;
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function runCommand(args: string[]) {
|
|
37
|
+
await multiRevokeCommand.parseAsync(['node', 'test', ...args]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('revokes multiple attestations grouped by schema', async () => {
|
|
41
|
+
const input = JSON.stringify([
|
|
42
|
+
{ schema: '0xschemaA', uid: '0xuid1' },
|
|
43
|
+
{ schema: '0xschemaB', uid: '0xuid2' },
|
|
44
|
+
{ schema: '0xschemaA', uid: '0xuid3' },
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
await runCommand(['-i', input]);
|
|
48
|
+
|
|
49
|
+
const groups = mockMultiRevoke.mock.calls[0][0];
|
|
50
|
+
expect(groups).toHaveLength(2);
|
|
51
|
+
const groupA = groups.find((g: any) => g.schema === '0xschemaA');
|
|
52
|
+
expect(groupA.data).toHaveLength(2);
|
|
53
|
+
expect(groupA.data[0]).toEqual({ uid: '0xuid1', value: 0n });
|
|
54
|
+
expect(groupA.data[1]).toEqual({ uid: '0xuid3', value: 0n });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('outputs revoked count and txHash', async () => {
|
|
58
|
+
const input = JSON.stringify([
|
|
59
|
+
{ schema: '0xschema', uid: '0xuid1' },
|
|
60
|
+
{ schema: '0xschema', uid: '0xuid2' },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
await runCommand(['-i', input]);
|
|
64
|
+
|
|
65
|
+
expect(output).toHaveBeenCalledWith({
|
|
66
|
+
success: true,
|
|
67
|
+
data: {
|
|
68
|
+
revoked: 2,
|
|
69
|
+
txHash: '0xmultirevokehash',
|
|
70
|
+
chain: 'ethereum',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('handles invalid JSON in --input', async () => {
|
|
76
|
+
await runCommand(['-i', 'not-json']);
|
|
77
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
78
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
79
|
+
expect(err.message).toContain('Invalid JSON in --input');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('passes custom value as BigInt', async () => {
|
|
83
|
+
const input = JSON.stringify([
|
|
84
|
+
{ schema: '0xschema', uid: '0xuid1', value: '500' },
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
await runCommand(['-i', input]);
|
|
88
|
+
|
|
89
|
+
const data = mockMultiRevoke.mock.calls[0][0][0].data[0];
|
|
90
|
+
expect(data.value).toBe(500n);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('passes SDK errors to handleError', async () => {
|
|
94
|
+
mockMultiRevoke.mockRejectedValueOnce(new Error('tx failed'));
|
|
95
|
+
const input = JSON.stringify([{ schema: '0xschema', uid: '0xuid1' }]);
|
|
96
|
+
await runCommand(['-i', input]);
|
|
97
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
98
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
99
|
+
expect(err.message).toBe('tx failed');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
103
|
+
const input = JSON.stringify([{ schema: '0xschema', uid: '0xuid1' }]);
|
|
104
|
+
|
|
105
|
+
await runCommand(['-i', input, '--dry-run']);
|
|
106
|
+
|
|
107
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
108
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
109
|
+
expect(output).toHaveBeenCalledWith({
|
|
110
|
+
success: true,
|
|
111
|
+
data: { dryRun: true, estimatedGas: '40000', chain: 'ethereum' },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(35000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockMultiTimestamp = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
eas: { multiTimestamp: mockMultiTimestamp },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
vi.mock('../../client.js', () => ({
|
|
12
|
+
createEASClient: vi.fn(() => mockClient),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('../../output.js', () => ({
|
|
16
|
+
output: vi.fn(),
|
|
17
|
+
handleError: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../../stdin.js', () => ({
|
|
21
|
+
resolveInput: vi.fn((v: string) => Promise.resolve(v)),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { multiTimestampCommand } from '../../commands/multi-timestamp.js';
|
|
25
|
+
import { output, handleError } from '../../output.js';
|
|
26
|
+
|
|
27
|
+
describe('multi-timestamp command', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
mockWait.mockImplementation(async () => {
|
|
31
|
+
mockTx.receipt = { hash: '0xmultitshash' };
|
|
32
|
+
return [1700000000n, 1700000001n];
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function runCommand(args: string[]) {
|
|
37
|
+
await multiTimestampCommand.parseAsync(['node', 'test', ...args]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('timestamps multiple data items', async () => {
|
|
41
|
+
const data = JSON.stringify(['0xdata1', '0xdata2']);
|
|
42
|
+
|
|
43
|
+
await runCommand(['-d', data]);
|
|
44
|
+
|
|
45
|
+
expect(mockMultiTimestamp).toHaveBeenCalledWith(['0xdata1', '0xdata2']);
|
|
46
|
+
expect(output).toHaveBeenCalledWith({
|
|
47
|
+
success: true,
|
|
48
|
+
data: {
|
|
49
|
+
timestamps: ['1700000000', '1700000001'],
|
|
50
|
+
count: 2,
|
|
51
|
+
txHash: '0xmultitshash',
|
|
52
|
+
chain: 'ethereum',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('handles invalid JSON in --data', async () => {
|
|
58
|
+
await runCommand(['-d', 'not-json']);
|
|
59
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
60
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
61
|
+
expect(err.message).toContain('Invalid JSON in --data');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('passes SDK errors to handleError', async () => {
|
|
65
|
+
mockMultiTimestamp.mockRejectedValueOnce(new Error('tx failed'));
|
|
66
|
+
await runCommand(['-d', '["0xdata1"]']);
|
|
67
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
68
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
69
|
+
expect(err.message).toBe('tx failed');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses specified chain', async () => {
|
|
73
|
+
const { createEASClient } = await import('../../client.js');
|
|
74
|
+
await runCommand(['-d', '["0xdata1"]', '-c', 'base']);
|
|
75
|
+
expect(createEASClient).toHaveBeenCalledWith('base', undefined);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
79
|
+
await runCommand(['-d', '["0xdata1"]', '--dry-run']);
|
|
80
|
+
|
|
81
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
82
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
83
|
+
expect(output).toHaveBeenCalledWith({
|
|
84
|
+
success: true,
|
|
85
|
+
data: { dryRun: true, estimatedGas: '35000', chain: 'ethereum' },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|