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,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "manual-test",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "Steve Dakh",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"easctl": "^0.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easctl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for the Ethereum Attestation Service — create, revoke, and query attestations from the command line",
|
|
5
|
+
"bin": {
|
|
6
|
+
"easctl": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ethereum",
|
|
17
|
+
"attestation",
|
|
18
|
+
"eas",
|
|
19
|
+
"cli",
|
|
20
|
+
"ethers",
|
|
21
|
+
"web3"
|
|
22
|
+
],
|
|
23
|
+
"author": "Ethereum Attestation Service",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@ethereum-attestation-service/eas-sdk": "^2.9.0",
|
|
27
|
+
"commander": "^13.1.0",
|
|
28
|
+
"ethers": "^6.16.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.1",
|
|
33
|
+
"tsup": "^8.4.0",
|
|
34
|
+
"typescript": "^5.7.0",
|
|
35
|
+
"vitest": "^4.1.1"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getChainConfig, listChains, CHAIN_CONFIGS } from '../chains.js';
|
|
3
|
+
|
|
4
|
+
describe('chains module', () => {
|
|
5
|
+
describe('getChainConfig', () => {
|
|
6
|
+
it('returns ethereum config', () => {
|
|
7
|
+
const config = getChainConfig('ethereum');
|
|
8
|
+
expect(config.chainId).toBe(1);
|
|
9
|
+
expect(config.eas).toBe('0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587');
|
|
10
|
+
expect(config.schemaRegistry).toBe('0xA7b39296258348C78294F95B872b282326A97BDF');
|
|
11
|
+
expect(config.defaultRpc).toBe('https://ethereum-rpc.publicnode.com');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns sepolia config', () => {
|
|
15
|
+
const config = getChainConfig('sepolia');
|
|
16
|
+
expect(config.chainId).toBe(11155111);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns base config with OP stack addresses', () => {
|
|
20
|
+
const config = getChainConfig('base');
|
|
21
|
+
expect(config.chainId).toBe(8453);
|
|
22
|
+
expect(config.eas).toBe('0x4200000000000000000000000000000000000021');
|
|
23
|
+
expect(config.schemaRegistry).toBe('0x4200000000000000000000000000000000000020');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns optimism config with OP stack addresses', () => {
|
|
27
|
+
const config = getChainConfig('optimism');
|
|
28
|
+
expect(config.eas).toBe('0x4200000000000000000000000000000000000021');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('throws for unknown chain', () => {
|
|
32
|
+
expect(() => getChainConfig('unknown')).toThrow('Unknown chain "unknown"');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('lists available chains in error message', () => {
|
|
36
|
+
expect(() => getChainConfig('invalid')).toThrow('Available chains:');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('listChains', () => {
|
|
41
|
+
it('returns all chain names', () => {
|
|
42
|
+
const chains = listChains();
|
|
43
|
+
expect(chains).toContain('ethereum');
|
|
44
|
+
expect(chains).toContain('sepolia');
|
|
45
|
+
expect(chains).toContain('base');
|
|
46
|
+
expect(chains).toContain('base-sepolia');
|
|
47
|
+
expect(chains).toContain('optimism');
|
|
48
|
+
expect(chains).toContain('optimism-sepolia');
|
|
49
|
+
expect(chains).toContain('arbitrum');
|
|
50
|
+
expect(chains).toContain('arbitrum-sepolia');
|
|
51
|
+
expect(chains).toContain('polygon');
|
|
52
|
+
expect(chains).toContain('scroll');
|
|
53
|
+
expect(chains).toContain('linea');
|
|
54
|
+
expect(chains).toContain('celo');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns 12 chains', () => {
|
|
58
|
+
expect(listChains()).toHaveLength(12);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('CHAIN_CONFIGS', () => {
|
|
63
|
+
it('all chains have required fields', () => {
|
|
64
|
+
for (const [name, config] of Object.entries(CHAIN_CONFIGS)) {
|
|
65
|
+
expect(config.chainId, `${name} missing chainId`).toBeTypeOf('number');
|
|
66
|
+
expect(config.eas, `${name} missing eas`).toBeTypeOf('string');
|
|
67
|
+
expect(config.schemaRegistry, `${name} missing schemaRegistry`).toBeTypeOf('string');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('all chains have a defaultRpc', () => {
|
|
72
|
+
for (const [name, config] of Object.entries(CHAIN_CONFIGS)) {
|
|
73
|
+
expect(config.defaultRpc, `${name} missing defaultRpc`).toBeTypeOf('string');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('all chain IDs are unique', () => {
|
|
78
|
+
const ids = Object.values(CHAIN_CONFIGS).map((c) => c.chainId);
|
|
79
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../config.js', () => ({
|
|
4
|
+
clearStoredPrivateKey: vi.fn(),
|
|
5
|
+
getStoredPrivateKey: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../output.js', () => ({
|
|
9
|
+
output: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { clearKeyCommand } from '../commands/clear-key.js';
|
|
13
|
+
import { clearStoredPrivateKey, getStoredPrivateKey } from '../config.js';
|
|
14
|
+
import { output } from '../output.js';
|
|
15
|
+
|
|
16
|
+
describe('clear-key command', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('clears the stored key', async () => {
|
|
22
|
+
vi.mocked(getStoredPrivateKey).mockReturnValue('0xabc');
|
|
23
|
+
await clearKeyCommand.parseAsync(['node', 'test']);
|
|
24
|
+
expect(clearStoredPrivateKey).toHaveBeenCalled();
|
|
25
|
+
expect(output).toHaveBeenCalledWith({
|
|
26
|
+
success: true,
|
|
27
|
+
data: { cleared: true },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('reports when no key is stored', async () => {
|
|
32
|
+
vi.mocked(getStoredPrivateKey).mockReturnValue(undefined);
|
|
33
|
+
await clearKeyCommand.parseAsync(['node', 'test']);
|
|
34
|
+
expect(clearStoredPrivateKey).not.toHaveBeenCalled();
|
|
35
|
+
expect(output).toHaveBeenCalledWith({
|
|
36
|
+
success: true,
|
|
37
|
+
data: { cleared: false, message: 'No private key is currently stored' },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockProvider = { getNetwork: vi.fn() };
|
|
4
|
+
const mockWallet = { address: '0xTestAddress', provider: mockProvider };
|
|
5
|
+
|
|
6
|
+
vi.mock('ethers', () => {
|
|
7
|
+
return {
|
|
8
|
+
ethers: {
|
|
9
|
+
JsonRpcProvider: class MockJsonRpcProvider {
|
|
10
|
+
constructor(...args: any[]) {
|
|
11
|
+
mockProviderConstructor(...args);
|
|
12
|
+
return mockProvider as any;
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
Wallet: class MockWallet {
|
|
16
|
+
constructor(...args: any[]) {
|
|
17
|
+
mockWalletConstructor(...args);
|
|
18
|
+
return mockWallet as any;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const mockProviderConstructor = vi.fn();
|
|
26
|
+
const mockWalletConstructor = vi.fn();
|
|
27
|
+
|
|
28
|
+
vi.mock('@ethereum-attestation-service/eas-sdk', () => ({
|
|
29
|
+
EAS: class MockEAS {
|
|
30
|
+
constructor(...args: any[]) { mockEASConstructor(...args); }
|
|
31
|
+
},
|
|
32
|
+
SchemaRegistry: class MockSchemaRegistry {
|
|
33
|
+
constructor(...args: any[]) { mockSchemaRegistryConstructor(...args); }
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const mockEASConstructor = vi.fn();
|
|
38
|
+
const mockSchemaRegistryConstructor = vi.fn();
|
|
39
|
+
|
|
40
|
+
import { getPrivateKey, createEASClient, createReadOnlyEASClient } from '../client.js';
|
|
41
|
+
import { getStoredPrivateKey } from '../config.js';
|
|
42
|
+
|
|
43
|
+
vi.mock('../config.js', () => ({
|
|
44
|
+
getStoredPrivateKey: vi.fn(),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
describe('client module', () => {
|
|
48
|
+
const originalEnv = process.env.EAS_PRIVATE_KEY;
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
if (originalEnv !== undefined) {
|
|
52
|
+
process.env.EAS_PRIVATE_KEY = originalEnv;
|
|
53
|
+
} else {
|
|
54
|
+
delete process.env.EAS_PRIVATE_KEY;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('getPrivateKey', () => {
|
|
59
|
+
it('returns key with 0x prefix when already present', () => {
|
|
60
|
+
process.env.EAS_PRIVATE_KEY = '0xabc123';
|
|
61
|
+
expect(getPrivateKey()).toBe('0xabc123');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('adds 0x prefix when missing', () => {
|
|
65
|
+
process.env.EAS_PRIVATE_KEY = 'abc123';
|
|
66
|
+
expect(getPrivateKey()).toBe('0xabc123');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws when env var is not set', () => {
|
|
70
|
+
delete process.env.EAS_PRIVATE_KEY;
|
|
71
|
+
expect(() => getPrivateKey()).toThrow('No private key found');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws when env var is empty string', () => {
|
|
75
|
+
process.env.EAS_PRIVATE_KEY = '';
|
|
76
|
+
expect(() => getPrivateKey()).toThrow('No private key found');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to stored key when env var not set', () => {
|
|
80
|
+
delete process.env.EAS_PRIVATE_KEY;
|
|
81
|
+
vi.mocked(getStoredPrivateKey).mockReturnValue('0xstoredkey');
|
|
82
|
+
expect(getPrivateKey()).toBe('0xstoredkey');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('env var takes priority over stored key', () => {
|
|
86
|
+
process.env.EAS_PRIVATE_KEY = '0xenvkey';
|
|
87
|
+
vi.mocked(getStoredPrivateKey).mockReturnValue('0xstoredkey');
|
|
88
|
+
expect(getPrivateKey()).toBe('0xenvkey');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('throws when both env var and stored key are missing', () => {
|
|
92
|
+
delete process.env.EAS_PRIVATE_KEY;
|
|
93
|
+
vi.mocked(getStoredPrivateKey).mockReturnValue(undefined);
|
|
94
|
+
expect(() => getPrivateKey()).toThrow('No private key found');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('createEASClient', () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
process.env.EAS_PRIVATE_KEY = '0xdeadbeef';
|
|
101
|
+
vi.clearAllMocks();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('creates client with custom rpcUrl', () => {
|
|
105
|
+
createEASClient('ethereum', 'https://custom-rpc.com');
|
|
106
|
+
expect(mockProviderConstructor).toHaveBeenCalledWith('https://custom-rpc.com');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('falls back to default chain RPC when no rpcUrl', () => {
|
|
110
|
+
createEASClient('ethereum');
|
|
111
|
+
expect(mockProviderConstructor).toHaveBeenCalledWith('https://ethereum-rpc.publicnode.com');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('creates EAS with correct contract address and signer', () => {
|
|
115
|
+
createEASClient('ethereum');
|
|
116
|
+
expect(mockEASConstructor).toHaveBeenCalledWith(
|
|
117
|
+
'0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587',
|
|
118
|
+
{ signer: mockWallet }
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('creates SchemaRegistry with correct contract address and signer', () => {
|
|
123
|
+
createEASClient('ethereum');
|
|
124
|
+
expect(mockSchemaRegistryConstructor).toHaveBeenCalledWith(
|
|
125
|
+
'0xA7b39296258348C78294F95B872b282326A97BDF',
|
|
126
|
+
{ signer: mockWallet }
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns client with address', () => {
|
|
131
|
+
const client = createEASClient('ethereum');
|
|
132
|
+
expect(client.address).toBe('0xTestAddress');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('throws for unknown chain', () => {
|
|
136
|
+
expect(() => createEASClient('unknown')).toThrow('Unknown chain "unknown"');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('createReadOnlyEASClient', () => {
|
|
141
|
+
beforeEach(() => vi.clearAllMocks());
|
|
142
|
+
|
|
143
|
+
it('creates read-only client without wallet, passes provider as signer', () => {
|
|
144
|
+
const client = createReadOnlyEASClient('sepolia');
|
|
145
|
+
expect(mockWalletConstructor).not.toHaveBeenCalled();
|
|
146
|
+
expect(client.provider).toBeDefined();
|
|
147
|
+
// Verify EAS and SchemaRegistry receive the provider as signer (not a Wallet)
|
|
148
|
+
expect(mockEASConstructor).toHaveBeenCalledWith(
|
|
149
|
+
expect.any(String),
|
|
150
|
+
{ signer: mockProvider }
|
|
151
|
+
);
|
|
152
|
+
expect(mockSchemaRegistryConstructor).toHaveBeenCalledWith(
|
|
153
|
+
expect.any(String),
|
|
154
|
+
{ signer: mockProvider }
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('uses default RPC when no custom URL', () => {
|
|
159
|
+
createReadOnlyEASClient('sepolia');
|
|
160
|
+
expect(mockProviderConstructor).toHaveBeenCalledWith('https://ethereum-sepolia-rpc.publicnode.com');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('uses custom RPC when provided', () => {
|
|
164
|
+
createReadOnlyEASClient('sepolia', 'https://my-rpc.com');
|
|
165
|
+
expect(mockProviderConstructor).toHaveBeenCalledWith('https://my-rpc.com');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockEstimateGas = vi.fn().mockResolvedValue(21000n);
|
|
4
|
+
const mockWait = vi.fn();
|
|
5
|
+
const mockTx = { wait: mockWait, receipt: null as any, estimateGas: mockEstimateGas };
|
|
6
|
+
const mockAttest = vi.fn().mockResolvedValue(mockTx);
|
|
7
|
+
const mockClient = {
|
|
8
|
+
eas: { attest: mockAttest },
|
|
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
|
+
vi.mock('../../validation.js', () => ({
|
|
26
|
+
validateAddress: vi.fn(),
|
|
27
|
+
validateBytes32: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const mockEncodeData = vi.fn().mockReturnValue('0xencoded');
|
|
31
|
+
|
|
32
|
+
vi.mock('@ethereum-attestation-service/eas-sdk', () => ({
|
|
33
|
+
SchemaEncoder: class MockSchemaEncoder {
|
|
34
|
+
constructor(...args: any[]) { mockSchemaEncoderConstructor(...args); }
|
|
35
|
+
encodeData = mockEncodeData;
|
|
36
|
+
},
|
|
37
|
+
NO_EXPIRATION: 0n,
|
|
38
|
+
ZERO_BYTES32: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const mockSchemaEncoderConstructor = vi.fn();
|
|
42
|
+
|
|
43
|
+
import { attestCommand } from '../../commands/attest.js';
|
|
44
|
+
import { createEASClient } from '../../client.js';
|
|
45
|
+
import { output, handleError } from '../../output.js';
|
|
46
|
+
import { resolveInput } from '../../stdin.js';
|
|
47
|
+
|
|
48
|
+
describe('attest command', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
mockWait.mockImplementation(async () => {
|
|
52
|
+
mockTx.receipt = { hash: '0xtxhash123' };
|
|
53
|
+
return '0xuid123';
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
async function runCommand(args: string[]) {
|
|
58
|
+
await attestCommand.parseAsync(['node', 'test', ...args]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
it('creates attestation with valid data', async () => {
|
|
62
|
+
await runCommand([
|
|
63
|
+
'-s', '0xschema',
|
|
64
|
+
'-d', '[{"name":"score","type":"uint256","value":"100"}]',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
expect(createEASClient).toHaveBeenCalledWith('ethereum', undefined);
|
|
68
|
+
expect(resolveInput).toHaveBeenCalledWith('[{"name":"score","type":"uint256","value":"100"}]');
|
|
69
|
+
expect(mockSchemaEncoderConstructor).toHaveBeenCalledWith('uint256 score');
|
|
70
|
+
expect(mockEncodeData).toHaveBeenCalledWith([
|
|
71
|
+
{ name: 'score', type: 'uint256', value: '100' },
|
|
72
|
+
]);
|
|
73
|
+
expect(mockAttest).toHaveBeenCalledWith({
|
|
74
|
+
schema: '0xschema',
|
|
75
|
+
data: expect.objectContaining({
|
|
76
|
+
recipient: '0x0000000000000000000000000000000000000000',
|
|
77
|
+
revocable: true,
|
|
78
|
+
data: '0xencoded',
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
expect(output).toHaveBeenCalledWith({
|
|
82
|
+
success: true,
|
|
83
|
+
data: expect.objectContaining({
|
|
84
|
+
uid: '0xuid123',
|
|
85
|
+
txHash: '0xtxhash123',
|
|
86
|
+
attester: '0xAttester',
|
|
87
|
+
schema: '0xschema',
|
|
88
|
+
chain: 'ethereum',
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles invalid JSON in --data', async () => {
|
|
94
|
+
await runCommand(['-s', '0xschema', '-d', '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 --data');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('uses custom chain and rpc-url', async () => {
|
|
101
|
+
await runCommand([
|
|
102
|
+
'-s', '0xschema',
|
|
103
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
104
|
+
'-c', 'sepolia',
|
|
105
|
+
'--rpc-url', 'https://custom.rpc',
|
|
106
|
+
]);
|
|
107
|
+
expect(createEASClient).toHaveBeenCalledWith('sepolia', 'https://custom.rpc');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('passes custom recipient', async () => {
|
|
111
|
+
await runCommand([
|
|
112
|
+
'-s', '0xschema',
|
|
113
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
114
|
+
'-r', '0xRecipient',
|
|
115
|
+
]);
|
|
116
|
+
expect(mockAttest).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
data: expect.objectContaining({ recipient: '0xRecipient' }),
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('passes value as BigInt', async () => {
|
|
124
|
+
await runCommand([
|
|
125
|
+
'-s', '0xschema',
|
|
126
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
127
|
+
'--value', '1000',
|
|
128
|
+
]);
|
|
129
|
+
expect(mockAttest).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
data: expect.objectContaining({ value: 1000n }),
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('uses NO_EXPIRATION constant when expiration is 0', async () => {
|
|
137
|
+
await runCommand([
|
|
138
|
+
'-s', '0xschema',
|
|
139
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
140
|
+
'--expiration', '0',
|
|
141
|
+
]);
|
|
142
|
+
const callData = mockAttest.mock.calls[0][0].data;
|
|
143
|
+
expect(callData.expirationTime).toBe(0n);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('passes non-zero expiration as BigInt (not NO_EXPIRATION)', async () => {
|
|
147
|
+
await runCommand([
|
|
148
|
+
'-s', '0xschema',
|
|
149
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
150
|
+
'--expiration', '1700000000',
|
|
151
|
+
]);
|
|
152
|
+
const callData = mockAttest.mock.calls[0][0].data;
|
|
153
|
+
expect(callData.expirationTime).toBe(1700000000n);
|
|
154
|
+
expect(callData.expirationTime).not.toBe(0n);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('passes --no-revocable as false', async () => {
|
|
158
|
+
await runCommand([
|
|
159
|
+
'-s', '0xschema',
|
|
160
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
161
|
+
'--no-revocable',
|
|
162
|
+
]);
|
|
163
|
+
expect(mockAttest).toHaveBeenCalledWith(
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
data: expect.objectContaining({ revocable: false }),
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('passes SDK errors to handleError', async () => {
|
|
171
|
+
mockAttest.mockRejectedValueOnce(new Error('insufficient funds'));
|
|
172
|
+
await runCommand([
|
|
173
|
+
'-s', '0xschema',
|
|
174
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
175
|
+
]);
|
|
176
|
+
expect(handleError).toHaveBeenCalledWith(expect.any(Error));
|
|
177
|
+
const err = (handleError as any).mock.calls[0][0] as Error;
|
|
178
|
+
expect(err.message).toBe('insufficient funds');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('builds schema string from multiple data items', async () => {
|
|
182
|
+
await runCommand([
|
|
183
|
+
'-s', '0xschema',
|
|
184
|
+
'-d', '[{"name":"score","type":"uint256","value":"1"},{"name":"name","type":"string","value":"test"}]',
|
|
185
|
+
]);
|
|
186
|
+
expect(mockSchemaEncoderConstructor).toHaveBeenCalledWith('uint256 score, string name');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('estimates gas in dry-run mode without sending', async () => {
|
|
190
|
+
await runCommand([
|
|
191
|
+
'-s', '0xschema',
|
|
192
|
+
'-d', '[{"name":"x","type":"uint8","value":"1"}]',
|
|
193
|
+
'--dry-run',
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
expect(mockEstimateGas).toHaveBeenCalled();
|
|
197
|
+
expect(mockWait).not.toHaveBeenCalled();
|
|
198
|
+
expect(output).toHaveBeenCalledWith({
|
|
199
|
+
success: true,
|
|
200
|
+
data: { dryRun: true, estimatedGas: '21000', chain: 'ethereum' },
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|