easctl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/README.md +195 -0
  3. package/dist/index.js +31401 -0
  4. package/dist/index.js.map +1 -0
  5. package/manual-test/package-lock.json +4483 -0
  6. package/manual-test/package.json +15 -0
  7. package/package.json +40 -0
  8. package/src/__tests__/chains.test.ts +82 -0
  9. package/src/__tests__/clear-key.test.ts +40 -0
  10. package/src/__tests__/client.test.ts +168 -0
  11. package/src/__tests__/commands/attest.test.ts +203 -0
  12. package/src/__tests__/commands/get-attestation.test.ts +164 -0
  13. package/src/__tests__/commands/multi-attest.test.ts +166 -0
  14. package/src/__tests__/commands/multi-revoke.test.ts +114 -0
  15. package/src/__tests__/commands/multi-timestamp.test.ts +88 -0
  16. package/src/__tests__/commands/offchain-attest.test.ts +217 -0
  17. package/src/__tests__/commands/query-attestation.test.ts +84 -0
  18. package/src/__tests__/commands/query-attestations.test.ts +156 -0
  19. package/src/__tests__/commands/query-schema.test.ts +62 -0
  20. package/src/__tests__/commands/query-schemas.test.ts +110 -0
  21. package/src/__tests__/commands/revoke.test.ts +86 -0
  22. package/src/__tests__/commands/schema-get.test.ts +66 -0
  23. package/src/__tests__/commands/schema-register.test.ts +94 -0
  24. package/src/__tests__/commands/timestamp.test.ts +78 -0
  25. package/src/__tests__/config.test.ts +103 -0
  26. package/src/__tests__/graphql.test.ts +148 -0
  27. package/src/__tests__/integration/graphql-live.test.ts +103 -0
  28. package/src/__tests__/integration/offchain-signing.test.ts +252 -0
  29. package/src/__tests__/integration/schema-encoder.test.ts +131 -0
  30. package/src/__tests__/output.test.ts +138 -0
  31. package/src/__tests__/set-key.test.ts +58 -0
  32. package/src/__tests__/stdin.test.ts +15 -0
  33. package/src/chains.ts +99 -0
  34. package/src/client.ts +53 -0
  35. package/src/commands/attest.ts +73 -0
  36. package/src/commands/clear-key.ts +15 -0
  37. package/src/commands/get-attestation.ts +58 -0
  38. package/src/commands/multi-attest.ts +75 -0
  39. package/src/commands/multi-revoke.ts +60 -0
  40. package/src/commands/multi-timestamp.ts +43 -0
  41. package/src/commands/offchain-attest.ts +78 -0
  42. package/src/commands/query-attestation.ts +31 -0
  43. package/src/commands/query-attestations.ts +57 -0
  44. package/src/commands/query-schema.ts +24 -0
  45. package/src/commands/query-schemas.ts +35 -0
  46. package/src/commands/revoke.ts +48 -0
  47. package/src/commands/schema-get.ts +30 -0
  48. package/src/commands/schema-register.ts +49 -0
  49. package/src/commands/set-key.ts +19 -0
  50. package/src/commands/timestamp.ts +35 -0
  51. package/src/config.ts +41 -0
  52. package/src/graphql.ts +136 -0
  53. package/src/index.ts +74 -0
  54. package/src/output.ts +50 -0
  55. package/src/stdin.ts +15 -0
  56. package/src/validation.ts +15 -0
  57. package/tsconfig.json +16 -0
  58. package/tsup.config.ts +21 -0
  59. package/vitest.config.ts +7 -0
package/src/client.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { ethers } from 'ethers';
2
+ import { EAS, SchemaRegistry } from '@ethereum-attestation-service/eas-sdk';
3
+ import { getChainConfig, type ChainConfig } from './chains.js';
4
+ import { getStoredPrivateKey } from './config.js';
5
+
6
+ export interface EASClient {
7
+ eas: EAS;
8
+ schemaRegistry: SchemaRegistry;
9
+ signer: ethers.Wallet;
10
+ provider: ethers.JsonRpcProvider;
11
+ chainConfig: ChainConfig;
12
+ address: string;
13
+ }
14
+
15
+ export function getPrivateKey(): string {
16
+ const key = process.env.EAS_PRIVATE_KEY || getStoredPrivateKey();
17
+ if (!key) {
18
+ throw new Error(
19
+ 'No private key found. Set one with: eas set-key <key> (or set EAS_PRIVATE_KEY env var)'
20
+ );
21
+ }
22
+ return key.startsWith('0x') ? key : `0x${key}`;
23
+ }
24
+
25
+ export function createEASClient(chainName: string, rpcUrl?: string): EASClient {
26
+ const chainConfig = getChainConfig(chainName);
27
+ const privateKey = getPrivateKey();
28
+
29
+ const provider = new ethers.JsonRpcProvider(rpcUrl || chainConfig.defaultRpc);
30
+ const signer = new ethers.Wallet(privateKey, provider);
31
+
32
+ const eas = new EAS(chainConfig.eas, { signer });
33
+ const schemaRegistry = new SchemaRegistry(chainConfig.schemaRegistry, { signer });
34
+
35
+ return {
36
+ eas,
37
+ schemaRegistry,
38
+ signer,
39
+ provider,
40
+ chainConfig,
41
+ address: signer.address,
42
+ };
43
+ }
44
+
45
+ export function createReadOnlyEASClient(chainName: string, rpcUrl?: string) {
46
+ const chainConfig = getChainConfig(chainName);
47
+ const provider = new ethers.JsonRpcProvider(rpcUrl || chainConfig.defaultRpc);
48
+
49
+ const eas = new EAS(chainConfig.eas, { signer: provider });
50
+ const schemaRegistry = new SchemaRegistry(chainConfig.schemaRegistry, { signer: provider });
51
+
52
+ return { eas, schemaRegistry, provider, chainConfig };
53
+ }
@@ -0,0 +1,73 @@
1
+ import { Command } from 'commander';
2
+ import { SchemaEncoder, NO_EXPIRATION, ZERO_BYTES32 } from '@ethereum-attestation-service/eas-sdk';
3
+ import { createEASClient } from '../client.js';
4
+ import { output, handleError } from '../output.js';
5
+ import { resolveInput } from '../stdin.js';
6
+ import { validateAddress, validateBytes32 } from '../validation.js';
7
+
8
+ export const attestCommand = new Command('attest')
9
+ .description('Create an on-chain attestation')
10
+ .requiredOption('-s, --schema <uid>', 'Schema UID to attest with')
11
+ .requiredOption('-d, --data <json>', 'Attestation data as JSON array: [{"name":"field","type":"uint256","value":"123"}]')
12
+ .option('-r, --recipient <address>', 'Recipient address', '0x0000000000000000000000000000000000000000')
13
+ .option('--ref-uid <uid>', 'Referenced attestation UID', ZERO_BYTES32)
14
+ .option('--expiration <timestamp>', 'Expiration timestamp (0 for none)', '0')
15
+ .option('--revocable', 'Whether the attestation is revocable', true)
16
+ .option('--no-revocable', 'Make the attestation non-revocable')
17
+ .option('--value <wei>', 'ETH value to send (in wei)', '0')
18
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
19
+ .option('--rpc-url <url>', 'Custom RPC URL')
20
+ .option('--dry-run', 'Estimate gas without sending the transaction')
21
+ .action(async (opts) => {
22
+ try {
23
+ validateBytes32(opts.schema, 'schema UID');
24
+ if (opts.recipient !== '0x0000000000000000000000000000000000000000') {
25
+ validateAddress(opts.recipient, 'recipient');
26
+ }
27
+
28
+ const client = createEASClient(opts.chain, opts.rpcUrl);
29
+
30
+ const rawData = await resolveInput(opts.data);
31
+ let dataItems;
32
+ try {
33
+ dataItems = JSON.parse(rawData);
34
+ } catch (e) {
35
+ throw new Error(`Invalid JSON in --data: ${e instanceof Error ? e.message : e}`);
36
+ }
37
+ const schemaString = dataItems.map((item: any) => `${item.type} ${item.name}`).join(', ');
38
+ const encoder = new SchemaEncoder(schemaString);
39
+ const encodedData = encoder.encodeData(dataItems);
40
+
41
+ const tx = await client.eas.attest({
42
+ schema: opts.schema,
43
+ data: {
44
+ recipient: opts.recipient,
45
+ expirationTime: BigInt(opts.expiration) === 0n ? NO_EXPIRATION : BigInt(opts.expiration),
46
+ revocable: opts.revocable,
47
+ refUID: opts.refUid,
48
+ data: encodedData,
49
+ value: BigInt(opts.value),
50
+ },
51
+ });
52
+
53
+ if (opts.dryRun) {
54
+ const gasEstimate = await tx.estimateGas();
55
+ output({ success: true, data: { dryRun: true, estimatedGas: gasEstimate.toString(), chain: opts.chain } });
56
+ } else {
57
+ const uid = await tx.wait();
58
+ output({
59
+ success: true,
60
+ data: {
61
+ uid,
62
+ txHash: tx.receipt!.hash,
63
+ attester: client.address,
64
+ recipient: opts.recipient,
65
+ schema: opts.schema,
66
+ chain: opts.chain,
67
+ },
68
+ });
69
+ }
70
+ } catch (err) {
71
+ handleError(err);
72
+ }
73
+ });
@@ -0,0 +1,15 @@
1
+ import { Command } from 'commander';
2
+ import { clearStoredPrivateKey, getStoredPrivateKey } from '../config.js';
3
+ import { output } from '../output.js';
4
+
5
+ export const clearKeyCommand = new Command('clear-key')
6
+ .description('Remove the stored private key from ~/.eas-cli')
7
+ .action(() => {
8
+ if (!getStoredPrivateKey()) {
9
+ output({ success: true, data: { cleared: false, message: 'No private key is currently stored' } });
10
+ return;
11
+ }
12
+
13
+ clearStoredPrivateKey();
14
+ output({ success: true, data: { cleared: true } });
15
+ });
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander';
2
+ import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk';
3
+ import { createReadOnlyEASClient } from '../client.js';
4
+ import { output, handleError } from '../output.js';
5
+ import { validateBytes32 } from '../validation.js';
6
+
7
+ export const getAttestationCommand = new Command('get-attestation')
8
+ .description('Get an attestation by UID')
9
+ .requiredOption('-u, --uid <uid>', 'Attestation UID')
10
+ .option('--decode [schema]', 'Decode data using schema string, or pass without value to auto-fetch from chain')
11
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
12
+ .option('--rpc-url <url>', 'Custom RPC URL')
13
+ .action(async (opts) => {
14
+ try {
15
+ validateBytes32(opts.uid, 'attestation UID');
16
+
17
+ const client = createReadOnlyEASClient(opts.chain, opts.rpcUrl);
18
+ const attestation = await client.eas.getAttestation(opts.uid);
19
+
20
+ const result: Record<string, unknown> = {
21
+ uid: attestation.uid,
22
+ schema: attestation.schema,
23
+ attester: attestation.attester,
24
+ recipient: attestation.recipient,
25
+ refUID: attestation.refUID,
26
+ revocable: attestation.revocable,
27
+ revocationTime: Number(attestation.revocationTime),
28
+ expirationTime: Number(attestation.expirationTime),
29
+ time: Number(attestation.time),
30
+ data: attestation.data,
31
+ };
32
+
33
+ if (opts.decode) {
34
+ try {
35
+ let schemaString: string;
36
+ if (typeof opts.decode === 'string') {
37
+ schemaString = opts.decode;
38
+ } else {
39
+ const schemaRecord = await client.schemaRegistry.getSchema({ uid: attestation.schema });
40
+ schemaString = schemaRecord.schema;
41
+ }
42
+ const encoder = new SchemaEncoder(schemaString);
43
+ const decoded = encoder.decodeData(attestation.data);
44
+ result.decodedData = decoded.map((item) => ({
45
+ name: item.name,
46
+ type: item.type,
47
+ value: typeof item.value.value === 'bigint' ? item.value.value.toString() : item.value.value,
48
+ }));
49
+ } catch (decodeErr) {
50
+ result.decodeError = decodeErr instanceof Error ? decodeErr.message : String(decodeErr);
51
+ }
52
+ }
53
+
54
+ output({ success: true, data: result });
55
+ } catch (err) {
56
+ handleError(err);
57
+ }
58
+ });
@@ -0,0 +1,75 @@
1
+ import { Command } from 'commander';
2
+ import { SchemaEncoder, NO_EXPIRATION, ZERO_BYTES32 } from '@ethereum-attestation-service/eas-sdk';
3
+ import { createEASClient } from '../client.js';
4
+ import { output, handleError } from '../output.js';
5
+ import { resolveInput } from '../stdin.js';
6
+
7
+ interface AttestationInput {
8
+ schema: string;
9
+ recipient?: string;
10
+ refUID?: string;
11
+ expirationTime?: string;
12
+ revocable?: boolean;
13
+ value?: string;
14
+ data: Array<{ name: string; type: string; value: unknown }>;
15
+ }
16
+
17
+ export const multiAttestCommand = new Command('multi-attest')
18
+ .description('Create multiple on-chain attestations in a single transaction')
19
+ .requiredOption('-i, --input <json>', 'JSON array of attestation objects')
20
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
21
+ .option('--rpc-url <url>', 'Custom RPC URL')
22
+ .option('--dry-run', 'Estimate gas without sending the transaction')
23
+ .action(async (opts) => {
24
+ try {
25
+ const client = createEASClient(opts.chain, opts.rpcUrl);
26
+ const rawInput = await resolveInput(opts.input);
27
+ let inputs: AttestationInput[];
28
+ try {
29
+ inputs = JSON.parse(rawInput);
30
+ } catch (e) {
31
+ throw new Error(`Invalid JSON in --input: ${e instanceof Error ? e.message : e}`);
32
+ }
33
+
34
+ const grouped = new Map<string, { schema: string; data: any[] }>();
35
+
36
+ for (const input of inputs) {
37
+ const schemaString = input.data.map((item) => `${item.type} ${item.name}`).join(', ');
38
+ const encoder = new SchemaEncoder(schemaString);
39
+ const encodedData = encoder.encodeData(input.data as any);
40
+
41
+ if (!grouped.has(input.schema)) {
42
+ grouped.set(input.schema, { schema: input.schema, data: [] });
43
+ }
44
+
45
+ grouped.get(input.schema)!.data.push({
46
+ recipient: input.recipient || '0x0000000000000000000000000000000000000000',
47
+ expirationTime: input.expirationTime ? BigInt(input.expirationTime) : NO_EXPIRATION,
48
+ revocable: input.revocable ?? true,
49
+ refUID: input.refUID || ZERO_BYTES32,
50
+ data: encodedData,
51
+ value: input.value ? BigInt(input.value) : 0n,
52
+ });
53
+ }
54
+
55
+ const tx = await client.eas.multiAttest(Array.from(grouped.values()));
56
+
57
+ if (opts.dryRun) {
58
+ const gasEstimate = await tx.estimateGas();
59
+ output({ success: true, data: { dryRun: true, estimatedGas: gasEstimate.toString(), chain: opts.chain } });
60
+ } else {
61
+ const uids = await tx.wait();
62
+ output({
63
+ success: true,
64
+ data: {
65
+ uids,
66
+ count: uids.length,
67
+ txHash: tx.receipt!.hash,
68
+ chain: opts.chain,
69
+ },
70
+ });
71
+ }
72
+ } catch (err) {
73
+ handleError(err);
74
+ }
75
+ });
@@ -0,0 +1,60 @@
1
+ import { Command } from 'commander';
2
+ import { createEASClient } from '../client.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { resolveInput } from '../stdin.js';
5
+
6
+ interface RevocationInput {
7
+ schema: string;
8
+ uid: string;
9
+ value?: string;
10
+ }
11
+
12
+ export const multiRevokeCommand = new Command('multi-revoke')
13
+ .description('Revoke multiple attestations in a single transaction')
14
+ .requiredOption('-i, --input <json>', 'JSON array of revocation objects: [{"schema":"0x...","uid":"0x..."}]')
15
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
16
+ .option('--rpc-url <url>', 'Custom RPC URL')
17
+ .option('--dry-run', 'Estimate gas without sending the transaction')
18
+ .action(async (opts) => {
19
+ try {
20
+ const client = createEASClient(opts.chain, opts.rpcUrl);
21
+ const rawInput = await resolveInput(opts.input);
22
+ let inputs: RevocationInput[];
23
+ try {
24
+ inputs = JSON.parse(rawInput);
25
+ } catch (e) {
26
+ throw new Error(`Invalid JSON in --input: ${e instanceof Error ? e.message : e}`);
27
+ }
28
+
29
+ const grouped = new Map<string, { schema: string; data: any[] }>();
30
+
31
+ for (const input of inputs) {
32
+ if (!grouped.has(input.schema)) {
33
+ grouped.set(input.schema, { schema: input.schema, data: [] });
34
+ }
35
+ grouped.get(input.schema)!.data.push({
36
+ uid: input.uid,
37
+ value: input.value ? BigInt(input.value) : 0n,
38
+ });
39
+ }
40
+
41
+ const tx = await client.eas.multiRevoke(Array.from(grouped.values()));
42
+
43
+ if (opts.dryRun) {
44
+ const gasEstimate = await tx.estimateGas();
45
+ output({ success: true, data: { dryRun: true, estimatedGas: gasEstimate.toString(), chain: opts.chain } });
46
+ } else {
47
+ await tx.wait();
48
+ output({
49
+ success: true,
50
+ data: {
51
+ revoked: inputs.length,
52
+ txHash: tx.receipt!.hash,
53
+ chain: opts.chain,
54
+ },
55
+ });
56
+ }
57
+ } catch (err) {
58
+ handleError(err);
59
+ }
60
+ });
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { createEASClient } from '../client.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { resolveInput } from '../stdin.js';
5
+
6
+ export const multiTimestampCommand = new Command('multi-timestamp')
7
+ .description('Timestamp multiple data items in a single transaction')
8
+ .requiredOption('-d, --data <json>', 'JSON array of bytes32 hex strings to timestamp')
9
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
10
+ .option('--rpc-url <url>', 'Custom RPC URL')
11
+ .option('--dry-run', 'Estimate gas without sending the transaction')
12
+ .action(async (opts) => {
13
+ try {
14
+ const client = createEASClient(opts.chain, opts.rpcUrl);
15
+ const rawData = await resolveInput(opts.data);
16
+ let items: string[];
17
+ try {
18
+ items = JSON.parse(rawData);
19
+ } catch (e) {
20
+ throw new Error(`Invalid JSON in --data: ${e instanceof Error ? e.message : e}`);
21
+ }
22
+
23
+ const tx = await client.eas.multiTimestamp(items);
24
+
25
+ if (opts.dryRun) {
26
+ const gasEstimate = await tx.estimateGas();
27
+ output({ success: true, data: { dryRun: true, estimatedGas: gasEstimate.toString(), chain: opts.chain } });
28
+ } else {
29
+ const timestamps = await tx.wait();
30
+ output({
31
+ success: true,
32
+ data: {
33
+ timestamps: timestamps.map((t) => t.toString()),
34
+ count: timestamps.length,
35
+ txHash: tx.receipt!.hash,
36
+ chain: opts.chain,
37
+ },
38
+ });
39
+ }
40
+ } catch (err) {
41
+ handleError(err);
42
+ }
43
+ });
@@ -0,0 +1,78 @@
1
+ import { Command } from 'commander';
2
+ import {
3
+ SchemaEncoder,
4
+ NO_EXPIRATION,
5
+ ZERO_BYTES32,
6
+ createOffchainURL,
7
+ } from '@ethereum-attestation-service/eas-sdk';
8
+ import { createEASClient } from '../client.js';
9
+ import { output, handleError } from '../output.js';
10
+ import { resolveInput } from '../stdin.js';
11
+ import { validateAddress, validateBytes32 } from '../validation.js';
12
+ import { EASSCAN_URLS } from '../graphql.js';
13
+
14
+ export const offchainAttestCommand = new Command('offchain-attest')
15
+ .description('Create an off-chain attestation (signed but not submitted on-chain)')
16
+ .requiredOption('-s, --schema <uid>', 'Schema UID')
17
+ .requiredOption('-d, --data <json>', 'Attestation data as JSON array: [{"name":"field","type":"uint256","value":"123"}]')
18
+ .option('-r, --recipient <address>', 'Recipient address', '0x0000000000000000000000000000000000000000')
19
+ .option('--ref-uid <uid>', 'Referenced attestation UID', ZERO_BYTES32)
20
+ .option('--expiration <timestamp>', 'Expiration timestamp (0 for none)', '0')
21
+ .option('--revocable', 'Whether the attestation is revocable', true)
22
+ .option('--no-revocable', 'Make the attestation non-revocable')
23
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
24
+ .option('--rpc-url <url>', 'Custom RPC URL')
25
+ .action(async (opts) => {
26
+ try {
27
+ validateBytes32(opts.schema, 'schema UID');
28
+ if (opts.recipient !== '0x0000000000000000000000000000000000000000') {
29
+ validateAddress(opts.recipient, 'recipient');
30
+ }
31
+
32
+ const client = createEASClient(opts.chain, opts.rpcUrl);
33
+
34
+ const rawData = await resolveInput(opts.data);
35
+ let dataItems;
36
+ try {
37
+ dataItems = JSON.parse(rawData);
38
+ } catch (e) {
39
+ throw new Error(`Invalid JSON in --data: ${e instanceof Error ? e.message : e}`);
40
+ }
41
+ const schemaString = dataItems.map((item: any) => `${item.type} ${item.name}`).join(', ');
42
+ const encoder = new SchemaEncoder(schemaString);
43
+ const encodedData = encoder.encodeData(dataItems);
44
+
45
+ const offchain = await client.eas.getOffchain();
46
+
47
+ const attestation = await offchain.signOffchainAttestation(
48
+ {
49
+ schema: opts.schema,
50
+ recipient: opts.recipient,
51
+ expirationTime: BigInt(opts.expiration) === 0n ? NO_EXPIRATION : BigInt(opts.expiration),
52
+ revocable: opts.revocable,
53
+ refUID: opts.refUid,
54
+ data: encodedData,
55
+ time: BigInt(Math.floor(Date.now() / 1000)),
56
+ },
57
+ client.signer
58
+ );
59
+
60
+ const pkg = { sig: attestation, signer: client.address };
61
+ const urlPath = createOffchainURL(pkg);
62
+ const host = EASSCAN_URLS[opts.chain] || EASSCAN_URLS['ethereum'];
63
+ const offchainUrl = `${host}${urlPath}`;
64
+
65
+ output({
66
+ success: true,
67
+ data: {
68
+ uid: attestation.uid,
69
+ offchainUrl,
70
+ attester: client.address,
71
+ chain: opts.chain,
72
+ attestation: attestation as unknown as Record<string, unknown>,
73
+ },
74
+ });
75
+ } catch (err) {
76
+ handleError(err);
77
+ }
78
+ });
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander';
2
+ import { graphqlQuery, QUERIES } from '../graphql.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { validateBytes32 } from '../validation.js';
5
+
6
+ export const queryAttestationCommand = new Command('query-attestation')
7
+ .description('Query an attestation from the EAS GraphQL API')
8
+ .requiredOption('-u, --uid <uid>', 'Attestation UID')
9
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
10
+ .action(async (opts) => {
11
+ try {
12
+ validateBytes32(opts.uid, 'attestation UID');
13
+
14
+ const data = await graphqlQuery(opts.chain, QUERIES.getAttestation, { id: opts.uid });
15
+
16
+ if (!data.attestation) {
17
+ throw new Error(`Attestation ${opts.uid} not found on ${opts.chain}`);
18
+ }
19
+
20
+ const att = data.attestation;
21
+ if (att.decodedDataJson) {
22
+ try {
23
+ att.decodedData = JSON.parse(att.decodedDataJson);
24
+ } catch { /* keep raw */ }
25
+ }
26
+
27
+ output({ success: true, data: att });
28
+ } catch (err) {
29
+ handleError(err);
30
+ }
31
+ });
@@ -0,0 +1,57 @@
1
+ import { Command } from 'commander';
2
+ import { graphqlQuery, QUERIES } from '../graphql.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { validateAddress, validateBytes32 } from '../validation.js';
5
+
6
+ export const queryAttestationsCommand = new Command('query-attestations')
7
+ .description('Query attestations by schema or attester from the EAS GraphQL API')
8
+ .option('-s, --schema <uid>', 'Filter by schema UID')
9
+ .option('-a, --attester <address>', 'Filter by attester address')
10
+ .option('-n, --limit <number>', 'Max results to return', '10')
11
+ .option('--skip <number>', 'Number of results to skip (for pagination)', '0')
12
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
13
+ .action(async (opts) => {
14
+ try {
15
+ if (!opts.schema && !opts.attester) {
16
+ throw new Error('Provide at least one filter: --schema or --attester');
17
+ }
18
+ if (opts.schema) validateBytes32(opts.schema, 'schema UID');
19
+ if (opts.attester) validateAddress(opts.attester, 'attester');
20
+
21
+ const take = parseInt(opts.limit, 10);
22
+ const skip = parseInt(opts.skip, 10);
23
+ if (isNaN(take) || take < 1) throw new Error('--limit must be a positive integer');
24
+ if (isNaN(skip) || skip < 0) throw new Error('--skip must be a non-negative integer');
25
+ let data;
26
+
27
+ if (opts.schema) {
28
+ data = await graphqlQuery(opts.chain, QUERIES.getAttestationsBySchema, {
29
+ schemaId: opts.schema,
30
+ take,
31
+ skip,
32
+ });
33
+ } else {
34
+ data = await graphqlQuery(opts.chain, QUERIES.getAttestationsByAttester, {
35
+ attester: opts.attester,
36
+ take,
37
+ skip,
38
+ });
39
+ }
40
+
41
+ const attestations = data.attestations || [];
42
+ for (const att of attestations) {
43
+ if (att.decodedDataJson) {
44
+ try {
45
+ att.decodedData = JSON.parse(att.decodedDataJson);
46
+ } catch { /* keep raw */ }
47
+ }
48
+ }
49
+
50
+ output({
51
+ success: true,
52
+ data: { count: attestations.length, attestations },
53
+ });
54
+ } catch (err) {
55
+ handleError(err);
56
+ }
57
+ });
@@ -0,0 +1,24 @@
1
+ import { Command } from 'commander';
2
+ import { graphqlQuery, QUERIES } from '../graphql.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { validateBytes32 } from '../validation.js';
5
+
6
+ export const querySchemaCommand = new Command('query-schema')
7
+ .description('Query a schema from the EAS GraphQL API')
8
+ .requiredOption('-u, --uid <uid>', 'Schema UID')
9
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
10
+ .action(async (opts) => {
11
+ try {
12
+ validateBytes32(opts.uid, 'schema UID');
13
+
14
+ const data = await graphqlQuery(opts.chain, QUERIES.getSchema, { id: opts.uid });
15
+
16
+ if (!data.schema) {
17
+ throw new Error(`Schema ${opts.uid} not found on ${opts.chain}`);
18
+ }
19
+
20
+ output({ success: true, data: data.schema });
21
+ } catch (err) {
22
+ handleError(err);
23
+ }
24
+ });
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander';
2
+ import { graphqlQuery, QUERIES } from '../graphql.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { validateAddress } from '../validation.js';
5
+
6
+ export const querySchemasCommand = new Command('query-schemas')
7
+ .description('Query schemas by creator from the EAS GraphQL API')
8
+ .requiredOption('-a, --creator <address>', 'Creator address')
9
+ .option('-n, --limit <number>', 'Max results to return', '10')
10
+ .option('--skip <number>', 'Number of results to skip (for pagination)', '0')
11
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
12
+ .action(async (opts) => {
13
+ try {
14
+ validateAddress(opts.creator, 'creator');
15
+
16
+ const take = parseInt(opts.limit, 10);
17
+ const skip = parseInt(opts.skip, 10);
18
+ if (isNaN(take) || take < 1) throw new Error('--limit must be a positive integer');
19
+ if (isNaN(skip) || skip < 0) throw new Error('--skip must be a non-negative integer');
20
+ const data = await graphqlQuery(opts.chain, QUERIES.getSchemata, {
21
+ creator: opts.creator,
22
+ take,
23
+ skip,
24
+ });
25
+
26
+ const schemas = data.schemata || [];
27
+
28
+ output({
29
+ success: true,
30
+ data: { count: schemas.length, schemas },
31
+ });
32
+ } catch (err) {
33
+ handleError(err);
34
+ }
35
+ });
@@ -0,0 +1,48 @@
1
+ import { Command } from 'commander';
2
+ import { createEASClient } from '../client.js';
3
+ import { output, handleError } from '../output.js';
4
+ import { validateBytes32 } from '../validation.js';
5
+
6
+ export const revokeCommand = new Command('revoke')
7
+ .description('Revoke an on-chain attestation')
8
+ .requiredOption('-s, --schema <uid>', 'Schema UID of the attestation')
9
+ .requiredOption('-u, --uid <uid>', 'Attestation UID to revoke')
10
+ .option('--value <wei>', 'ETH value to send (in wei)', '0')
11
+ .option('-c, --chain <name>', 'Chain name', 'ethereum')
12
+ .option('--rpc-url <url>', 'Custom RPC URL')
13
+ .option('--dry-run', 'Estimate gas without sending the transaction')
14
+ .action(async (opts) => {
15
+ try {
16
+ validateBytes32(opts.schema, 'schema UID');
17
+ validateBytes32(opts.uid, 'attestation UID');
18
+
19
+ const client = createEASClient(opts.chain, opts.rpcUrl);
20
+
21
+ const tx = await client.eas.revoke({
22
+ schema: opts.schema,
23
+ data: {
24
+ uid: opts.uid,
25
+ value: BigInt(opts.value),
26
+ },
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
+ await tx.wait();
34
+ output({
35
+ success: true,
36
+ data: {
37
+ revoked: true,
38
+ uid: opts.uid,
39
+ txHash: tx.receipt!.hash,
40
+ schema: opts.schema,
41
+ chain: opts.chain,
42
+ },
43
+ });
44
+ }
45
+ } catch (err) {
46
+ handleError(err);
47
+ }
48
+ });