2
0
mirror of synced 2025-02-23 11:38:42 +00:00

Additional sanity checks in ethers-ens.

This commit is contained in:
Richard Moore 2019-08-06 19:11:09 -04:00
parent 9977c9f66a
commit de4b2a449c
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
2 changed files with 386 additions and 80 deletions

220
packages/cli/README.md Normal file
View File

@ -0,0 +1,220 @@
Command-Line Interface (CLI)
============================
The command-line interface provides several simple tools to manage
and debug Ethereum-related tasks using the ethers.js library.
**To install:**
```
/home/ricmoo> npm install -g @ethersproject/cli
```
-----
Sandbox Utility
===============
The sandbox utility run on its own will run a REPL environment similar
to running `node`, with many features from the ethers.js library
already imported and permits loading accounts and setting up a provider.
It also provides a simple interface to common tasks, such as sweeping
accounts, signing messages and compiling Solidity.
**Example:** Create and fund testnet account
```
/home/ricmoo> ethers init ropsten.json
Creating a new JSON Wallet - ropsten.json
Keep this password and file SAFE!! If lost or forgotten
it CANNOT be recovered, by ANYone, EVER.
Choose a password: ****
Confirm password: ****
Encrypting... 100%
New account address: 0xe923a7f82860C30442a1A541C14bE4251bd71A34
Saved: ropsten.json
/home/ricmoo> ethers --wait --network ropsten fund 0xe923a7f82860C30442a1A541C14bE4251bd71A34
Transaction Hash: 0x457c1d8b58170c73a02afa2816e877de41d6337a483d4af9cbd674d2b478473d
/home/ethers>
```
**Example:** Simple evaluations
```
/home/ricmoo> ethers eval 'namehash("ricmoose.eth")'
0xb52c4744695ed3be701ccef35d5901de3aaf7294245966ef16617c30aab7b626
/home/ricmoo> ethers eval 'id("Hello...")'
0x9cd41c139084dafa62261ce045f504e3c697fa303c87a78b241a9f8ae65bae88
/home/ricmoo> ethers --network ropsten eval '(new Contract(provider.network.ensAddress, [ "function owner(bytes32) view returns (address)" ], provider)).owner(namehash("eth"))'
0x227Fcb6Ddf14880413EF4f1A3dF2Bbb32bcb29d7
```
**Example:** REPL
```
/home/ricmoo> ethers --network ropsten --account mnemonic.txt
network: ropsten (chainId: 3)
ropsten> provider.getGasPrice()
BigNumber { _hex: '0xb2d05e00', _isBigNumber: true }
ropsten> accounts[0].signMessage("Hello...");
Message:
Message: "Hello..."
Message (hex): 0x48656c6c6f2e2e2e
Sign Message? (y/N/a) yy
Signature
Flat: 0x37e9add966fe86d50bc5d816f9cb8213107d428551e64f118bc087fe1e4031f61685c7123d14f47e673ada87049cbbecab0c4e9ef5e8ded073e0be9980d14e761b
r: 0x37e9add966fe86d50bc5d816f9cb8213107d428551e64f118bc087fe1e4031f6
s: 0x1685c7123d14f47e673ada87049cbbecab0c4e9ef5e8ded073e0be9980d14e76
vs: 0x1685c7123d14f47e673ada87049cbbecab0c4e9ef5e8ded073e0be9980d14e76
v: 27
recid: 0
'0x37e9add966fe86d50bc5d816f9cb8213107d428551e64f118bc087fe1e4031f61685c7123d14f47e673ada87049cbbecab0c4e9ef5e8ded073e0be9980d14e761b'
```
Help (--help)
-------------
```
Usage:
ethers [ COMMAND ] [ ARGS ] [ OPTIONS ]
COMMANDS (default: sandbox)
sandbox Run a REPL VM environment with ethers
init FILENAME Create a new JSON wallet
[ --force ] Overwrite any existing files
fund TARGET Fund TARGET with testnet ether
info [ TARGET ... ] Dump info for accounts, addresses and ENS names
send TARGET ETHER Send ETHER ether to TARGET form accounts[0]
[ --allow-zero ] Allow sending to the address zero
[ --data DATA ] Include data in the transaction
sweep TARGET Send all ether from accounts[0] to TARGET
sign-message MESSAGE Sign a MESSAGE with accounts[0]
[ --hex ] The message content is hex encoded
eval CODE Run CODE in a VM with ethers
run FILENAME Run FILENAME in a VM with ethers
wait HASH Wait for a transaction HASH to be mined
compile FILENAME Compiles a Solidity contract
[ --no-optimize ] Do not optimize the compiled output
[ --warnings ] Error on any warning
deploy FILENAME Compile and deploy a Solidity contract
[ --no-optimize ] Do not optimize the compiled output
[ --contract NAME ] Specify the contract to deploy
ACCOUNT OPTIONS
--account FILENAME Load from a file (JSON, RAW or mnemonic)
--account RAW_KEY Use a private key (insecure *)
--account 'MNEMONIC' Use a mnemonic (insecure *)
--account - Use secure entry for a raw key or mnemonic
--account-void ADDRESS Use an address as a void signer
--account-void ENS_NAME Add the resolved address as a void signer
--account-rpc ADDRESS Add the address from a JSON-RPC provider
--account-rpc INDEX Add the index from a JSON-RPC provider
--mnemonic-password Prompt for a password for mnemonics
--xxx-mnemonic-password Prompt for a (experimental) hard password
PROVIDER OPTIONS (default: all + homestead)
--alchemy Include Alchemy
--etherscan Include Etherscan
--infura Include INFURA
--nodesmith Include nodesmith
--rpc URL Include a custom JSON-RPC
--offline Dump signed transactions (no send)
--network NETWORK Network to connect to (default: homestead)
TRANSACTION OPTIONS (default: query network)
--gasPrice GWEI Default gas price for transactions(in wei)
--gasLimit GAS Default gas limit for transactions
--nonce NONCE Initial nonce for the first transaction
--yes Always accept Siging and Sending
OTHER OPTIONS
--wait Wait until transactions are mined
--debug Show stack traces for errors
--help Show this usage and exit
--version Show this version and exit
(*) By including mnemonics or private keys on the command line they are
possibly readable by other users on your system and may get stored in
your bash history file. This is NOT recommended.
```
-----
Ethereum Naming Service (ENS)
=============================
These tools help manage ENS names.
Help (--help)
-------------
```
Usage:
ethers-ens COMMAND [ ARGS ] [ OPTIONS ]
COMMANDS
lookup [ NAME | ADDRESS [ ... ] ]
Lookup a name or address
commit NAME Submit a pre-commitment
[ --duration DAYS ] Register duration (default: 365 days)
[ --salt SALT ] SALT to blind the commit with
[ --secret SECRET ] Use id(SECRET) as the salt
[ --owner OWNER ] The target owner (default: current account)
reveal NAME Reveal a previous pre-commitment
[ --duration DAYS ] Register duration (default: 365 days)
[ --salt SALT ] SALT to blind the commit with
[ --secret SECRET ] Use id(SECRET) as the salt
[ --owner OWNER ] The target owner (default: current account)
set-controller NAME Set the controller (default: current account)
[ --address ADDRESS ] Specify another address
set-subnode NAME Set a subnode owner (default: current account)
[ --address ADDRESS ] Specify another address
set-resolver NAME Set the resolver (default: resolver.eth)
[ --address ADDRESS ] Specify another address
set-addr NAME Set the addr record (default: current account)
[ --address ADDRESS ] Specify another address
set-text NAME KEY VALUE Set a text record
set-email NAME EMAIL Set the email text record
set-website NAME URL Set the website text record
set-content NAME HASH Set the IPFS Content Hash
migrate-registrar NAME Migrate from the Legacy to the Permanent Registrar
transfer NAME NEW_OWNER Transfer registrant ownership
reclaim NAME Reset the controller by the registrant
[ --address ADDRESS ] Specify another address
```
See above for the `ACCOUNT`, `PROVIDER`, `TRANSACTION`, and `OTHER`
options.
-----
TypeScript Utility
==================
The TypeScript utility compiles Solidity contracts into a single file
with each contract sub-classing Contract, with all typing information
added.
Help (--help)
-------------
```
Usage:
ethers-ts FILENAME [ ... ] [ OPTIONS ]
OPTIONS
--output FILENAME Write the output to FILENAME (default: stdout)
--force Overwrite files if they already exist
--no-optimize Do not run the solc optimizer
--no-bytecode Do not include bytecode and Factory methods
```
-----
License
=======
MIT License

View File

@ -21,9 +21,12 @@ const ensAbi = [
const States = Object.freeze([ "Open", "Auction", "Owned", "Forbidden", "Reveal", "NotAvailable" ]);
const deedAbi = [
"function owner() view returns (address)"
];
const ethLegacyRegistrarAbi = [
"function entries(bytes32 _hash) view returns (uint8 state, address owner, uint registrationDate, uint value, uint highestBid)",
"function state(bytes32 _hash) public view returns (uint8)",
"function transferRegistrars(bytes32 _hash) @500000",
];
@ -31,13 +34,15 @@ const ethControllerAbi = [
"function rentPrice(string memory name, uint duration) view public returns(uint)",
"function available(string memory label) public view returns(bool)",
"function makeCommitment(string memory name, address owner, bytes32 secret) pure public returns(bytes32)",
"function commit(bytes32 commitment) public",
"function commit(bytes32 commitment) public @500000",
"function register(string calldata name, address owner, uint duration, bytes32 secret) payable @500000",
"function renew(string calldata name, uint duration) payable @500000",
];
const ethRegistrarAbi = [
"function transferFrom(address from, address to, uint256 tokenId)"
"function ownerOf(uint256 tokenId) view returns (address)",
"function reclaim(uint256 id, address owner) @500000",
"function transferFrom(address from, address to, uint256 tokenId) @500000"
];
const resolverAbi = [
@ -50,9 +55,9 @@ const resolverAbi = [
"function setContenthash(bytes32 nodehash, bytes contenthash) @500000",
];
const InterfaceID_ERC721 = "0x6ccb2df4";
const InterfaceID_Controller = "0x018fac06";
const InterfaceID_Legacy = "0x7ba18ba1";
//const InterfaceID_ERC721 = "0x6ccb2df4";
const InterfaceID_Controller = "0x018fac06";
const InterfaceID_Legacy = "0x7ba18ba1";
/*
@ -108,7 +113,8 @@ abstract class EnsPlugin extends Plugin {
}
async getEthRegistrar(): Promise<ethers.Contract> {
let address = await this.getEthInterfaceAddress(InterfaceID_ERC721);
//let address = await this.getEthInterfaceAddress(InterfaceID_ERC721);
let address = await this.getEns().owner(ethers.utils.namehash("eth"));
return new ethers.Contract(address, ethRegistrarAbi, this.accounts[0] || this.provider);
}
}
@ -135,74 +141,90 @@ class LookupPlugin extends EnsPlugin {
let ens = this.getEns();
let controller = await this.getEthController();
let registrar = await this.getEthRegistrar();
let legacyRegistrar = await this.getEthLegacyRegistrar();
for (let i = 0; i < this.names.length; i++) {
let name = this.names[i];
let nodehash = ethers.utils.namehash(name);
let details: any = {
Owner: ens.owner(nodehash),
Resolver: ens.resolver(nodehash)
let details: { [ key: string]: string } = {
Nodehash: nodehash
};
let comps = name.split(".");
if (comps.length === 2 && comps[1] === "eth") {
let labelhash = ethers.utils.id(comps[0].toLowerCase()); // @TODO: nameprep
let available = this.getEthController().then((ethController) => {
return ethController.available(comps[0]);
});
details.Available = available;
let legacyRegistrarPromise = this.getEthLegacyRegistrar();
details._Registrar = Promise.all([
available,
legacyRegistrarPromise.then((legacyRegistrar) => {
return legacyRegistrar.state(labelhash);
})
]).then((results) => {
let available = results[0];
let state = States[results[1]];
if (!available && state === "Owned") {
return legacyRegistrarPromise.then((legacyRegistrar) => {
return legacyRegistrar.entries(labelhash).then((entries: any) => {
return {
Registrar: "Legacy",
"Deed Value": (ethers.utils.formatEther(entries.value) + " ether"),
"Highest Bid": (ethers.utils.formatEther(entries.highestBid) + " ether"),
}
});
});
let owner = await ens.owner(nodehash);
let resolverAddress: string = null;
if (owner === ethers.constants.AddressZero) {
owner = null;
} else {
details.Controller = owner;
details.Resolver = await ens.resolver(nodehash).then((address: string) => {
if (address === ethers.constants.AddressZero) {
return "(not configured)";
}
return { Registrar: "Permanent" };
resolverAddress = address;
return address;
});
}
details = await ethers.utils.resolveProperties(details);
let comps = name.split(".");
if (comps.length === 2 && comps[1] === "eth") {
details.Labelhash = ethers.utils.id(comps[0].toLowerCase()); // @TODO: nameprep
if (details.Resolver !== ethers.constants.AddressZero) {
let resolver = new ethers.Contract(details.Resolver, resolverAbi, this.provider);
details["Address"] = resolver.addr(nodehash);
details["E-mail"] = resolver.text(nodehash, "email").catch((error: any) => (""));
details["Website"] = resolver.text(nodehash, "website").catch((error: any) => (""));
details["Content Hash"] = resolver.contenthash(nodehash).then((hash: string) => {
if (hash === "0x") { return "0x"; }
details.Available = await controller.available(comps[0]);
if (!details.Available) {
try {
let ownerOf = await registrar.ownerOf(details.Labelhash);
if (ownerOf !== ethers.constants.AddressZero) {
details.Registrant = ownerOf;
details.Registrar = "Permanent";
}
} catch (error) {
let entry = await legacyRegistrar.entries(details.Labelhash);
let deed = new ethers.Contract(entry.owner, deedAbi, this.provider);
details.Registrant = await deed.owner();
details.Registrar = "Legacy";
details["Deed Value"] = (ethers.utils.formatEther(entry.value) + " ether");
details["Highest Bid"] = (ethers.utils.formatEther(entry.highestBid) + " ether");
}
}
}
if (resolverAddress) {
let resolver = new ethers.Contract(resolverAddress, resolverAbi, this.provider);
details["Address"] = await resolver.addr(nodehash);
let email = await resolver.text(nodehash, "email").catch((error: any) => (""));
if (email) { details["E-mail"] = email; }
let website = await resolver.text(nodehash, "website").catch((error: any) => (""));
if (website) { details["Website"] = website; }
let content = await resolver.contenthash(nodehash).then((hash: string) => {
if (hash === "0x") { return ""; }
if (hash.substring(0, 10) === "0xe3010170" && ethers.utils.isHexString(hash, 38)) {
return Base58.encode(ethers.utils.hexDataSlice(hash, 4)) + " (IPFS)";
}
return hash + " (unknown format)";
}, (error: any) => (""));
if (content) { details["Content Hash"] = content; }
}
details = await ethers.utils.resolveProperties(details);
for (let key in details._Registrar) {
details[key] = details._Registrar[key];
let ordered: { [ key: string]: string } = { };
"Nodehash,Labelhash,Available,Registrant,Controller,Resolver,Address,Registrar,Deed Value,Highest Bid,E-mail,Website,Content Hash".split(",").forEach((key) => {
if (!details[key]) { return; }
ordered[key] = details[key];
});
for (let key in details) {
if (ordered[key]) { continue; }
ordered[key] = details[key];
}
delete details._Registrar;
this.dump("Name: " + this.names[i], details);
this.dump("Name: " + this.names[i], ordered);
}
}
}
@ -335,7 +357,7 @@ class CommitPlugin extends ControllerPlugin {
static getHelp(): Help {
return {
name: "commit NAME",
help: "Commit to NAME"
help: "Submit a pre-commitment"
}
}
@ -364,8 +386,8 @@ class RevealPlugin extends ControllerPlugin {
static getHelp(): Help {
return {
name: "reveal LABEL",
help: "Reveal a previously committed name"
name: "reveal NAME",
help: "Reveal a previous pre-commitment"
}
}
@ -438,7 +460,7 @@ abstract class AddressAccountPlugin extends AccountPlugin {
return [
{
name: "[ --address ADDRESS ]",
help: "Override the address"
help: "Specify another address"
}
];
}
@ -459,12 +481,12 @@ abstract class AddressAccountPlugin extends AccountPlugin {
}
}
class SetOwnerPlugin extends AddressAccountPlugin {
class SetControllerPlugin extends AddressAccountPlugin {
static getHelp(): Help {
return {
name: "set-owner NAME",
help: "Set the owner of NAME (default: current account)"
name: "set-controller NAME",
help: "Set the controller (default: current account)"
}
}
@ -474,7 +496,7 @@ class SetOwnerPlugin extends AddressAccountPlugin {
this.getEns().setOwner(this.nodehash, this.address);
}
}
cli.addPlugin("set-owner", SetOwnerPlugin);
cli.addPlugin("set-controller", SetControllerPlugin);
class SetSubnodePlugin extends AddressAccountPlugin {
label: string;
@ -483,7 +505,7 @@ class SetSubnodePlugin extends AddressAccountPlugin {
static getHelp(): Help {
return {
name: "set-subnode NAME",
help: "Set the subnode owner"
help: "Set a subnode owner (default: current account)"
}
}
@ -513,7 +535,7 @@ class SetResolverPlugin extends AddressAccountPlugin {
static getHelp(): Help {
return {
name: "set-resolver NAME",
help: "Set the resolver for NAME (default: resolver.eth)"
help: "Set the resolver (default: resolver.eth)"
}
}
@ -586,7 +608,7 @@ class SetTextPlugin extends TextAccountPlugin {
static getHelp(): Help {
return {
name: "set-text NAME KEY VALUE",
help: "Set the KEY text record to VALUE"
help: "Set a text record"
}
}
@ -602,7 +624,7 @@ class SetEmailPlugin extends TextAccountPlugin {
static getHelp(): Help {
return {
name: "set-email NAME EMAIL",
help: "Set the email text record to EMAIL"
help: "Set the email text record"
}
}
@ -618,7 +640,7 @@ class SetWebsitePlugin extends TextAccountPlugin {
static getHelp(): Help {
return {
name: "set-website NAME URL",
help: "Set the website text record to URL"
help: "Set the website text record"
}
}
@ -636,7 +658,7 @@ class SetContentPlugin extends AccountPlugin {
static getHelp(): Help {
return {
name: "set-content NAME HASH",
help: "Set the IPFS HASH for NAME"
help: "Set the IPFS Content Hash"
}
}
@ -669,40 +691,54 @@ cli.addPlugin("set-content", SetContentPlugin);
class MigrateRegistrarPlugin extends AccountPlugin {
readonly label: string;
readonly deedValue: ethers.BigNumber;
readonly highestBid: ethers.BigNumber;
static getHelp(): Help {
return {
name: "migrate-registrar NAME",
help: "Migrates NAME from the Legacy to Permanent Registrar"
help: "Migrate from the Legacy to the Permanent Registrar"
}
}
async prepareArgs(args: Array<string>): Promise<void> {
await super.prepareArgs(args);
// Only Top-Level names can be migrated
let comps = this.name.split(".");
if (comps.length !== 2 || comps[1] !== "eth") {
this.throwError("Not a top-level .eth name");
}
// @TODO: Should probably check that accounts[0].getAddress() matches
// the owner in the legacy registrar
await super._setValue("label", comps[0]);
let ethLegacyRegistrar = await this.getEthLegacyRegistrar();
let state = await ethLegacyRegistrar.state(ethers.utils.id(comps[0]));
let entry: any = await ethLegacyRegistrar.entries(ethers.utils.id(comps[0]));
if (States[state] !== "Owned") {
// Only owned names can be migrated
if (States[entry.state] !== "Owned") {
this.throwError("Name not present in the Legacy registrar");
}
await super._setValue("label", comps[0]);
let deed = new ethers.Contract(entry.owner, deedAbi, this.provider);
let owner = await deed.owner();
let address = await this.accounts[0].getAddress();
// Only the deed owner (registrant) may migrate a name
if (owner !== address) {
this.throwError("Only the registrant can migrate");
}
await super._setValue("deedValue", entry.value);
await super._setValue("highestBid", entry.highestBid);
}
async run(): Promise<void> {
await super.run();
this.dump("Migrate Registrar: " + this.name, {
Nodehash: this.nodehash
"Nodehash": this.nodehash,
"Highest Bid": (ethers.utils.formatEther(this.highestBid) + " ether"),
"Deed Value": (ethers.utils.formatEther(this.deedValue) + " ether"),
});
let legacyRegistrar = await this.getEthLegacyRegistrar();
@ -720,16 +756,16 @@ class TransferPlugin extends AccountPlugin {
static getHelp(): Help {
return {
name: "transfer NAME NEW_OWNER",
help: "Transfers NAME to NEW_OWNER (permanent regstrar only)"
help: "Transfer registrant ownership"
}
}
async _setValue(key: string, value: string): Promise<void> {
if (key === "new_owner") {
let address = await this.getAddress(value);
await this._setValue(key, address);
await super._setValue(key, address);
} else if (key === "name") {
let comps = this.name.split(".");
let comps = value.split(".");
if (comps.length !== 2 || comps[1] !== "eth") {
this.throwError("Not a top-level .eth name");
}
@ -754,12 +790,61 @@ class TransferPlugin extends AccountPlugin {
}
cli.addPlugin("transfer", TransferPlugin);
class ReclaimPlugin extends AddressAccountPlugin {
readonly label: string;
static getHelp(): Help {
return {
name: "reclaim NAME",
help: "Reset the controller by the registrant"
}
}
async _setValue(key: string, value: string): Promise<void> {
if (key === "name") {
let comps = value.split(".");
if (comps.length !== 2 || comps[1] !== "eth") {
this.throwError("Not a top-level .eth name");
}
let account = await this.accounts[0].getAddress();
let registrar = await this.getEthRegistrar();
let ownerOf: string = null;
try {
ownerOf = await registrar.ownerOf(ethers.utils.id(comps[0]));
} catch (error) {
this.throwError("Name not present in Permantent Registrar");
}
if (account !== ownerOf) {
this.throwError("Only the registrant can call reclaim");
}
await super._setValue("label", comps[0]);
}
await super._setValue(key, value);
}
async run(): Promise<void> {
await super.run();
this.dump("Reclaim: " + this.name, {
Nodehash: this.nodehash,
"Address": this.address,
});
let registrar = await this.getEthRegistrar();
await registrar.reclaim(ethers.utils.id(this.label), this.address);
}
}
cli.addPlugin("reclaim", ReclaimPlugin);
/**
* To Do:
* register NAME --registrar
* set-reverse NAME
* renew NAME --duration DAYS
* reclaim NAME --address OWNER
*
* Done:
* migrate-registrar NAME
@ -773,6 +858,7 @@ cli.addPlugin("transfer", TransferPlugin);
* set-webstie NAME WEBSITE
* set-text NAME KEY VALUE
* set-content NAME HASH
* reclaim NAME --address OWNER
*/
cli.run(process.argv.slice(2))