Skip to content

Commit

Permalink
Adding more support for offline signing in the CLI.
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Jul 2, 2019
1 parent 6484908 commit 9cc269c
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 31 deletions.
7 changes: 7 additions & 0 deletions packages/cli/src.ts/bin/ethers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ class SendPlugin extends Plugin {
toAddress: string;
value: ethers.BigNumber;
allowZero: boolean;
data: string;

static getHelp(): Help {
return {
Expand All @@ -327,6 +328,10 @@ class SendPlugin extends Plugin {
{
name: "[ --allow-zero ]",
help: "Allow sending to the address zero"
},
{
name: "[ --data DATA ]",
help: "Include data in the transaction"
}
];
}
Expand All @@ -338,6 +343,7 @@ class SendPlugin extends Plugin {
this.throwUsageError("send requires exacly one account");
}

this.data = ethers.utils.hexlify(argParser.consumeOption("data") || "0x");
this.allowZero = argParser.consumeFlag("allow-zero");
}

Expand All @@ -355,6 +361,7 @@ class SendPlugin extends Plugin {
async run(): Promise<void> {
await this.accounts[0].sendTransaction({
to: this.toAddress,
data: this.data,
value: this.value
});;
}
Expand Down
120 changes: 89 additions & 31 deletions packages/cli/src.ts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ class UsageError extends Error { }
/////////////////////////////
// Signer

/*
const signerStates = new WeakMap();
class SignerState {
signerFunc: () => Promise<ethers.Signer>;
signer: ethers.Signer;
alwaysAllow: boolean;
static get(wrapper: WrappedSigner): SignerState {
return signerStates.get(wrapper);
}
}
*/
const signerFuncs = new WeakMap();
const signers = new WeakMap();
const alwaysAllow = new WeakMap();
Expand Down Expand Up @@ -147,6 +160,25 @@ class WrappedSigner extends ethers.Signer {
return result;
}

async populateTransaction(transactionRequest: ethers.providers.TransactionRequest): Promise<ethers.providers.TransactionRequest> {
transactionRequest = ethers.utils.shallowCopy(transactionRequest);

if (this.plugin.gasPrice != null) {
transactionRequest.gasPrice = this.plugin.gasPrice;
}

if (this.plugin.gasLimit != null) {
transactionRequest.gasLimit = this.plugin.gasLimit;
}

if (this.plugin.nonce != null) {
transactionRequest.nonce = this.plugin.nonce;
}

let signer = await getSigner(this);
return signer.populateTransaction(transactionRequest);
}

async signTransaction(transactionRequest: ethers.providers.TransactionRequest): Promise<string> {
let signer = await getSigner(this);

Expand All @@ -162,7 +194,6 @@ class WrappedSigner extends ethers.Signer {
info["Gas Limit"] = ethers.BigNumber.from(tx.gasLimit || 0).toString();
info["Gas Price"] = (ethers.utils.formatUnits(tx.gasPrice || 0, "gwei") + " gwei"),
info["Chain ID"] = (tx.chainId || 0);
info["Data"] = ethers.utils.hexlify(tx.data || "0x");
info["Network"] = network.name;

dump("Transaction:", info);
Expand All @@ -189,7 +220,7 @@ class WrappedSigner extends ethers.Signer {

let network = await this.provider.getNetwork();

let tx: any = await signer.populateTransaction(transactionRequest);
let tx: any = await this.populateTransaction(transactionRequest);
tx = await ethers.utils.resolveProperties(tx);

let info: any = { };
Expand All @@ -200,7 +231,6 @@ class WrappedSigner extends ethers.Signer {
info["Gas Limit"] = ethers.BigNumber.from(tx.gasLimit || 0).toString();
info["Gas Price"] = (ethers.utils.formatUnits(tx.gasPrice || 0, "gwei") + " gwei"),
info["Chain ID"] = (tx.chainId || 0);
info["Data"] = ethers.utils.hexlify(tx.data || "0x");
info["Network"] = network.name;

dump("Transaction:", info);
Expand All @@ -221,6 +251,16 @@ class WrappedSigner extends ethers.Signer {
}
}

class OfflineProvider extends ethers.providers.BaseProvider {
perform(method: string, params: any): Promise<any> {
if (method === "sendTransaction") {
console.log("Signed Transaction:");
console.log(params.signedTransaction);
return Promise.resolve(ethers.utils.keccak256(params.signedTransaction));
}
return super.perform(method, params);
}
}

/////////////////////////////
// Argument Parser
Expand Down Expand Up @@ -327,12 +367,12 @@ export class ArgParser {
// - JSON Wallet filename (which will require a password to unlock)
// - raw private key
// - mnemonic
async function loadAccount(arg: string, plugin: Plugin): Promise<WrappedSigner> {
async function loadAccount(arg: string, plugin: Plugin, preventFile?: boolean): Promise<WrappedSigner> {

// Secure entry; use prompt with mask
if (arg === "-") {
let content = await getPassword("Private Key / Mnemonic:");
return loadAccount(content, plugin);
return loadAccount(content, plugin, true);
}

// Raw private key
Expand All @@ -343,13 +383,27 @@ async function loadAccount(arg: string, plugin: Plugin): Promise<WrappedSigner>

// Mnemonic
if (ethers.utils.isValidMnemonic(arg)) {
let signer = ethers.Wallet.fromMnemonic(arg).connect(plugin.provider);
return Promise.resolve(new WrappedSigner(signer.getAddress(), () => Promise.resolve(signer), plugin));
let signerPromise: Promise<ethers.Wallet> = null;
if (plugin.mnemonicPassword) {
signerPromise = getPassword("Password (mnemonic): ").then((password) => {
let node = ethers.utils.HDNode.fromMnemonic(arg, password).derivePath(ethers.utils.defaultPath);
return new ethers.Wallet(node.privateKey, plugin.provider);
});
} else {
signerPromise = Promise.resolve(ethers.Wallet.fromMnemonic(arg).connect(plugin.provider));
}

return Promise.resolve(new WrappedSigner(
signerPromise.then((wallet) => wallet.getAddress()),
() => signerPromise,
plugin
));
}

// Check for a JSON wallet
try {
let content = fs.readFileSync(arg).toString();

let address = ethers.utils.getJsonWalletAddress(content);
if (address) {
return Promise.resolve(new WrappedSigner(
Expand All @@ -363,7 +417,10 @@ async function loadAccount(arg: string, plugin: Plugin): Promise<WrappedSigner>
});
},
plugin));
} else {
return loadAccount(content.trim(), plugin, true);
}

} catch (error) {
if (error.message === "cancelled") {
throw new Error("Cancelled.");
Expand Down Expand Up @@ -396,12 +453,12 @@ export class Plugin {
provider: ethers.providers.Provider;

accounts: Array<WrappedSigner>;
mnemonicPassword: boolean;

gasLimit: ethers.BigNumber;
gasPrice: ethers.BigNumber;
nonce: number;
data: string;
value: ethers.BigNumber;
yes: boolean;

constructor() {
Expand Down Expand Up @@ -449,18 +506,24 @@ export class Plugin {
providers.push(new ethers.providers.NodesmithProvider(network));
}

if (argParser.consumeFlag("offline")) {
providers.push(new OfflineProvider(network));
}

if (providers.length === 1) {
this.provider = providers[0];
ethers.utils.defineReadOnly(this, "provider", providers[0]);
} else if (providers.length) {
this.provider = new ethers.providers.FallbackProvider(providers);
ethers.utils.defineReadOnly(this, "provider", new ethers.providers.FallbackProvider(providers));
} else {
this.provider = ethers.getDefaultProvider(network);
ethers.utils.defineReadOnly(this, "provider", ethers.getDefaultProvider(network));
}


/////////////////////
// Accounts

ethers.utils.defineReadOnly(this, "mnemonicPassword", argParser.consumeFlag("mnemonic-password"));

let accounts: Array<WrappedSigner> = [ ];

let accountOptions = argParser.consumeMultiOptions([ "account", "account-rpc", "account-void" ]);
Expand Down Expand Up @@ -501,47 +564,41 @@ export class Plugin {
}
}

this.accounts = accounts;
ethers.utils.defineReadOnly(this, "accounts", Object.freeze(accounts));


/////////////////////
// Transaction Options

let gasPrice = argParser.consumeOption("gas-price");
if (gasPrice) {
this.gasPrice = ethers.utils.parseUnits(gasPrice, "gwei");
ethers.utils.defineReadOnly(this, "gasPrice", ethers.utils.parseUnits(gasPrice, "gwei"));
} else {
ethers.utils.defineReadOnly(this, "gasPrice", null);
}

let gasLimit = argParser.consumeOption("gas-limit");
if (gasLimit) {
this.gasLimit = ethers.BigNumber.from(gasLimit);
ethers.utils.defineReadOnly(this, "gasLimit", ethers.BigNumber.from(gasLimit));
} else {
ethers.utils.defineReadOnly(this, "gasLimit", null);
}

let nonce = argParser.consumeOption("nonce");
if (nonce) {
this.nonce = ethers.BigNumber.from(nonce).toNumber();
}

let value = argParser.consumeOption("value");
if (value) {
this.value = ethers.utils.parseEther(value);
}

let data = argParser.consumeOption("data");
if (data) {
this.data = ethers.utils.hexlify(data);
}


// Now wait for all asynchronous options to load

runners.push(this.provider.getNetwork().then((network) => {
this.network = network;
ethers.utils.defineReadOnly(this, "network", Object.freeze(network));
}, (error) => {
this.network = {
ethers.utils.defineReadOnly(this, "network", Object.freeze({
chainId: 0,
name: "no-network"
}
}));
}));

try {
Expand Down Expand Up @@ -592,7 +649,6 @@ export class Plugin {

export class CLI {
readonly defaultCommand: string;
//readonly plugins: { [ command: string ]: { new(...args: any[]): Plugin; getHelp(): Help; } };
readonly plugins: { [ command: string ]: PluginType };

constructor(defaultCommand: string) {
Expand Down Expand Up @@ -646,21 +702,23 @@ export class CLI {
}

console.log("ACCOUNT OPTIONS");
console.log(" --account FILENAME Load a JSON Wallet (crowdsale or keystore)");
console.log(" --account FILENAME Load from a file (JSON, RAW or mnemonic)");
console.log(" --account RAW_KEY Use a private key (insecure *)");
console.log(" --account 'MNEMONIC' Use a mnemonic (insecure *)");
console.log(" --account - Use secure entry for a raw key or mnemonic");
console.log(" --account-void ADDRESS Udd an address as a void signer");
console.log(" --account-void ENS_NAME Add the resolved address as a void signer");
console.log(" --account-rpc ADDRESS Add the address from a JSON-RPC provider");
console.log(" --account-rpc INDEX Add the index from a JSON-RPC provider");
console.log(" --mnemonic-password Prompt for a password for mnemonics");
console.log("");
console.log("PROVIDER OPTIONS (default: getDefaultProvider)");
console.log(" --alchemy Include Alchemy");
console.log(" --etherscan Include Etherscan");
console.log(" --infura Include INFURA");
console.log(" --nodesmith Include nodesmith");
console.log(" --rpc URL Include a custom JSON-RPC");
console.log(" --offline Dump signed transactions (no send)");
console.log(" --network NETWORK Network to connect to (default: homestead)");
console.log("");
console.log("TRANSACTION OPTIONS (default: query the network)");
Expand Down Expand Up @@ -696,14 +754,14 @@ export class CLI {
{
let argParser = new ArgParser(args);

[ "debug", "help", "yes"].forEach((key) => {
[ "debug", "help", "mnemonic-password", "offline", "yes"].forEach((key) => {
argParser.consumeFlag(key);
});

[ "alchemy", "etherscan", "infura", "nodesmith" ].forEach((flag) => {
argParser.consumeFlag(flag);
});
[ "network", "rpc", "account", "account-rpc", "account-void", "gas-price", "gas-limit", "nonce", "data" ].forEach((option) => {
[ "network", "rpc", "account", "account-rpc", "account-void", "gas-price", "gas-limit", "nonce" ].forEach((option) => {
argParser.consumeOption(option);
});

Expand Down

0 comments on commit 9cc269c

Please sign in to comment.