feat: mvp of edit commands b018e251
Steve · 2025-10-30 19:05 7 file(s) · +439 −13
src/commands/edit.ts (added) +233 −0
1 +
import {
2 +
	setTextRecord,
3 +
	setAddressRecord,
4 +
	setResolver as setResolverRecord,
5 +
	setPrimaryName as setPrimaryNameRecord,
6 +
	setAbiRecord,
7 +
} from "@ensdomains/ensjs/wallet";
8 +
import { normalize } from "viem/ens";
9 +
import { spinner, walletClient } from "../utils";
10 +
import { encodeAbi } from "@ensdomains/ensjs/utils";
11 +
import { readFile } from "node:fs/promises";
12 +
13 +
export async function setTxt(options: {
14 +
	name: string;
15 +
	record: string;
16 +
	value: string;
17 +
	resolverAddress?: string;
18 +
}) {
19 +
	try {
20 +
		spinner.start();
21 +
		const wallet = await walletClient();
22 +
23 +
		if (!wallet) {
24 +
			spinner.stop();
25 +
			console.error(
26 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
27 +
			);
28 +
			return;
29 +
		}
30 +
31 +
		// Get resolver if not provided
32 +
		let resolverAddress = options.resolverAddress;
33 +
		if (!resolverAddress) {
34 +
			const { ensClient } = await import("../utils");
35 +
			const resolver = await ensClient.getEnsResolver({
36 +
				name: normalize(options.name),
37 +
			});
38 +
			resolverAddress = resolver || undefined;
39 +
		}
40 +
41 +
		if (!resolverAddress) {
42 +
			spinner.stop();
43 +
			console.error("Error: No resolver found for this name");
44 +
			return;
45 +
		}
46 +
47 +
		const hash = await setTextRecord(wallet, {
48 +
			name: options.name,
49 +
			key: options.record,
50 +
			value: options.value,
51 +
			resolverAddress: resolverAddress as `0x${string}`,
52 +
		});
53 +
54 +
		spinner.stop();
55 +
		console.log(`✓ TXT record set successfully`);
56 +
		console.log(`Transaction hash: ${hash}`);
57 +
	} catch (error) {
58 +
		const e = error as { shortMessage?: string; message: string };
59 +
		spinner.stop();
60 +
		console.error("Error setting TXT record:", e.shortMessage || e.message);
61 +
	}
62 +
}
63 +
64 +
export async function setAddress(options: {
65 +
	name: string;
66 +
	coin: string;
67 +
	value: string;
68 +
	resolverAddress?: string;
69 +
}) {
70 +
	try {
71 +
		spinner.start();
72 +
		const wallet = await walletClient();
73 +
74 +
		if (!wallet) {
75 +
			spinner.stop();
76 +
			console.error(
77 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
78 +
			);
79 +
			return;
80 +
		}
81 +
82 +
		// Get resolver if not provided
83 +
		let resolverAddress = options.resolverAddress;
84 +
		if (!resolverAddress) {
85 +
			const { ensClient } = await import("../utils");
86 +
			const resolver = await ensClient.getEnsResolver({
87 +
				name: normalize(options.name),
88 +
			});
89 +
			resolverAddress = resolver || undefined;
90 +
		}
91 +
92 +
		if (!resolverAddress) {
93 +
			spinner.stop();
94 +
			console.error("Error: No resolver found for this name");
95 +
			return;
96 +
		}
97 +
98 +
		const hash = await setAddressRecord(wallet, {
99 +
			name: options.name,
100 +
			coin: options.coin,
101 +
			value: options.value,
102 +
			resolverAddress: resolverAddress as `0x${string}`,
103 +
		});
104 +
105 +
		spinner.stop();
106 +
		console.log(`✓ Address record set successfully`);
107 +
		console.log(`Transaction hash: ${hash}`);
108 +
	} catch (error) {
109 +
		const e = error as { shortMessage?: string; message: string };
110 +
		spinner.stop();
111 +
		console.error("Error setting address record:", e.shortMessage || e.message);
112 +
	}
113 +
}
114 +
115 +
export async function setResolver(options: {
116 +
	name: string;
117 +
	resolverAddress: string;
118 +
	contract?: "registry" | "nameWrapper";
119 +
}) {
120 +
	try {
121 +
		spinner.start();
122 +
		const wallet = await walletClient();
123 +
124 +
		if (!wallet) {
125 +
			spinner.stop();
126 +
			console.error(
127 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
128 +
			);
129 +
			return;
130 +
		}
131 +
132 +
		const hash = await setResolverRecord(wallet, {
133 +
			name: options.name,
134 +
			contract: options.contract || "registry",
135 +
			resolverAddress: options.resolverAddress as `0x${string}`,
136 +
		});
137 +
138 +
		spinner.stop();
139 +
		console.log(`✓ Resolver set successfully`);
140 +
		console.log(`Transaction hash: ${hash}`);
141 +
	} catch (error) {
142 +
		const e = error as { shortMessage?: string; message: string };
143 +
		spinner.stop();
144 +
		console.error("Error setting resolver:", e.shortMessage || e.message);
145 +
	}
146 +
}
147 +
148 +
export async function setPrimaryName(options: { name: string }) {
149 +
	try {
150 +
		spinner.start();
151 +
		const wallet = await walletClient();
152 +
153 +
		if (!wallet) {
154 +
			spinner.stop();
155 +
			console.error(
156 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
157 +
			);
158 +
			return;
159 +
		}
160 +
161 +
		const hash = await setPrimaryNameRecord(wallet, {
162 +
			name: options.name,
163 +
		});
164 +
165 +
		spinner.stop();
166 +
		console.log(`✓ Primary name set successfully`);
167 +
		console.log(`Transaction hash: ${hash}`);
168 +
	} catch (error) {
169 +
		const e = error as { shortMessage?: string; message: string };
170 +
		spinner.stop();
171 +
		console.error("Error setting primary name:", e.shortMessage || e.message);
172 +
	}
173 +
}
174 +
175 +
export async function setAbi(options: {
176 +
	name: string;
177 +
	abiPath: string;
178 +
	encodeAs?: "json" | "zlib" | "cbor" | "uri";
179 +
	resolverAddress?: string;
180 +
}) {
181 +
	try {
182 +
		spinner.start();
183 +
		const wallet = await walletClient();
184 +
185 +
		if (!wallet) {
186 +
			spinner.stop();
187 +
			console.error(
188 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
189 +
			);
190 +
			return;
191 +
		}
192 +
193 +
		// Read ABI file
194 +
		const abiContent = await readFile(options.abiPath, "utf-8");
195 +
		const abi = JSON.parse(abiContent);
196 +
197 +
		// Encode ABI
198 +
		const encodedAbi = await encodeAbi({
199 +
			encodeAs: options.encodeAs || "json",
200 +
			data: abi,
201 +
		});
202 +
203 +
		// Get resolver if not provided
204 +
		let resolverAddress = options.resolverAddress;
205 +
		if (!resolverAddress) {
206 +
			const { ensClient } = await import("../utils");
207 +
			const resolver = await ensClient.getEnsResolver({
208 +
				name: normalize(options.name),
209 +
			});
210 +
			resolverAddress = resolver || undefined;
211 +
		}
212 +
213 +
		if (!resolverAddress) {
214 +
			spinner.stop();
215 +
			console.error("Error: No resolver found for this name");
216 +
			return;
217 +
		}
218 +
219 +
		const hash = await setAbiRecord(wallet, {
220 +
			name: options.name,
221 +
			encodedAbi,
222 +
			resolverAddress: resolverAddress as `0x${string}`,
223 +
		});
224 +
225 +
		spinner.stop();
226 +
		console.log(`✓ ABI record set successfully`);
227 +
		console.log(`Transaction hash: ${hash}`);
228 +
	} catch (error) {
229 +
		const e = error as { shortMessage?: string; message: string };
230 +
		spinner.stop();
231 +
		console.error("Error setting ABI record:", e.shortMessage || e.message);
232 +
	}
233 +
}
src/commands/index.ts +1 −0
1 1
export * from "./profile";
2 2
export * from "./resolve";
3 3
export * from "./utils";
4 +
export * from "./edit";
src/commands/profile.ts +10 −0
125 125
		return;
126 126
	}
127 127
128 +
	// Note: If resolverAddress is provided, inform user it's for reference
129 +
	if (options.resolverAddress) {
130 +
		spinner.stop();
131 +
		console.log(`Note: Using custom resolver: ${options.resolverAddress}`);
132 +
		console.log(
133 +
			"(Custom resolver support for read operations is limited in current ENS.js version)\n",
134 +
		);
135 +
		spinner.start();
136 +
	}
137 +
128 138
	try {
129 139
		const subgraphRecords = await getSubgraphRecords(ensClient, {
130 140
			name: name as string,
src/commands/resolve.ts +10 −0
18 18
		return;
19 19
	}
20 20
21 +
	// Note: If resolverAddress is provided, inform user it's for reference
22 +
	if (options.resolverAddress) {
23 +
		spinner.stop();
24 +
		console.log(`Note: Using custom resolver: ${options.resolverAddress}`);
25 +
		console.log(
26 +
			"(Custom resolver support for read operations is limited in current ENS.js version)\n",
27 +
		);
28 +
		spinner.start();
29 +
	}
30 +
21 31
	// Handle TXT
22 32
	if (options.txt) {
23 33
		try {
src/index.ts +166 −12
17 17
	getResolver,
18 18
	profile as profileCmd,
19 19
	resolve as resolveCmd,
20 +
	setTxt as setTxtCmd,
21 +
	setAddress as setAddressCmd,
22 +
	setResolver as setResolverCmd,
23 +
	setPrimaryName as setPrimaryNameCmd,
24 +
	setAbi as setAbiCmd,
20 25
} from "./commands";
21 26
22 27
const resolve = command({
43 48
			long: "chain",
44 49
			description: "Get address for a specific chain",
45 50
		}),
51 +
		resolverAddress: option({
52 +
			type: optional(string),
53 +
			long: "resolver",
54 +
			short: "r",
55 +
			description: "Specify a custom resolver address",
56 +
		}),
46 57
	},
47 58
	handler: async (args) => {
48 59
		if (!args.input) {
62 73
		input: positional({
63 74
			type: string,
64 75
			description: "Provide either an address or an ENS name to resolve it",
76 +
		}),
77 +
		resolverAddress: option({
78 +
			type: optional(string),
79 +
			long: "resolver",
80 +
			short: "r",
81 +
			description: "Specify a custom resolver address",
65 82
		}),
66 83
	},
67 84
	handler: async (args) => {
148 165
		}),
149 166
		record: positional({
150 167
			type: string,
151 -
			description: "The type of TXT you want to update, e.g. .com.discord",
168 +
			description: "The type of TXT you want to update, e.g. com.discord",
152 169
		}),
153 170
		value: positional({
154 171
			type: string,
155 -
			description: "Value of the TXT record being set, e.g. @myusername",
172 +
			description: "Value of the TXT record being set, e.g. myusername",
173 +
		}),
174 +
		resolverAddress: option({
175 +
			type: optional(string),
176 +
			long: "resolver",
177 +
			short: "r",
178 +
			description:
179 +
				"Resolver address (optional, will auto-detect if not provided)",
156 180
		}),
157 181
	},
158 182
	handler: async (args) => {
159 -
		if (!args.name) {
160 -
			console.log("Please provide an ENS Name `atlas resolver <vitalik.eth>`");
183 +
		if (!args.name || !args.record || !args.value) {
184 +
			console.log(
185 +
				"Please provide all required arguments: `atlas edit txt <name> <record> <value>`",
186 +
			);
161 187
			return;
162 188
		}
163 -
		await getResolver(args);
189 +
		await setTxtCmd(args);
164 190
	},
165 191
});
166 192
167 -
const setAddress = command({});
193 +
const editAddress = command({
194 +
	name: "address",
195 +
	description: "Set address record for a specific coin/chain",
196 +
	args: {
197 +
		name: positional({
198 +
			type: string,
199 +
			description: "Target ENS name",
200 +
		}),
201 +
		coin: positional({
202 +
			type: string,
203 +
			description: "Coin/chain identifier (e.g. ETH, BTC, SOL)",
204 +
		}),
205 +
		value: positional({
206 +
			type: string,
207 +
			description: "Address value to set",
208 +
		}),
209 +
		resolverAddress: option({
210 +
			type: optional(string),
211 +
			long: "resolver",
212 +
			short: "r",
213 +
			description:
214 +
				"Resolver address (optional, will auto-detect if not provided)",
215 +
		}),
216 +
	},
217 +
	handler: async (args) => {
218 +
		if (!args.name || !args.coin || !args.value) {
219 +
			console.log(
220 +
				"Please provide all required arguments: `atlas edit address <name> <coin> <value>`",
221 +
			);
222 +
			return;
223 +
		}
224 +
		await setAddressCmd(args);
225 +
	},
226 +
});
168 227
169 -
const setResolver = command({});
228 +
const editResolver = command({
229 +
	name: "resolver",
230 +
	description: "Set the resolver for an ENS name",
231 +
	args: {
232 +
		name: positional({
233 +
			type: string,
234 +
			description: "Target ENS name",
235 +
		}),
236 +
		resolverAddress: positional({
237 +
			type: string,
238 +
			description: "New resolver address",
239 +
		}),
240 +
		contract: option({
241 +
			type: optional(string),
242 +
			long: "contract",
243 +
			short: "c",
244 +
			description:
245 +
				"Contract to use: registry or nameWrapper (default: registry)",
246 +
		}),
247 +
	},
248 +
	handler: async (args) => {
249 +
		if (!args.name || !args.resolverAddress) {
250 +
			console.log(
251 +
				"Please provide all required arguments: `atlas edit resolver <name> <resolverAddress>`",
252 +
			);
253 +
			return;
254 +
		}
255 +
		await setResolverCmd({
256 +
			...args,
257 +
			contract: (args.contract as "registry" | "nameWrapper") || "registry",
258 +
		});
259 +
	},
260 +
});
170 261
171 -
const setPrimaryName = command({});
262 +
const editPrimaryName = command({
263 +
	name: "primary",
264 +
	description: "Set the primary ENS name for your address",
265 +
	args: {
266 +
		name: positional({
267 +
			type: string,
268 +
			description: "ENS name to set as primary",
269 +
		}),
270 +
	},
271 +
	handler: async (args) => {
272 +
		if (!args.name) {
273 +
			console.log(
274 +
				"Please provide an ENS name: `atlas edit primary <vitalik.eth>`",
275 +
			);
276 +
			return;
277 +
		}
278 +
		await setPrimaryNameCmd(args);
279 +
	},
280 +
});
172 281
173 -
const setAbi = command({});
282 +
const editAbi = command({
283 +
	name: "abi",
284 +
	description: "Set ABI record for an ENS name",
285 +
	args: {
286 +
		name: positional({
287 +
			type: string,
288 +
			description: "Target ENS name",
289 +
		}),
290 +
		abiPath: positional({
291 +
			type: string,
292 +
			description: "Path to ABI JSON file",
293 +
		}),
294 +
		encodeAs: option({
295 +
			type: optional(string),
296 +
			long: "encode",
297 +
			short: "e",
298 +
			description: "Encoding format: json, zlib, cbor, or uri (default: json)",
299 +
		}),
300 +
		resolverAddress: option({
301 +
			type: optional(string),
302 +
			long: "resolver",
303 +
			short: "r",
304 +
			description:
305 +
				"Resolver address (optional, will auto-detect if not provided)",
306 +
		}),
307 +
	},
308 +
	handler: async (args) => {
309 +
		if (!args.name || !args.abiPath) {
310 +
			console.log(
311 +
				"Please provide all required arguments: `atlas edit abi <name> <abiPath>`",
312 +
			);
313 +
			return;
314 +
		}
315 +
		await setAbiCmd({
316 +
			...args,
317 +
			encodeAs: (args.encodeAs as "json" | "zlib" | "cbor" | "uri") || "json",
318 +
		});
319 +
	},
320 +
});
174 321
175 -
const set = subcommands({
322 +
const edit = subcommands({
176 323
	name: "edit",
177 -
	description: "Edit records an ENS name",
178 -
	cmds: { editTxt },
324 +
	description: "Edit records for an ENS name",
325 +
	cmds: {
326 +
		txt: editTxt,
327 +
		address: editAddress,
328 +
		resolver: editResolver,
329 +
		primaryName: editPrimaryName,
330 +
		abi: editAbi,
331 +
	},
179 332
});
180 333
181 334
const cli = subcommands({
207 360
		labelhash,
208 361
		resolver,
209 362
		deployments,
363 +
		edit,
210 364
	},
211 365
});
212 366
src/utils/types.ts +1 −0
5 5
	chain?: string;
6 6
	contenthash?: boolean;
7 7
	txt?: string;
8 +
	resolverAddress?: string;
8 9
};
src/utils/viem.ts +18 −1
1 1
import { addEnsContracts } from "@ensdomains/ensjs";
2 -
import { createPublicClient, http } from "viem";
2 +
import { createPublicClient, createWalletClient, http } from "viem";
3 3
import { mainnet } from "viem/chains";
4 +
import { privateKeyToAccount } from "viem/accounts";
4 5
5 6
export const ensClient = createPublicClient({
6 7
	chain: addEnsContracts(mainnet),
7 8
	transport: http(process.env.ETH_RPC_URL || "https://eth.drpc.org"),
8 9
});
10 +
11 +
export async function walletClient() {
12 +
	const privateKey = process.env.ATLAS_PRIVATE_KEY;
13 +
14 +
	if (!privateKey) {
15 +
		return null;
16 +
	}
17 +
18 +
	const account = privateKeyToAccount(privateKey as `0x${string}`);
19 +
20 +
	return createWalletClient({
21 +
		account,
22 +
		chain: addEnsContracts(mainnet),
23 +
		transport: http(process.env.ETH_RPC_URL || "https://eth.drpc.org"),
24 +
	});
25 +
}