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,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(30000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockRevoke = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
eas: { revoke: mockRevoke },
|
|
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('../../validation.js', () => ({
|
|
21
|
+
validateBytes32: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { revokeCommand } from '../../commands/revoke.js';
|
|
25
|
+
import { output, handleError } from '../../output.js';
|
|
26
|
+
|
|
27
|
+
describe('revoke command', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
mockWait.mockImplementation(async () => {
|
|
31
|
+
mockTx.receipt = { hash: '0xrevokehash' };
|
|
32
|
+
return undefined;
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function runCommand(args: string[]) {
|
|
37
|
+
await revokeCommand.parseAsync(['node', 'test', ...args]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('revokes attestation successfully', async () => {
|
|
41
|
+
await runCommand(['-s', '0xschema', '-u', '0xuid']);
|
|
42
|
+
|
|
43
|
+
expect(mockRevoke).toHaveBeenCalledWith({
|
|
44
|
+
schema: '0xschema',
|
|
45
|
+
data: { uid: '0xuid', value: 0n },
|
|
46
|
+
});
|
|
47
|
+
expect(output).toHaveBeenCalledWith({
|
|
48
|
+
success: true,
|
|
49
|
+
data: {
|
|
50
|
+
revoked: true,
|
|
51
|
+
uid: '0xuid',
|
|
52
|
+
txHash: '0xrevokehash',
|
|
53
|
+
schema: '0xschema',
|
|
54
|
+
chain: 'ethereum',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('passes SDK errors to handleError', async () => {
|
|
60
|
+
mockRevoke.mockRejectedValueOnce(new Error('revocation failed'));
|
|
61
|
+
await runCommand(['-s', '0xschema', '-u', '0xuid']);
|
|
62
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
63
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
64
|
+
expect(err.message).toBe('revocation failed');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('passes custom value as BigInt', async () => {
|
|
68
|
+
await runCommand(['-s', '0xschema', '-u', '0xuid', '--value', '1000']);
|
|
69
|
+
|
|
70
|
+
expect(mockRevoke).toHaveBeenCalledWith({
|
|
71
|
+
schema: '0xschema',
|
|
72
|
+
data: { uid: '0xuid', value: 1000n },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
77
|
+
await runCommand(['-s', '0xschema', '-u', '0xuid', '--dry-run']);
|
|
78
|
+
|
|
79
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
80
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
81
|
+
expect(output).toHaveBeenCalledWith({
|
|
82
|
+
success: true,
|
|
83
|
+
data: { dryRun: true, estimatedGas: '30000', chain: 'ethereum' },
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockGetSchema = vi.fn().mockResolvedValue({
|
|
4
|
+
uid: '0xschemauid',
|
|
5
|
+
schema: 'uint256 score',
|
|
6
|
+
resolver: '0x0000000000000000000000000000000000000000',
|
|
7
|
+
revocable: true,
|
|
8
|
+
});
|
|
9
|
+
const mockClient = {
|
|
10
|
+
schemaRegistry: { getSchema: mockGetSchema },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
vi.mock('../../client.js', () => ({
|
|
14
|
+
createReadOnlyEASClient: vi.fn(() => mockClient),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../../output.js', () => ({
|
|
18
|
+
output: vi.fn(),
|
|
19
|
+
handleError: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../../validation.js', () => ({
|
|
23
|
+
validateBytes32: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { schemaGetCommand } from '../../commands/schema-get.js';
|
|
27
|
+
import { createReadOnlyEASClient } from '../../client.js';
|
|
28
|
+
import { output } from '../../output.js';
|
|
29
|
+
|
|
30
|
+
describe('schema-get command', () => {
|
|
31
|
+
beforeEach(() => vi.clearAllMocks());
|
|
32
|
+
|
|
33
|
+
async function runCommand(args: string[]) {
|
|
34
|
+
await schemaGetCommand.parseAsync(['node', 'test', ...args]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('gets schema by uid', async () => {
|
|
38
|
+
await runCommand(['-u', '0xschemauid']);
|
|
39
|
+
|
|
40
|
+
expect(createReadOnlyEASClient).toHaveBeenCalledWith('ethereum', undefined);
|
|
41
|
+
expect(mockGetSchema).toHaveBeenCalledWith({ uid: '0xschemauid' });
|
|
42
|
+
expect(output).toHaveBeenCalledWith({
|
|
43
|
+
success: true,
|
|
44
|
+
data: {
|
|
45
|
+
uid: '0xschemauid',
|
|
46
|
+
schema: 'uint256 score',
|
|
47
|
+
resolver: '0x0000000000000000000000000000000000000000',
|
|
48
|
+
revocable: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('passes SDK errors to handleError', async () => {
|
|
54
|
+
mockGetSchema.mockRejectedValueOnce(new Error('contract not found'));
|
|
55
|
+
await runCommand(['-u', '0xschemauid']);
|
|
56
|
+
const { handleError } = await import('../../output.js');
|
|
57
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
58
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
59
|
+
expect(err.message).toBe('contract not found');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('uses specified chain', async () => {
|
|
63
|
+
await runCommand(['-u', '0xschemauid', '-c', 'base']);
|
|
64
|
+
expect(createReadOnlyEASClient).toHaveBeenCalledWith('base', undefined);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(45000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockRegister = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
schemaRegistry: { register: mockRegister },
|
|
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('../../validation.js', () => ({
|
|
21
|
+
validateAddress: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { schemaRegisterCommand } from '../../commands/schema-register.js';
|
|
25
|
+
import { output } from '../../output.js';
|
|
26
|
+
|
|
27
|
+
describe('schema-register command', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
mockWait.mockImplementation(async () => {
|
|
31
|
+
mockTx.receipt = { hash: '0xreghash' };
|
|
32
|
+
return '0xschemauid';
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function runCommand(args: string[]) {
|
|
37
|
+
await schemaRegisterCommand.parseAsync(['node', 'test', ...args]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('registers schema with defaults', async () => {
|
|
41
|
+
await runCommand(['-s', 'uint256 score, string name']);
|
|
42
|
+
|
|
43
|
+
expect(mockRegister).toHaveBeenCalledWith({
|
|
44
|
+
schema: 'uint256 score, string name',
|
|
45
|
+
resolverAddress: '0x0000000000000000000000000000000000000000',
|
|
46
|
+
revocable: true,
|
|
47
|
+
});
|
|
48
|
+
expect(output).toHaveBeenCalledWith({
|
|
49
|
+
success: true,
|
|
50
|
+
data: {
|
|
51
|
+
uid: '0xschemauid',
|
|
52
|
+
txHash: '0xreghash',
|
|
53
|
+
schema: 'uint256 score, string name',
|
|
54
|
+
resolver: '0x0000000000000000000000000000000000000000',
|
|
55
|
+
revocable: true,
|
|
56
|
+
chain: 'ethereum',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('passes custom resolver', async () => {
|
|
62
|
+
await runCommand(['-s', 'uint8 x', '--resolver', '0xResolver']);
|
|
63
|
+
expect(mockRegister).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining({ resolverAddress: '0xResolver' })
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('passes SDK errors to handleError', async () => {
|
|
69
|
+
mockRegister.mockRejectedValueOnce(new Error('tx reverted'));
|
|
70
|
+
await runCommand(['-s', 'uint8 x']);
|
|
71
|
+
const { handleError } = await import('../../output.js');
|
|
72
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
73
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
74
|
+
expect(err.message).toBe('tx reverted');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles --no-revocable', async () => {
|
|
78
|
+
await runCommand(['-s', 'uint8 x', '--no-revocable']);
|
|
79
|
+
expect(mockRegister).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({ revocable: false })
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
85
|
+
await runCommand(['-s', 'uint8 x', '--dry-run']);
|
|
86
|
+
|
|
87
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
88
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
89
|
+
expect(output).toHaveBeenCalledWith({
|
|
90
|
+
success: true,
|
|
91
|
+
data: { dryRun: true, estimatedGas: '45000', chain: 'ethereum' },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(25000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockTimestamp = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
eas: { timestamp: mockTimestamp },
|
|
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
|
+
import { timestampCommand } from '../../commands/timestamp.js';
|
|
21
|
+
import { output } from '../../output.js';
|
|
22
|
+
|
|
23
|
+
describe('timestamp command', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
mockWait.mockImplementation(async () => {
|
|
27
|
+
mockTx.receipt = { hash: '0xtshash' };
|
|
28
|
+
return 1700000000n;
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function runCommand(args: string[]) {
|
|
33
|
+
await timestampCommand.parseAsync(['node', 'test', ...args]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('timestamps data on-chain', async () => {
|
|
37
|
+
await runCommand(['-d', '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890']);
|
|
38
|
+
|
|
39
|
+
expect(mockTimestamp).toHaveBeenCalledWith(
|
|
40
|
+
'0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'
|
|
41
|
+
);
|
|
42
|
+
expect(output).toHaveBeenCalledWith({
|
|
43
|
+
success: true,
|
|
44
|
+
data: {
|
|
45
|
+
timestamp: '1700000000',
|
|
46
|
+
txHash: '0xtshash',
|
|
47
|
+
data: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
|
|
48
|
+
chain: 'ethereum',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('passes SDK errors to handleError', async () => {
|
|
54
|
+
mockTimestamp.mockRejectedValueOnce(new Error('nonce too low'));
|
|
55
|
+
await runCommand(['-d', '0x1234']);
|
|
56
|
+
const { handleError } = await import('../../output.js');
|
|
57
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
58
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
59
|
+
expect(err.message).toBe('nonce too low');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('uses specified chain', async () => {
|
|
63
|
+
const { createEASClient } = await import('../../client.js');
|
|
64
|
+
await runCommand(['-d', '0x1234', '-c', 'base']);
|
|
65
|
+
expect(createEASClient).toHaveBeenCalledWith('base', undefined);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
69
|
+
await runCommand(['-d', '0x1234', '--dry-run']);
|
|
70
|
+
|
|
71
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
72
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
73
|
+
expect(output).toHaveBeenCalledWith({
|
|
74
|
+
success: true,
|
|
75
|
+
data: { dryRun: true, estimatedGas: '25000', chain: 'ethereum' },
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import {
|
|
5
|
+
getConfigPath,
|
|
6
|
+
readConfig,
|
|
7
|
+
getStoredPrivateKey,
|
|
8
|
+
setStoredPrivateKey,
|
|
9
|
+
clearStoredPrivateKey,
|
|
10
|
+
} from '../config.js';
|
|
11
|
+
|
|
12
|
+
vi.mock('fs', () => ({
|
|
13
|
+
readFileSync: vi.fn(),
|
|
14
|
+
writeFileSync: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('os', () => ({
|
|
18
|
+
homedir: vi.fn(() => '/mock/home'),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('config module', () => {
|
|
22
|
+
beforeEach(() => vi.clearAllMocks());
|
|
23
|
+
|
|
24
|
+
describe('getConfigPath', () => {
|
|
25
|
+
it('returns ~/.eas-cli', () => {
|
|
26
|
+
expect(getConfigPath()).toBe('/mock/home/.eas-cli');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('readConfig', () => {
|
|
31
|
+
it('returns parsed config when file exists', () => {
|
|
32
|
+
vi.mocked(readFileSync).mockReturnValue('{"privateKey":"0xabc"}');
|
|
33
|
+
expect(readConfig()).toEqual({ privateKey: '0xabc' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns empty object when file does not exist', () => {
|
|
37
|
+
vi.mocked(readFileSync).mockImplementation(() => { throw new Error('ENOENT'); });
|
|
38
|
+
expect(readConfig()).toEqual({});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns empty object when file contains malformed JSON', () => {
|
|
42
|
+
vi.mocked(readFileSync).mockReturnValue('not valid json{{{');
|
|
43
|
+
expect(readConfig()).toEqual({});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getStoredPrivateKey', () => {
|
|
48
|
+
it('returns the stored key', () => {
|
|
49
|
+
vi.mocked(readFileSync).mockReturnValue('{"privateKey":"0xdef"}');
|
|
50
|
+
expect(getStoredPrivateKey()).toBe('0xdef');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns undefined when no key is stored', () => {
|
|
54
|
+
vi.mocked(readFileSync).mockReturnValue('{}');
|
|
55
|
+
expect(getStoredPrivateKey()).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('setStoredPrivateKey', () => {
|
|
60
|
+
it('stores key with 0x prefix', () => {
|
|
61
|
+
vi.mocked(readFileSync).mockReturnValue('{}');
|
|
62
|
+
setStoredPrivateKey('0xabc123');
|
|
63
|
+
expect(writeFileSync).toHaveBeenCalledWith(
|
|
64
|
+
'/mock/home/.eas-cli',
|
|
65
|
+
JSON.stringify({ privateKey: '0xabc123' }, null, 2) + '\n',
|
|
66
|
+
{ mode: 0o600 },
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('adds 0x prefix when missing', () => {
|
|
71
|
+
vi.mocked(readFileSync).mockReturnValue('{}');
|
|
72
|
+
setStoredPrivateKey('abc123');
|
|
73
|
+
expect(writeFileSync).toHaveBeenCalledWith(
|
|
74
|
+
'/mock/home/.eas-cli',
|
|
75
|
+
JSON.stringify({ privateKey: '0xabc123' }, null, 2) + '\n',
|
|
76
|
+
{ mode: 0o600 },
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('preserves other config fields', () => {
|
|
81
|
+
vi.mocked(readFileSync).mockReturnValue('{"other":"value"}');
|
|
82
|
+
setStoredPrivateKey('0xkey');
|
|
83
|
+
const written = vi.mocked(writeFileSync).mock.calls[0][1] as string;
|
|
84
|
+
expect(JSON.parse(written)).toEqual({ other: 'value', privateKey: '0xkey' });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('clearStoredPrivateKey', () => {
|
|
89
|
+
it('removes the privateKey field', () => {
|
|
90
|
+
vi.mocked(readFileSync).mockReturnValue('{"privateKey":"0xabc","other":"value"}');
|
|
91
|
+
clearStoredPrivateKey();
|
|
92
|
+
const written = vi.mocked(writeFileSync).mock.calls[0][1] as string;
|
|
93
|
+
expect(JSON.parse(written)).toEqual({ other: 'value' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('handles missing key gracefully', () => {
|
|
97
|
+
vi.mocked(readFileSync).mockReturnValue('{}');
|
|
98
|
+
clearStoredPrivateKey();
|
|
99
|
+
const written = vi.mocked(writeFileSync).mock.calls[0][1] as string;
|
|
100
|
+
expect(JSON.parse(written)).toEqual({});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { getGraphQLEndpoint, graphqlQuery, QUERIES } from '../graphql.js';
|
|
3
|
+
|
|
4
|
+
describe('graphql module', () => {
|
|
5
|
+
describe('getGraphQLEndpoint', () => {
|
|
6
|
+
it('returns ethereum endpoint', () => {
|
|
7
|
+
expect(getGraphQLEndpoint('ethereum')).toBe('https://easscan.org/graphql');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns sepolia endpoint', () => {
|
|
11
|
+
expect(getGraphQLEndpoint('sepolia')).toBe('https://sepolia.easscan.org/graphql');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns base endpoint', () => {
|
|
15
|
+
expect(getGraphQLEndpoint('base')).toBe('https://base.easscan.org/graphql');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('throws for unknown chain', () => {
|
|
19
|
+
expect(() => getGraphQLEndpoint('unknown')).toThrow('No EASScan URL for chain "unknown"');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('graphqlQuery', () => {
|
|
24
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
fetchMock = vi.fn();
|
|
28
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.unstubAllGlobals();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('sends POST request with correct body', async () => {
|
|
36
|
+
fetchMock.mockResolvedValue({
|
|
37
|
+
ok: true,
|
|
38
|
+
json: async () => ({ data: { schema: { id: '0x123' } } }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await graphqlQuery('ethereum', 'query { schema }', { id: '0x123' });
|
|
42
|
+
|
|
43
|
+
expect(fetchMock).toHaveBeenCalledWith('https://easscan.org/graphql', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/json' },
|
|
46
|
+
body: JSON.stringify({ query: 'query { schema }', variables: { id: '0x123' } }),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns data on success', async () => {
|
|
51
|
+
fetchMock.mockResolvedValue({
|
|
52
|
+
ok: true,
|
|
53
|
+
json: async () => ({ data: { schema: { id: '0x123' } } }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await graphqlQuery('ethereum', 'query { schema }');
|
|
57
|
+
expect(result).toEqual({ schema: { id: '0x123' } });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('uses empty object as default variables', async () => {
|
|
61
|
+
fetchMock.mockResolvedValue({
|
|
62
|
+
ok: true,
|
|
63
|
+
json: async () => ({ data: {} }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await graphqlQuery('ethereum', 'query { test }');
|
|
67
|
+
|
|
68
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
|
69
|
+
expect(body.variables).toEqual({});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('throws on HTTP error', async () => {
|
|
73
|
+
fetchMock.mockResolvedValue({
|
|
74
|
+
ok: false,
|
|
75
|
+
status: 500,
|
|
76
|
+
statusText: 'Internal Server Error',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await expect(graphqlQuery('ethereum', 'query { test }')).rejects.toThrow(
|
|
80
|
+
'GraphQL request failed: 500 Internal Server Error'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('throws on GraphQL errors', async () => {
|
|
85
|
+
fetchMock.mockResolvedValue({
|
|
86
|
+
ok: true,
|
|
87
|
+
json: async () => ({
|
|
88
|
+
errors: [{ message: 'Field not found' }],
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await expect(graphqlQuery('ethereum', 'query { test }')).rejects.toThrow(
|
|
93
|
+
'GraphQL error: Field not found'
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('throws for unknown chain', async () => {
|
|
98
|
+
await expect(graphqlQuery('unknown', 'query { test }')).rejects.toThrow(
|
|
99
|
+
'No EASScan URL for chain "unknown"'
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('QUERIES', () => {
|
|
105
|
+
it('getSchema queries the schema root field with required fields', () => {
|
|
106
|
+
expect(QUERIES.getSchema).toContain('$id: String!');
|
|
107
|
+
expect(QUERIES.getSchema).toMatch(/schema\s*\(\s*where:/);
|
|
108
|
+
expect(QUERIES.getSchema).toContain('schema');
|
|
109
|
+
expect(QUERIES.getSchema).toContain('creator');
|
|
110
|
+
expect(QUERIES.getSchema).toContain('resolver');
|
|
111
|
+
expect(QUERIES.getSchema).toContain('revocable');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('getAttestation queries the attestation root field with required fields', () => {
|
|
115
|
+
expect(QUERIES.getAttestation).toContain('$id: String!');
|
|
116
|
+
expect(QUERIES.getAttestation).toMatch(/attestation\s*\(\s*where:/);
|
|
117
|
+
expect(QUERIES.getAttestation).toContain('attester');
|
|
118
|
+
expect(QUERIES.getAttestation).toContain('recipient');
|
|
119
|
+
expect(QUERIES.getAttestation).toContain('decodedDataJson');
|
|
120
|
+
expect(QUERIES.getAttestation).toContain('schemaId');
|
|
121
|
+
expect(QUERIES.getAttestation).toContain('data');
|
|
122
|
+
expect(QUERIES.getAttestation).toContain('revoked');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('getAttestationsBySchema queries attestations root field with schema filter', () => {
|
|
126
|
+
expect(QUERIES.getAttestationsBySchema).toContain('$schemaId: String!');
|
|
127
|
+
expect(QUERIES.getAttestationsBySchema).toContain('$take: Int');
|
|
128
|
+
expect(QUERIES.getAttestationsBySchema).toMatch(/attestations\s*\(/);
|
|
129
|
+
expect(QUERIES.getAttestationsBySchema).toContain('schemaId');
|
|
130
|
+
expect(QUERIES.getAttestationsBySchema).toContain('decodedDataJson');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('getAttestationsByAttester queries attestations root field with attester filter', () => {
|
|
134
|
+
expect(QUERIES.getAttestationsByAttester).toContain('$attester: String!');
|
|
135
|
+
expect(QUERIES.getAttestationsByAttester).toContain('$take: Int');
|
|
136
|
+
expect(QUERIES.getAttestationsByAttester).toMatch(/attestations\s*\(/);
|
|
137
|
+
expect(QUERIES.getAttestationsByAttester).toContain('decodedDataJson');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('getSchemata queries schemata root field with creator filter', () => {
|
|
141
|
+
expect(QUERIES.getSchemata).toContain('$creator: String');
|
|
142
|
+
expect(QUERIES.getSchemata).toContain('$take: Int');
|
|
143
|
+
expect(QUERIES.getSchemata).toMatch(/schemata\s*\(/);
|
|
144
|
+
expect(QUERIES.getSchemata).toContain('schema');
|
|
145
|
+
expect(QUERIES.getSchemata).toContain('creator');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { graphqlQuery, QUERIES } from '../../graphql.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Live smoke tests against the real EASScan GraphQL API (Sepolia).
|
|
6
|
+
* These verify that our query strings and expected response shapes
|
|
7
|
+
* actually work against the real indexer — something unit tests with
|
|
8
|
+
* mocked fetch cannot prove.
|
|
9
|
+
*
|
|
10
|
+
* These hit the network so they're slower (~1-2s each).
|
|
11
|
+
*/
|
|
12
|
+
describe('GraphQL live queries (sepolia)', () => {
|
|
13
|
+
const chain = 'sepolia';
|
|
14
|
+
|
|
15
|
+
it('getSchemata returns schemas with expected fields', async () => {
|
|
16
|
+
const data = await graphqlQuery(chain, QUERIES.getSchemata, { take: 2 });
|
|
17
|
+
|
|
18
|
+
expect(data.schemata).toBeDefined();
|
|
19
|
+
expect(data.schemata.length).toBeGreaterThan(0);
|
|
20
|
+
|
|
21
|
+
const schema = data.schemata[0];
|
|
22
|
+
expect(schema).toHaveProperty('id');
|
|
23
|
+
expect(schema).toHaveProperty('schema');
|
|
24
|
+
expect(schema).toHaveProperty('creator');
|
|
25
|
+
expect(schema).toHaveProperty('revocable');
|
|
26
|
+
expect(schema).toHaveProperty('time');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('getSchema returns a single schema with expected fields', async () => {
|
|
30
|
+
// First get a schema ID to query
|
|
31
|
+
const list = await graphqlQuery(chain, QUERIES.getSchemata, { take: 1 });
|
|
32
|
+
const schemaId = list.schemata[0].id;
|
|
33
|
+
|
|
34
|
+
const data = await graphqlQuery(chain, QUERIES.getSchema, { id: schemaId });
|
|
35
|
+
|
|
36
|
+
expect(data.schema).toBeDefined();
|
|
37
|
+
expect(data.schema.id).toBe(schemaId);
|
|
38
|
+
expect(data.schema).toHaveProperty('schema');
|
|
39
|
+
expect(data.schema).toHaveProperty('creator');
|
|
40
|
+
expect(data.schema).toHaveProperty('resolver');
|
|
41
|
+
expect(data.schema).toHaveProperty('revocable');
|
|
42
|
+
expect(data.schema).toHaveProperty('txid');
|
|
43
|
+
expect(data.schema).toHaveProperty('time');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('getAttestationsBySchema returns attestations with expected fields', async () => {
|
|
47
|
+
// Use a well-known schema that has attestations on sepolia
|
|
48
|
+
const data = await graphqlQuery(chain, QUERIES.getAttestationsBySchema, {
|
|
49
|
+
schemaId: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
50
|
+
take: 1,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(data.attestations).toBeDefined();
|
|
54
|
+
expect(Array.isArray(data.attestations)).toBe(true);
|
|
55
|
+
// Zero-schema may or may not have attestations, but the shape should be valid
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('getAttestationsByAttester returns attestations array', async () => {
|
|
59
|
+
const data = await graphqlQuery(chain, QUERIES.getAttestationsByAttester, {
|
|
60
|
+
attester: '0x0000000000000000000000000000000000000000',
|
|
61
|
+
take: 1,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(data.attestations).toBeDefined();
|
|
65
|
+
expect(Array.isArray(data.attestations)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('getAttestation returns a single attestation with all expected fields', async () => {
|
|
69
|
+
// First get a real attestation ID by querying recent attestations by a known schema
|
|
70
|
+
// We use getSchemata to find a schema, then query its attestations
|
|
71
|
+
const schemaList = await graphqlQuery(chain, QUERIES.getSchemata, { take: 1 });
|
|
72
|
+
const schemaId = schemaList.schemata[0].id;
|
|
73
|
+
|
|
74
|
+
const attList = await graphqlQuery(chain, QUERIES.getAttestationsBySchema, {
|
|
75
|
+
schemaId,
|
|
76
|
+
take: 1,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (attList.attestations.length === 0) {
|
|
80
|
+
// No attestations for this schema — skip rather than fail
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const attId = attList.attestations[0].id;
|
|
85
|
+
|
|
86
|
+
const data = await graphqlQuery(chain, QUERIES.getAttestation, { id: attId });
|
|
87
|
+
|
|
88
|
+
expect(data.attestation).toBeDefined();
|
|
89
|
+
expect(data.attestation.id).toBe(attId);
|
|
90
|
+
expect(data.attestation).toHaveProperty('attester');
|
|
91
|
+
expect(data.attestation).toHaveProperty('recipient');
|
|
92
|
+
expect(data.attestation).toHaveProperty('time');
|
|
93
|
+
expect(data.attestation).toHaveProperty('expirationTime');
|
|
94
|
+
expect(data.attestation).toHaveProperty('revocationTime');
|
|
95
|
+
expect(data.attestation).toHaveProperty('revoked');
|
|
96
|
+
expect(data.attestation).toHaveProperty('revocable');
|
|
97
|
+
expect(data.attestation).toHaveProperty('schemaId');
|
|
98
|
+
expect(data.attestation).toHaveProperty('data');
|
|
99
|
+
expect(data.attestation).toHaveProperty('decodedDataJson');
|
|
100
|
+
expect(data.attestation).toHaveProperty('isOffchain');
|
|
101
|
+
expect(data.attestation).toHaveProperty('txid');
|
|
102
|
+
});
|
|
103
|
+
});
|