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,30 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createReadOnlyEASClient } from '../client.js';
|
|
3
|
+
import { output, handleError } from '../output.js';
|
|
4
|
+
import { validateBytes32 } from '../validation.js';
|
|
5
|
+
|
|
6
|
+
export const schemaGetCommand = new Command('schema-get')
|
|
7
|
+
.description('Get a schema by UID')
|
|
8
|
+
.requiredOption('-u, --uid <uid>', 'Schema UID')
|
|
9
|
+
.option('-c, --chain <name>', 'Chain name', 'ethereum')
|
|
10
|
+
.option('--rpc-url <url>', 'Custom RPC URL')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
try {
|
|
13
|
+
validateBytes32(opts.uid, 'schema UID');
|
|
14
|
+
|
|
15
|
+
const client = createReadOnlyEASClient(opts.chain, opts.rpcUrl);
|
|
16
|
+
const schema = await client.schemaRegistry.getSchema({ uid: opts.uid });
|
|
17
|
+
|
|
18
|
+
output({
|
|
19
|
+
success: true,
|
|
20
|
+
data: {
|
|
21
|
+
uid: schema.uid,
|
|
22
|
+
schema: schema.schema,
|
|
23
|
+
resolver: schema.resolver,
|
|
24
|
+
revocable: schema.revocable,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
handleError(err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createEASClient } from '../client.js';
|
|
3
|
+
import { output, handleError } from '../output.js';
|
|
4
|
+
import { validateAddress } from '../validation.js';
|
|
5
|
+
|
|
6
|
+
export const schemaRegisterCommand = new Command('schema-register')
|
|
7
|
+
.description('Register a new schema')
|
|
8
|
+
.requiredOption('-s, --schema <definition>', 'Schema definition (e.g. "uint256 score, string name")')
|
|
9
|
+
.option('--resolver <address>', 'Resolver contract address', '0x0000000000000000000000000000000000000000')
|
|
10
|
+
.option('--revocable', 'Whether attestations using this schema can be revoked', true)
|
|
11
|
+
.option('--no-revocable', 'Make attestations non-revocable')
|
|
12
|
+
.option('-c, --chain <name>', 'Chain name', 'ethereum')
|
|
13
|
+
.option('--rpc-url <url>', 'Custom RPC URL')
|
|
14
|
+
.option('--dry-run', 'Estimate gas without sending the transaction')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
try {
|
|
17
|
+
if (opts.resolver !== '0x0000000000000000000000000000000000000000') {
|
|
18
|
+
validateAddress(opts.resolver, 'resolver');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const client = createEASClient(opts.chain, opts.rpcUrl);
|
|
22
|
+
|
|
23
|
+
const tx = await client.schemaRegistry.register({
|
|
24
|
+
schema: opts.schema,
|
|
25
|
+
resolverAddress: opts.resolver,
|
|
26
|
+
revocable: opts.revocable,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (opts.dryRun) {
|
|
30
|
+
const gasEstimate = await tx.estimateGas();
|
|
31
|
+
output({ success: true, data: { dryRun: true, estimatedGas: gasEstimate.toString(), chain: opts.chain } });
|
|
32
|
+
} else {
|
|
33
|
+
const uid = await tx.wait();
|
|
34
|
+
output({
|
|
35
|
+
success: true,
|
|
36
|
+
data: {
|
|
37
|
+
uid,
|
|
38
|
+
txHash: tx.receipt!.hash,
|
|
39
|
+
schema: opts.schema,
|
|
40
|
+
resolver: opts.resolver,
|
|
41
|
+
revocable: opts.revocable,
|
|
42
|
+
chain: opts.chain,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
handleError(err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ethers } from 'ethers';
|
|
3
|
+
import { setStoredPrivateKey } from '../config.js';
|
|
4
|
+
import { output, handleError } from '../output.js';
|
|
5
|
+
|
|
6
|
+
export const setKeyCommand = new Command('set-key')
|
|
7
|
+
.description('Store your private key in ~/.eas-cli for future use')
|
|
8
|
+
.argument('<key>', 'Wallet private key (hex string, with or without 0x prefix)')
|
|
9
|
+
.action((key: string) => {
|
|
10
|
+
const normalized = key.startsWith('0x') ? key : `0x${key}`;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const wallet = new ethers.Wallet(normalized);
|
|
14
|
+
setStoredPrivateKey(key);
|
|
15
|
+
output({ success: true, data: { address: wallet.address } });
|
|
16
|
+
} catch {
|
|
17
|
+
handleError(new Error('Invalid private key format'));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createEASClient } from '../client.js';
|
|
3
|
+
import { output, handleError } from '../output.js';
|
|
4
|
+
|
|
5
|
+
export const timestampCommand = new Command('timestamp')
|
|
6
|
+
.description('Timestamp data on-chain')
|
|
7
|
+
.requiredOption('-d, --data <bytes32>', 'Data to timestamp (bytes32 hex string)')
|
|
8
|
+
.option('-c, --chain <name>', 'Chain name', 'ethereum')
|
|
9
|
+
.option('--rpc-url <url>', 'Custom RPC URL')
|
|
10
|
+
.option('--dry-run', 'Estimate gas without sending the transaction')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
try {
|
|
13
|
+
const client = createEASClient(opts.chain, opts.rpcUrl);
|
|
14
|
+
|
|
15
|
+
const tx = await client.eas.timestamp(opts.data);
|
|
16
|
+
|
|
17
|
+
if (opts.dryRun) {
|
|
18
|
+
const gasEstimate = await tx.estimateGas();
|
|
19
|
+
output({ success: true, data: { dryRun: true, estimatedGas: gasEstimate.toString(), chain: opts.chain } });
|
|
20
|
+
} else {
|
|
21
|
+
const timestamp = await tx.wait();
|
|
22
|
+
output({
|
|
23
|
+
success: true,
|
|
24
|
+
data: {
|
|
25
|
+
timestamp: timestamp.toString(),
|
|
26
|
+
txHash: tx.receipt!.hash,
|
|
27
|
+
data: opts.data,
|
|
28
|
+
chain: opts.chain,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
handleError(err);
|
|
34
|
+
}
|
|
35
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
interface EASConfig {
|
|
6
|
+
privateKey?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getConfigPath(): string {
|
|
10
|
+
return join(homedir(), '.eas-cli');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readConfig(): EASConfig {
|
|
14
|
+
try {
|
|
15
|
+
const data = readFileSync(getConfigPath(), 'utf-8');
|
|
16
|
+
return JSON.parse(data);
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeConfig(config: EASConfig): void {
|
|
23
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getStoredPrivateKey(): string | undefined {
|
|
27
|
+
return readConfig().privateKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function setStoredPrivateKey(key: string): void {
|
|
31
|
+
const normalized = key.startsWith('0x') ? key : `0x${key}`;
|
|
32
|
+
const config = readConfig();
|
|
33
|
+
config.privateKey = normalized;
|
|
34
|
+
writeConfig(config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clearStoredPrivateKey(): void {
|
|
38
|
+
const config = readConfig();
|
|
39
|
+
delete config.privateKey;
|
|
40
|
+
writeConfig(config);
|
|
41
|
+
}
|
package/src/graphql.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const EASSCAN_URLS: Record<string, string> = {
|
|
2
|
+
ethereum: 'https://easscan.org',
|
|
3
|
+
sepolia: 'https://sepolia.easscan.org',
|
|
4
|
+
base: 'https://base.easscan.org',
|
|
5
|
+
'base-sepolia': 'https://base-sepolia.easscan.org',
|
|
6
|
+
optimism: 'https://optimism.easscan.org',
|
|
7
|
+
'optimism-sepolia': 'https://optimism-sepolia.easscan.org',
|
|
8
|
+
arbitrum: 'https://arbitrum.easscan.org',
|
|
9
|
+
'arbitrum-sepolia': 'https://arbitrum-sepolia.easscan.org',
|
|
10
|
+
polygon: 'https://polygon.easscan.org',
|
|
11
|
+
scroll: 'https://scroll.easscan.org',
|
|
12
|
+
linea: 'https://linea.easscan.org',
|
|
13
|
+
celo: 'https://celo.easscan.org',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function getEASScanUrl(chainName: string): string {
|
|
17
|
+
const url = EASSCAN_URLS[chainName];
|
|
18
|
+
if (!url) {
|
|
19
|
+
throw new Error(`No EASScan URL for chain "${chainName}"`);
|
|
20
|
+
}
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getGraphQLEndpoint(chainName: string): string {
|
|
25
|
+
return `${getEASScanUrl(chainName)}/graphql`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function graphqlQuery(
|
|
29
|
+
chainName: string,
|
|
30
|
+
query: string,
|
|
31
|
+
variables: Record<string, unknown> = {}
|
|
32
|
+
): Promise<any> {
|
|
33
|
+
const endpoint = getGraphQLEndpoint(chainName);
|
|
34
|
+
const res = await fetch(endpoint, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ query, variables }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const json = await res.json();
|
|
45
|
+
if (json.errors?.length) {
|
|
46
|
+
throw new Error(`GraphQL error: ${json.errors[0].message}`);
|
|
47
|
+
}
|
|
48
|
+
return json.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const QUERIES = {
|
|
52
|
+
getSchema: `
|
|
53
|
+
query GetSchema($id: String!) {
|
|
54
|
+
schema(where: { id: $id }) {
|
|
55
|
+
id
|
|
56
|
+
schema
|
|
57
|
+
creator
|
|
58
|
+
resolver
|
|
59
|
+
revocable
|
|
60
|
+
txid
|
|
61
|
+
time
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`,
|
|
65
|
+
getAttestation: `
|
|
66
|
+
query GetAttestation($id: String!) {
|
|
67
|
+
attestation(where: { id: $id }) {
|
|
68
|
+
id
|
|
69
|
+
attester
|
|
70
|
+
recipient
|
|
71
|
+
time
|
|
72
|
+
expirationTime
|
|
73
|
+
revocationTime
|
|
74
|
+
revoked
|
|
75
|
+
revocable
|
|
76
|
+
schemaId
|
|
77
|
+
data
|
|
78
|
+
decodedDataJson
|
|
79
|
+
isOffchain
|
|
80
|
+
txid
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
getAttestationsBySchema: `
|
|
85
|
+
query GetAttestationsBySchema($schemaId: String!, $take: Int, $skip: Int) {
|
|
86
|
+
attestations(
|
|
87
|
+
where: { schemaId: { equals: $schemaId } }
|
|
88
|
+
take: $take
|
|
89
|
+
skip: $skip
|
|
90
|
+
orderBy: [{ time: desc }]
|
|
91
|
+
) {
|
|
92
|
+
id
|
|
93
|
+
attester
|
|
94
|
+
recipient
|
|
95
|
+
time
|
|
96
|
+
revoked
|
|
97
|
+
decodedDataJson
|
|
98
|
+
isOffchain
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`,
|
|
102
|
+
getAttestationsByAttester: `
|
|
103
|
+
query GetAttestationsByAttester($attester: String!, $take: Int, $skip: Int) {
|
|
104
|
+
attestations(
|
|
105
|
+
where: { attester: { equals: $attester } }
|
|
106
|
+
take: $take
|
|
107
|
+
skip: $skip
|
|
108
|
+
orderBy: [{ time: desc }]
|
|
109
|
+
) {
|
|
110
|
+
id
|
|
111
|
+
recipient
|
|
112
|
+
schemaId
|
|
113
|
+
time
|
|
114
|
+
revoked
|
|
115
|
+
decodedDataJson
|
|
116
|
+
isOffchain
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
getSchemata: `
|
|
121
|
+
query GetSchemata($creator: String, $take: Int, $skip: Int) {
|
|
122
|
+
schemata(
|
|
123
|
+
where: { creator: { equals: $creator } }
|
|
124
|
+
take: $take
|
|
125
|
+
skip: $skip
|
|
126
|
+
orderBy: [{ time: desc }]
|
|
127
|
+
) {
|
|
128
|
+
id
|
|
129
|
+
schema
|
|
130
|
+
creator
|
|
131
|
+
revocable
|
|
132
|
+
time
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`,
|
|
136
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { setJsonMode, output } from './output.js';
|
|
3
|
+
import { listChains } from './chains.js';
|
|
4
|
+
import { attestCommand } from './commands/attest.js';
|
|
5
|
+
import { revokeCommand } from './commands/revoke.js';
|
|
6
|
+
import { getAttestationCommand } from './commands/get-attestation.js';
|
|
7
|
+
import { schemaRegisterCommand } from './commands/schema-register.js';
|
|
8
|
+
import { schemaGetCommand } from './commands/schema-get.js';
|
|
9
|
+
import { multiAttestCommand } from './commands/multi-attest.js';
|
|
10
|
+
import { offchainAttestCommand } from './commands/offchain-attest.js';
|
|
11
|
+
import { timestampCommand } from './commands/timestamp.js';
|
|
12
|
+
import { querySchemaCommand } from './commands/query-schema.js';
|
|
13
|
+
import { queryAttestationCommand } from './commands/query-attestation.js';
|
|
14
|
+
import { queryAttestationsCommand } from './commands/query-attestations.js';
|
|
15
|
+
import { querySchemasCommand } from './commands/query-schemas.js';
|
|
16
|
+
import { multiRevokeCommand } from './commands/multi-revoke.js';
|
|
17
|
+
import { multiTimestampCommand } from './commands/multi-timestamp.js';
|
|
18
|
+
import { setKeyCommand } from './commands/set-key.js';
|
|
19
|
+
import { clearKeyCommand } from './commands/clear-key.js';
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name('easctl')
|
|
25
|
+
.description('Ethereum Attestation Service CLI — create, revoke, and query attestations')
|
|
26
|
+
.version(process.env.CLI_VERSION || '0.0.0-dev')
|
|
27
|
+
.option('--json', 'Output results as JSON (useful for agents and scripting)')
|
|
28
|
+
.hook('preAction', (thisCommand, actionCommand) => {
|
|
29
|
+
if (thisCommand.opts().json || actionCommand.opts().json) {
|
|
30
|
+
setJsonMode(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Attestation commands
|
|
35
|
+
program.addCommand(attestCommand);
|
|
36
|
+
program.addCommand(multiAttestCommand);
|
|
37
|
+
program.addCommand(offchainAttestCommand);
|
|
38
|
+
program.addCommand(revokeCommand);
|
|
39
|
+
program.addCommand(multiRevokeCommand);
|
|
40
|
+
program.addCommand(getAttestationCommand);
|
|
41
|
+
|
|
42
|
+
// Schema commands
|
|
43
|
+
program.addCommand(schemaRegisterCommand);
|
|
44
|
+
program.addCommand(schemaGetCommand);
|
|
45
|
+
|
|
46
|
+
// Timestamp commands
|
|
47
|
+
program.addCommand(timestampCommand);
|
|
48
|
+
program.addCommand(multiTimestampCommand);
|
|
49
|
+
|
|
50
|
+
// GraphQL query commands
|
|
51
|
+
program.addCommand(querySchemaCommand);
|
|
52
|
+
program.addCommand(queryAttestationCommand);
|
|
53
|
+
program.addCommand(queryAttestationsCommand);
|
|
54
|
+
program.addCommand(querySchemasCommand);
|
|
55
|
+
|
|
56
|
+
// Key management commands
|
|
57
|
+
program.addCommand(setKeyCommand);
|
|
58
|
+
program.addCommand(clearKeyCommand);
|
|
59
|
+
|
|
60
|
+
// Chains command
|
|
61
|
+
program
|
|
62
|
+
.command('chains')
|
|
63
|
+
.description('List supported chains and their EAS contract addresses')
|
|
64
|
+
.action(() => {
|
|
65
|
+
const chains = listChains();
|
|
66
|
+
output({ success: true, data: { chains } });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Add --json to every subcommand so it works regardless of position
|
|
70
|
+
for (const cmd of program.commands) {
|
|
71
|
+
cmd.option('--json', 'Output results as JSON');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
program.parse();
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface CLIOutput {
|
|
2
|
+
success: boolean;
|
|
3
|
+
data?: Record<string, unknown>;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let jsonMode = false;
|
|
8
|
+
|
|
9
|
+
export function setJsonMode(enabled: boolean) {
|
|
10
|
+
jsonMode = enabled;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isJsonMode(): boolean {
|
|
14
|
+
return jsonMode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function output(result: CLIOutput) {
|
|
18
|
+
if (jsonMode) {
|
|
19
|
+
console.log(JSON.stringify(result, bigintReplacer, 2));
|
|
20
|
+
} else if (result.success && result.data) {
|
|
21
|
+
for (const [key, value] of Object.entries(result.data)) {
|
|
22
|
+
if (typeof value === 'object' && value !== null) {
|
|
23
|
+
console.log(`${key}:`);
|
|
24
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
25
|
+
console.log(` ${k}: ${formatValue(v)}`);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
console.log(`${key}: ${formatValue(value)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else if (!result.success && result.error) {
|
|
32
|
+
console.error(`Error: ${result.error}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatValue(v: unknown): string {
|
|
37
|
+
if (typeof v === 'bigint') return v.toString();
|
|
38
|
+
if (typeof v === 'object' && v !== null) return JSON.stringify(v, bigintReplacer);
|
|
39
|
+
return String(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bigintReplacer(_key: string, value: unknown): unknown {
|
|
43
|
+
return typeof value === 'bigint' ? value.toString() : value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function handleError(err: unknown): never {
|
|
47
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
+
output({ success: false, error: message });
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
package/src/stdin.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function readStdin(): Promise<string> {
|
|
2
|
+
const chunks: Buffer[] = [];
|
|
3
|
+
for await (const chunk of process.stdin) {
|
|
4
|
+
chunks.push(chunk);
|
|
5
|
+
}
|
|
6
|
+
const text = Buffer.concat(chunks).toString('utf-8').trim();
|
|
7
|
+
if (!text) {
|
|
8
|
+
throw new Error('No data received from stdin');
|
|
9
|
+
}
|
|
10
|
+
return text;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function resolveInput(value: string): Promise<string> {
|
|
14
|
+
return value === '-' ? readStdin() : value;
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
2
|
+
|
|
3
|
+
export function validateAddress(value: string, label: string): void {
|
|
4
|
+
if (!ADDRESS_RE.test(value)) {
|
|
5
|
+
throw new Error(`Invalid ${label} address: expected 0x + 40 hex characters, got "${value}"`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BYTES32_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
10
|
+
|
|
11
|
+
export function validateBytes32(value: string, label: string): void {
|
|
12
|
+
if (!BYTES32_RE.test(value)) {
|
|
13
|
+
throw new Error(`Invalid ${label}: expected 0x + 64 hex characters, got "${value}"`);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
import pkg from './package.json';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
entry: ['src/index.ts'],
|
|
6
|
+
format: ['cjs'],
|
|
7
|
+
target: 'node18',
|
|
8
|
+
clean: true,
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
noExternal: [
|
|
11
|
+
'@ethereum-attestation-service/eas-sdk',
|
|
12
|
+
'@ethereum-attestation-service/eas-contracts',
|
|
13
|
+
'@ethereum-attestation-service/eas-contracts-legacy',
|
|
14
|
+
],
|
|
15
|
+
define: {
|
|
16
|
+
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
|
17
|
+
},
|
|
18
|
+
banner: {
|
|
19
|
+
js: '#!/usr/bin/env node',
|
|
20
|
+
},
|
|
21
|
+
});
|