Merge pull request #1 from stevedylandev/feat/edits bb03e28a
feat/edits
Steve Simkins · 2025-10-30 22:55 8 file(s) · +607 −1
README.md +86 −0
129 129
#   ....
130 130
```
131 131
132 +
## Edit Commands
133 +
134 +
Edit commands require setting the `ATLAS_PRIVATE_KEY` environment variable with your wallet's private key to sign transactions. A great way to do this is to use the Foundry `cast` utility within a shell session.
135 +
136 +
```bash
137 +
# By running the command below you export the env variable into your shell session
138 +
139 +
export ATLAS_PRIVATE_KEY=$(cast wallet private-key --account someaccount)
140 +
```
141 +
142 +
> [!WARNING]
143 +
> I would **not** recommend making this environment variable permanent in your shell configurtation files! Use it during the session then close the session to keep the key encrypted locally
144 +
145 +
### `edit txt`
146 +
Set or clear a text record for an ENS name
147 +
148 +
```bash
149 +
# Set a text record
150 +
atlas edit txt myname.eth com.github myusername
151 +
152 +
# Set a Discord username
153 +
atlas edit txt myname.eth com.discord mydiscord#1234
154 +
155 +
# Clear a text record by passing 'null'
156 +
atlas edit txt myname.eth com.github null
157 +
158 +
# Specify a custom resolver address
159 +
atlas edit txt myname.eth com.twitter myhandle --resolver 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
160 +
```
161 +
162 +
### `edit address`
163 +
Set or clear an address record for a specific coin/chain
164 +
165 +
```bash
166 +
# Set an ETH address
167 +
atlas edit address myname.eth ETH 0x1234567890123456789012345678901234567890
168 +
169 +
# Set a Bitcoin address
170 +
atlas edit address myname.eth BTC bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
171 +
172 +
# Set a Solana address
173 +
atlas edit address myname.eth SOL 7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV
174 +
175 +
# Clear an address record by passing 'null'
176 +
atlas edit address myname.eth BTC null
177 +
178 +
# Specify a custom resolver address
179 +
atlas edit address myname.eth ETH 0x1234... --resolver 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
180 +
```
181 +
182 +
### `edit resolver`
183 +
Set the resolver for an ENS name
184 +
185 +
```bash
186 +
# Set resolver using registry contract (default)
187 +
atlas edit resolver myname.eth 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
188 +
189 +
# Set resolver using nameWrapper contract
190 +
atlas edit resolver myname.eth 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63 --contract nameWrapper
191 +
```
192 +
193 +
### `edit primary`
194 +
Set the primary ENS name for your address (reverse record)
195 +
196 +
```bash
197 +
# Set primary name
198 +
atlas edit primary myname.eth
199 +
```
200 +
201 +
### `edit abi`
202 +
Set or clear an ABI record for an ENS name
203 +
204 +
```bash
205 +
# Set ABI from a JSON file
206 +
atlas edit abi myname.eth ./contract-abi.json
207 +
208 +
# Set ABI with specific encoding
209 +
atlas edit abi myname.eth ./contract-abi.json --encode zlib
210 +
211 +
# Clear an ABI record by passing 'null'
212 +
atlas edit abi myname.eth null
213 +
214 +
# Specify a custom resolver address
215 +
atlas edit abi myname.eth ./contract-abi.json --resolver 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63
216 +
```
217 +
132 218
## Development
133 219
134 220
Make sure [Bun](https://bun.sh) is installed
src/commands/edit.ts (added) +271 −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 +
		if (options.value === "") {
56 +
			console.log(`✓ TXT record cleared successfully`);
57 +
		} else {
58 +
			console.log(`✓ TXT record set successfully`);
59 +
		}
60 +
		console.log(`Transaction hash: ${hash}`);
61 +
	} catch (error) {
62 +
		const e = error as { shortMessage?: string; message: string };
63 +
		spinner.stop();
64 +
		console.error("Error setting TXT record:", e.shortMessage || e.message);
65 +
		console.error(
66 +
			"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
67 +
		);
68 +
	}
69 +
}
70 +
71 +
export async function setAddress(options: {
72 +
	name: string;
73 +
	coin: string;
74 +
	value: string;
75 +
	resolverAddress?: string;
76 +
}) {
77 +
	try {
78 +
		spinner.start();
79 +
		const wallet = await walletClient();
80 +
81 +
		if (!wallet) {
82 +
			spinner.stop();
83 +
			console.error(
84 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
85 +
			);
86 +
			return;
87 +
		}
88 +
89 +
		// Get resolver if not provided
90 +
		let resolverAddress = options.resolverAddress;
91 +
		if (!resolverAddress) {
92 +
			const { ensClient } = await import("../utils");
93 +
			const resolver = await ensClient.getEnsResolver({
94 +
				name: normalize(options.name),
95 +
			});
96 +
			resolverAddress = resolver || undefined;
97 +
		}
98 +
99 +
		if (!resolverAddress) {
100 +
			spinner.stop();
101 +
			console.error("Error: No resolver found for this name");
102 +
			return;
103 +
		}
104 +
105 +
		const hash = await setAddressRecord(wallet, {
106 +
			name: options.name,
107 +
			coin: options.coin,
108 +
			value: options.value === "null" ? null : options.value,
109 +
			resolverAddress: resolverAddress as `0x${string}`,
110 +
		});
111 +
112 +
		spinner.stop();
113 +
		if (options.value === "null") {
114 +
			console.log(`✓ Address record cleared successfully`);
115 +
		} else {
116 +
			console.log(`✓ Address record set successfully`);
117 +
		}
118 +
		console.log(`Transaction hash: ${hash}`);
119 +
	} catch (error) {
120 +
		const e = error as { shortMessage?: string; message: string };
121 +
		spinner.stop();
122 +
		console.error("Error setting address record:", e.shortMessage || e.message);
123 +
		console.error(
124 +
			"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
125 +
		);
126 +
	}
127 +
}
128 +
129 +
export async function setResolver(options: {
130 +
	name: string;
131 +
	resolverAddress: string;
132 +
	contract?: "registry" | "nameWrapper";
133 +
}) {
134 +
	try {
135 +
		spinner.start();
136 +
		const wallet = await walletClient();
137 +
138 +
		if (!wallet) {
139 +
			spinner.stop();
140 +
			console.error(
141 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
142 +
			);
143 +
			return;
144 +
		}
145 +
146 +
		const hash = await setResolverRecord(wallet, {
147 +
			name: options.name,
148 +
			contract: options.contract || "registry",
149 +
			resolverAddress: options.resolverAddress as `0x${string}`,
150 +
		});
151 +
152 +
		spinner.stop();
153 +
		console.log(`✓ Resolver set successfully`);
154 +
		console.log(`Transaction hash: ${hash}`);
155 +
	} catch (error) {
156 +
		const e = error as { shortMessage?: string; message: string };
157 +
		spinner.stop();
158 +
		console.error("Error setting resolver:", e.shortMessage || e.message);
159 +
		console.error(
160 +
			"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
161 +
		);
162 +
	}
163 +
}
164 +
165 +
export async function setPrimaryName(options: { name: string }) {
166 +
	try {
167 +
		spinner.start();
168 +
		const wallet = await walletClient();
169 +
170 +
		if (!wallet) {
171 +
			spinner.stop();
172 +
			console.error(
173 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
174 +
			);
175 +
			return;
176 +
		}
177 +
178 +
		const hash = await setPrimaryNameRecord(wallet, {
179 +
			name: options.name,
180 +
		});
181 +
182 +
		spinner.stop();
183 +
		console.log(`✓ Primary name set successfully`);
184 +
		console.log(`Transaction hash: ${hash}`);
185 +
	} catch (error) {
186 +
		const e = error as { shortMessage?: string; message: string };
187 +
		spinner.stop();
188 +
		console.error("Error setting primary name:", e.shortMessage || e.message);
189 +
		console.error(
190 +
			"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
191 +
		);
192 +
	}
193 +
}
194 +
195 +
export async function setAbi(options: {
196 +
	name: string;
197 +
	abiPath: string;
198 +
	encodeAs?: "json" | "zlib" | "cbor" | "uri";
199 +
	resolverAddress?: string;
200 +
}) {
201 +
	try {
202 +
		spinner.start();
203 +
		const wallet = await walletClient();
204 +
205 +
		if (!wallet) {
206 +
			spinner.stop();
207 +
			console.error(
208 +
				"Error: Wallet not configured. Please set ATLAS_PRIVATE_KEY environment variable.",
209 +
			);
210 +
			return;
211 +
		}
212 +
213 +
		let encodedAbi: `0x${string}`;
214 +
215 +
		// Handle null case to clear ABI
216 +
		if (options.abiPath === "null") {
217 +
			// Encode empty ABI to clear the record
218 +
			encodedAbi = await encodeAbi({
219 +
				encodeAs: options.encodeAs || "json",
220 +
				data: null,
221 +
			});
222 +
		} else {
223 +
			// Read ABI file
224 +
			const abiContent = await readFile(options.abiPath, "utf-8");
225 +
			const abi = JSON.parse(abiContent);
226 +
227 +
			// Encode ABI
228 +
			encodedAbi = await encodeAbi({
229 +
				encodeAs: options.encodeAs || "json",
230 +
				data: abi,
231 +
			});
232 +
		}
233 +
234 +
		// Get resolver if not provided
235 +
		let resolverAddress = options.resolverAddress;
236 +
		if (!resolverAddress) {
237 +
			const { ensClient } = await import("../utils");
238 +
			const resolver = await ensClient.getEnsResolver({
239 +
				name: normalize(options.name),
240 +
			});
241 +
			resolverAddress = resolver || undefined;
242 +
		}
243 +
244 +
		if (!resolverAddress) {
245 +
			spinner.stop();
246 +
			console.error("Error: No resolver found for this name");
247 +
			return;
248 +
		}
249 +
250 +
		const hash = await setAbiRecord(wallet, {
251 +
			name: options.name,
252 +
			encodedAbi,
253 +
			resolverAddress: resolverAddress as `0x${string}`,
254 +
		});
255 +
256 +
		spinner.stop();
257 +
		if (options.abiPath === "null") {
258 +
			console.log(`✓ ABI record cleared successfully`);
259 +
		} else {
260 +
			console.log(`✓ ABI record set successfully`);
261 +
		}
262 +
		console.log(`Transaction hash: ${hash}`);
263 +
	} catch (error) {
264 +
		const e = error as { shortMessage?: string; message: string };
265 +
		spinner.stop();
266 +
		console.error("Error setting ABI record:", e.shortMessage || e.message);
267 +
		console.error(
268 +
			"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
269 +
		);
270 +
	}
271 +
}
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 +19 −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 {
31 41
			const e = error as { shortMessage: string };
32 42
			spinner.stop();
33 43
			console.error("Error fetching TXT record:", e.shortMessage);
44 +
			console.error(
45 +
				"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
46 +
			);
34 47
		}
35 48
		return;
36 49
	}
47 60
			const e = error as { shortMessage: string };
48 61
			spinner.stop();
49 62
			console.error("Error fetching content hash:", e.shortMessage);
63 +
			console.error(
64 +
				"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
65 +
			);
50 66
		}
51 67
		return;
52 68
	}
63 79
			const e = error as { shortMessage: string };
64 80
			spinner.stop();
65 81
			console.error("Error fetching chain record:", e.shortMessage);
82 +
			console.error(
83 +
				"If you are receiving HTTP errors consider setting ETH_RPC_URL as an environemnt variable",
84 +
			);
66 85
		}
67 86
		spinner.stop();
68 87
		return;
src/index.ts +201 −0
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) => {
138 155
	},
139 156
});
140 157
158 +
const editTxt = command({
159 +
	name: "txt",
160 +
	description:
161 +
		"Set TXT record for an ENS name (use 'null' to clear the record)",
162 +
	args: {
163 +
		name: positional({
164 +
			type: string,
165 +
			description: "Target ENS name",
166 +
		}),
167 +
		record: positional({
168 +
			type: string,
169 +
			description: "The type of TXT you want to update, e.g. com.discord",
170 +
		}),
171 +
		value: positional({
172 +
			type: string,
173 +
			description:
174 +
				"Value of the TXT record being set (use 'null' to clear), e.g. myusername",
175 +
		}),
176 +
		resolverAddress: option({
177 +
			type: optional(string),
178 +
			long: "resolver",
179 +
			short: "r",
180 +
			description:
181 +
				"Resolver address (optional, will auto-detect if not provided)",
182 +
		}),
183 +
	},
184 +
	handler: async (args) => {
185 +
		if (!args.name || !args.record || args.value === undefined) {
186 +
			console.log(
187 +
				"Please provide all required arguments: `atlas edit txt <name> <record> <value>`",
188 +
			);
189 +
			return;
190 +
		}
191 +
		await setTxtCmd({
192 +
			...args,
193 +
			value: args.value === "null" ? "" : args.value,
194 +
		});
195 +
	},
196 +
});
197 +
198 +
const editAddress = command({
199 +
	name: "address",
200 +
	description: "Set address record for a specific coin/chain",
201 +
	args: {
202 +
		name: positional({
203 +
			type: string,
204 +
			description: "Target ENS name",
205 +
		}),
206 +
		coin: positional({
207 +
			type: string,
208 +
			description: "Coin/chain identifier (e.g. ETH, BTC, SOL)",
209 +
		}),
210 +
		value: positional({
211 +
			type: string,
212 +
			description: "Address value to set",
213 +
		}),
214 +
		resolverAddress: option({
215 +
			type: optional(string),
216 +
			long: "resolver",
217 +
			short: "r",
218 +
			description:
219 +
				"Resolver address (optional, will auto-detect if not provided)",
220 +
		}),
221 +
	},
222 +
	handler: async (args) => {
223 +
		if (!args.name || !args.coin || !args.value) {
224 +
			console.log(
225 +
				"Please provide all required arguments: `atlas edit address <name> <coin> <value>`",
226 +
			);
227 +
			return;
228 +
		}
229 +
		await setAddressCmd(args);
230 +
	},
231 +
});
232 +
233 +
const editResolver = command({
234 +
	name: "resolver",
235 +
	description: "Set the resolver for an ENS name",
236 +
	args: {
237 +
		name: positional({
238 +
			type: string,
239 +
			description: "Target ENS name",
240 +
		}),
241 +
		resolverAddress: positional({
242 +
			type: string,
243 +
			description: "New resolver address",
244 +
		}),
245 +
		contract: option({
246 +
			type: optional(string),
247 +
			long: "contract",
248 +
			short: "c",
249 +
			description:
250 +
				"Contract to use: registry or nameWrapper (default: registry)",
251 +
		}),
252 +
	},
253 +
	handler: async (args) => {
254 +
		if (!args.name || !args.resolverAddress) {
255 +
			console.log(
256 +
				"Please provide all required arguments: `atlas edit resolver <name> <resolverAddress>`",
257 +
			);
258 +
			return;
259 +
		}
260 +
		await setResolverCmd({
261 +
			...args,
262 +
			contract: (args.contract as "registry" | "nameWrapper") || "registry",
263 +
		});
264 +
	},
265 +
});
266 +
267 +
const editPrimaryName = command({
268 +
	name: "primary",
269 +
	description: "Set the primary ENS name for your address",
270 +
	args: {
271 +
		name: positional({
272 +
			type: string,
273 +
			description: "ENS name to set as primary",
274 +
		}),
275 +
	},
276 +
	handler: async (args) => {
277 +
		if (!args.name) {
278 +
			console.log(
279 +
				"Please provide an ENS name: `atlas edit primary <vitalik.eth>`",
280 +
			);
281 +
			return;
282 +
		}
283 +
		await setPrimaryNameCmd(args);
284 +
	},
285 +
});
286 +
287 +
const editAbi = command({
288 +
	name: "abi",
289 +
	description:
290 +
		"Set ABI record for an ENS name (use 'null' to clear the record)",
291 +
	args: {
292 +
		name: positional({
293 +
			type: string,
294 +
			description: "Target ENS name",
295 +
		}),
296 +
		abiPath: positional({
297 +
			type: string,
298 +
			description: "Path to ABI JSON file (use 'null' to clear)",
299 +
		}),
300 +
		encodeAs: option({
301 +
			type: optional(string),
302 +
			long: "encode",
303 +
			short: "e",
304 +
			description: "Encoding format: json, zlib, cbor, or uri (default: json)",
305 +
		}),
306 +
		resolverAddress: option({
307 +
			type: optional(string),
308 +
			long: "resolver",
309 +
			short: "r",
310 +
			description:
311 +
				"Resolver address (optional, will auto-detect if not provided)",
312 +
		}),
313 +
	},
314 +
	handler: async (args) => {
315 +
		if (!args.name || args.abiPath === undefined) {
316 +
			console.log(
317 +
				"Please provide all required arguments: `atlas edit abi <name> <abiPath>`",
318 +
			);
319 +
			return;
320 +
		}
321 +
		await setAbiCmd({
322 +
			...args,
323 +
			abiPath: args.abiPath,
324 +
			encodeAs: (args.encodeAs as "json" | "zlib" | "cbor" | "uri") || "json",
325 +
		});
326 +
	},
327 +
});
328 +
329 +
const edit = subcommands({
330 +
	name: "edit",
331 +
	description: "Edit records for an ENS name",
332 +
	cmds: {
333 +
		txt: editTxt,
334 +
		address: editAddress,
335 +
		resolver: editResolver,
336 +
		primaryName: editPrimaryName,
337 +
		abi: editAbi,
338 +
	},
339 +
});
340 +
141 341
const cli = subcommands({
142 342
	name: "atlas",
143 343
	description: `
167 367
		labelhash,
168 368
		resolver,
169 369
		deployments,
370 +
		edit,
170 371
	},
171 372
});
172 373
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 +
}