Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor for ethers v6 #9

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 90 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🔐 Hardhat Secure Accounts

This plugin provides a secure way of storing private keys to use with [Hardhat](https://hardhat.org/). The keys are encrypted using a user-provided password and stored using [keystore](https://julien-maffre.medium.com/what-is-an-ethereum-keystore-file-86c8c5917b97). The plugin also provides several ways of unlocking and using the accounts to sign transactions and messages.
This plugin provides a secure way of storing Ethereum account private keys and mnemonics to use with [Hardhat](https://hardhat.org/). Keys are encrypted using a password and stored with [keystore](https://julien-maffre.medium.com/what-is-an-ethereum-keystore-file-86c8c5917b97). The plugin also provides several ways of unlocking and using the accounts to sign transactions and messages.

**Why**

Expand All @@ -13,12 +13,18 @@ A few reasons why you might want to use this plugin:

What this plugin can do for you:
- Manage multiple accounts on your hardhat project using mnemonic phrases (TODO: support private keys!)
- Create and access secure keystore files using [ethers](https://docs.ethers.io/v5/) to [encrypt](https://docs.ethers.io/v5/api/signer/#Wallet-encrypt) and [decrypt](https://docs.ethers.io/v5/api/signer/#Wallet-fromEncryptedJsonSync) the private keys. By default keystore files are stored at the root of your project in a `.keystore` folder, you should gitignore this folder as an extra security measure.
- Unlock your accounts and get a wallet, signer or provider to use with your hardhat tasks/scripts/console.
- Unlock your accounts and get a signer or provider to use with your hardhat tasks/scripts/console.

**How**

The plugin works as follow:
- Extends the Hardhat provider (`hre.network.provider`) using [extendProvider()](https://hardhat.org/hardhat-runner/docs/advanced/building-plugins#extending-the-hardhat-provider) function
- Create and access secure keystore files using [ethers](https://docs.ethers.io/v6/) to [encrypt](https://docs.ethers.org/v6/api/wallet/#Wallet-encrypt) and [decrypt](https://docs.ethers.org/v6/api/wallet/#Wallet_fromEncryptedJson) the private keys. By default keystore files are stored at the root of your project in a `.keystore` folder, you should gitignore this folder as an extra security measure.
- Extend Hardhat Runtime Environment with several methods to unlock accounts and get signer, wallet and provider instances.

## ⚠️ Disclaimers ⚠️

- Exercise caution when using this plugin! For any serious production work you should use more reliable and safe ways of securing your keys/contracts such as hardware wallets, multisigs, ownable contracts, etc.
- Exercise caution when using this plugin! This plugin is mostly meant to simplify account key management for non production accounts. For any serious production work you should use more reliable and safe ways of securing your keys/contracts such as hardware wallets, multisigs, ownable contracts, etc.

- Because of how [repl](https://github.com/nodejs/repl) works it's not possible to use most of the popular prompt libraries while working on repl environments such as `npx hardhat console`. The plugin supports these environments via usage of [prompt-sync](https://www.npmjs.com/package/prompt-sync) which is a project that's not actively maintained (and it doesn't look as nice!). Please use with caution.

Expand All @@ -42,9 +48,14 @@ Or, if you are using TypeScript, add this to your hardhat.config.ts:
import "hardhat-secure-accounts";
```

## Configuration




## Usage

#### Adding an account
### Adding an account

To add an account to your project, run the following command:

Expand All @@ -54,84 +65,75 @@ npx hardhat accounts add

You'll be prompted for an account name, mnemonic and password and the account will be stored under the `.keystore` folder (unless you specify a different path via plugin configuration).

#### Removing an account
### Removing an account

To remove an account from your project, run the following command:

```bash
npx hardhat accounts remove
```

You'll be prompted for an account nameand the account will be deleted.
You'll be prompted for an account name and the account will be deleted.

#### Listing managed accounts
### Listing managed accounts

You can list the accounts you have added to your project by running:

```bash
npx hardhat accounts list
```

#### Unlocking an account
### Using the accounts

This plugin offers a few methods for using the accounts in a Hardhat project. Depending on your workflow you might want to choose one over the other.

**Hardhat provider extension (recommended)**

This plugin offers two methods for unlocking accounts and several ways of using the unlocked account. Depending on your workflow you might want to choose one over the other.
The plugin extends Hardhat's default provider (`hre.network.provider`), decorating it to be a `SecureAccountsProvider` which will know how to sign transactions using the accounts you have added to your project.

Accounts can be unlocked using:
- Hardhat tasks
- Hardhat environment extension
Note that the provider is not extended by default. See [Configuration](#configuration) for instructions on how to enable it. Additionally, the provider will only be extended if there are accounts available in the project (which need to be created via the add command: `npx hardhat accounts add`).

With the unlocked account you can:
- Get one or multiple wallet instances (`ethers' Wallet`)
- Get a provider instance (`hardhat's EthersProviderWrapper`)
- Get one or multiple signer instances (`hardhat's SignerWithAddress`)
Any accounts defined using Hardhat's `accounts` field in the configuration file will be ignored by the plugin:

```ts
...
hardhat: {
secureAccounts: {
enabled: true,
},
accounts: {
mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', // ignored
},
},
...
```

**Hardhat environment extension (recommended)**

The plugin extends hardhat's environment with several convenience methods to unlock accounts. You can use these methods to get a signer, wallet or provider instance to use with your scripts and tasks:
The plugin extends hardhat's environment with several convenience methods to unlock accounts. You can use these methods to get a signer or a provider instance to use with your scripts and tasks:

```typescript
import hre from 'hardhat'

const signer = await hre.accounts.getSigner()
console.log(`Account ${signer.address} unlocked!`)
```

See [API reference](#api-reference) for a complete list of the available methods.

**Hardhat tasks**

For a quick account unlock using the CLI:

```bash
npx hardhat accounts unlock
await hre.accounts.provider.send('eth_sendTransaction', [{ from: signer.address, to: '0x1234', value: '0x5678' }])
```

This can be useful to validate an account password but not much else since the account only remains unlocked until the task ends its execution. If you want to use it in the context of a script/task you can run the task programmatically:

```typescript
import hre from 'hre'
import { TASK_ACCOUNTS_UNLOCK_SIGNER } from 'hardhat-secure-accounts'
The complete interface for the runtime environment extension is as follows:

const signer = await hre.run(TASK_ACCOUNTS_UNLOCK_SIGNER)
console.log(`Account ${signer.address} unlocked!`)
```ts
interface SecureAccountsRuntimeEnvironment {
provider: HardhatEthersProvider
getSigner(name?: string, password?: string): Promise<HDNodeWallet>
getSigners(name?: string, password?: string): Promise<HDNodeWallet[]>
}
```

See [API reference](#api-reference) for a complete list of the available tasks.

## API reference

| Task | CLI | Hardhat task | Hardhat environment extension | Description |
| --- | --- | --- | --- | --- |
| Unlock wallet | `npx hardhat accounts unlock:wallet` | `TASK_ACCOUNTS_UNLOCK_WALLET` | `await hre.accounts.getWallet()` | Returns the main wallet from the mnemonic derivation path. Return type: `Wallet` |
| Unlock wallets | `npx hardhat accounts unlock:wallets` | `TASK_ACCOUNTS_UNLOCK_WALLETS` | `await hre.accounts.getWallets()` | Returns multiple wallets (20) derived from the mnemonic. Return type: `Wallet[]` |
| Unlock signer | `npx hardhat accounts unlock` | `TASK_ACCOUNTS_UNLOCK_SIGNER` | `await hre.accounts.getSigner()` | Returns the main signer from the mnemonic derivation path. Return type: `SignerWithAddress` |
| Unlock signers | `npx hardhat accounts unlock:signers` | `TASK_ACCOUNTS_UNLOCK_SIGNERS` | `await hre.accounts.getSigners()` | Returns multiple signers (20) derived from the mnemonic. Return type: `SignerWithAddress[]` |
| Unlock provider | `npx hardhat accounts unlock:provider` | `TASK_ACCOUNTS_UNLOCK_PROVIDER` | `await hre.accounts.getProvider()` | Returns a provider with pre-configured local accounts based on the mnemonic. Return type: `EthersProviderWrapper` |
Note that the provider at `hre.accounts.provider` is a `HardhatEthersProvider` created using the extended `SecureAccountsProvider` provider.


**Optional parameters**

For convenience, all of the tasks and methods listed above have optional parameters that allow passing the `name` and `password` of the account to unlock. If any of the optional parameters are provided the plugin will not prompt the user for that input. This might be useful for scripting or testing purposes.
For convenience, `getSigner()` and `getSigners()` have optional parameters that allow passing the `name` and `password` of the account to unlock. If any of the optional parameters are provided the plugin will not prompt the user for that input. This might be useful for scripting or testing purposes.

Example using the different API's:

Expand All @@ -143,49 +145,68 @@ const signer = await hre.accounts.getSigner('goerli-deployer')
console.log(`Account ${signer.address} unlocked!`)

// This will not prompt the user for any input
const signer2 = await hre.run(TASK_ACCOUNTS_UNLOCK_SIGNER, { name: 'goerli-deployer', password: 'batman-with-cheese' })
const signer2 = await hre.accounts.getSigner('goerli-deployer', 'batman-with-cheese' )
console.log(`Account ${signer2.address} unlocked!`)
```

Or using the CLI:

```bash
# This will prompt the user only for the account password
npx hardhat accounts unlock --name goerli-deployer
```

## Configuration

By default accounts are stored in the root of your hardhat project in a `.keystore` folder. You can change this by adding the following to your hardhat.config.js:
Plugin behavior can be modified via the Hardhat configuration file. The following options are available:

```js
require('hardhat-secure-accounts')
**Accounts directory**
By default accounts are stored in the root of your hardhat project in a `.keystore` folder. You can change this by adding the following to your Hardhat configuration file:

module.exports = {
```ts
const config: HardhatUserConfig = {
solidity: '0.7.3',
defaultNetwork: 'hardhat',
paths: {
accounts: '.accounts',
secureAccounts: '.accounts', // This will store accounts in ./project-root/.accounts folder
},
};
...
}
export default config
```

Or if you are using TypeScript, modify your `hardhat.config.ts`:
**Global settings**
The following optional global settings can be configured:

```ts
import { HardhatUserConfig } from 'hardhat/types'
const config: HardhatUserConfig = {
solidity: '0.7.3',
defaultNetwork: 'hardhat',
...
secureAccounts: {
enabled: true, // Enable or disable the provider extension
defaultAccount: 'testnet-account-1', // Default account to use when unlocking accounts, setting it will skip the prompt for which account to unlock
defaultAccountPassword: 'secret' // Default password to use when unlocking accounts, setting it will skip the prompt for a password when unlocking -- not recommended!
},
...
}
export default config
```

import 'hardhat-secure-accounts'
**Network settings**
Global settings can also be applied at the network level overriding the global settings. This is useful when you want to use different accounts for different networks:

```ts
const config: HardhatUserConfig = {
solidity: '0.7.3',
defaultNetwork: 'hardhat',
paths: {
accounts: '.accounts',
...
networks: {
hardhat: {
secureAccounts: {
enabled: false
}
}
...
},
...
secureAccounts: {
enabled: true
}
}

export default config
```

Expand Down
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hardhat-secure-accounts",
"version": "0.0.6",
"version": "1.0.3",
"description": "Account management plugin for Hardhat",
"repository": "github:edgeandnode/hardhat-secure-accounts",
"author": "Edge & Node",
Expand Down Expand Up @@ -31,7 +31,7 @@
"README.md"
],
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.1.1",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.5",
"@types/debug": "^4.1.7",
Expand All @@ -42,20 +42,20 @@
"@types/prompt-sync": "^4.1.1",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"ethers": "^5.0.0",
"hardhat": "^2.0.0",
"ethers": "^6.13.0",
"hardhat": "^2.22.0",
"mocha": "^7.1.2",
"prettier": "2.0.5",
"ts-node": "^8.1.0",
"tslint": "^5.16.0",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^4.0.3"
"typescript": "^5.6.2"
},
"peerDependencies": {
"@nomiclabs/hardhat-ethers": "^2.1.1",
"ethers": "^5.0.0",
"hardhat": "^2.0.0"
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"ethers": "^6.13.0",
"hardhat": "^2.22.0"
},
"dependencies": {
"debug": "^4.3.4",
Expand Down
14 changes: 9 additions & 5 deletions src/lib/ask.ts → src/helpers/ask.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Enquirer from 'enquirer'
import Prompt from 'prompt-sync'

import { getSecureAccount, getSecureAccounts, SecureAccount } from './account'
import { logDebug, logWarn } from '../helpers/logger'
import { SecureAccountPluginError } from '../helpers/error'
import { getSecureAccount, getSecureAccounts, SecureAccount } from '../lib/account'
import { logDebug, logWarn } from './logger'
import { SecureAccountsPluginError } from './error'

export const isRepl = () => !!require('repl').repl

Expand All @@ -22,7 +22,7 @@ export async function getAccountOrAsk(

const account = getSecureAccount(accountsDir, name)
if (account === undefined) {
throw new SecureAccountPluginError('Account not found!')
throw new SecureAccountsPluginError('Account not found!')
}
logDebug(`Found account ${account.name} at ${account.filename}`)

Expand All @@ -39,7 +39,7 @@ export async function getPasswordOrAsk(_password: string | undefined): Promise<s
const password = _password ?? (await askForPassword())

if (password === undefined) {
throw new SecureAccountPluginError('Password not provided!')
throw new SecureAccountsPluginError('Password not provided!')
}

return password
Expand All @@ -60,6 +60,10 @@ export async function getStringOrAsk(
}

async function askForAccount(accounts: SecureAccount[]): Promise<string> {
if (accounts.length === 0) {
throw new SecureAccountsPluginError('No accounts found!')
}

const question = 'Choose an account to unlock'
let answer: string = ''
const options = accounts.map((a) => a.name)
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HardhatPluginError } from 'hardhat/plugins'
import { logError } from './logger'

export class SecureAccountPluginError extends HardhatPluginError {
export class SecureAccountsPluginError extends HardhatPluginError {
constructor(_error: string | Error) {
const error = (_error instanceof Error) ? _error : undefined
const message = (_error instanceof Error) ? _error.message : _error
const error = _error instanceof Error ? _error : undefined
const message = _error instanceof Error ? _error.message : _error

super('SecureAccount', message, error)
logError(message)
Expand Down
Loading