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: hot wallet cli #74

Merged
merged 21 commits into from
Jul 15, 2024
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ jobs:
id: test
- name: Generate coverage report
run: |
forge clean
forge coverage --report summary
id: coverage
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"plugins": ["prettier-plugin-solidity"],
"printWidth": 120,
"endOfLine": "lf",
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false,
"arrowParens": "avoid",
"jsxSingleQuote": true,
"trailingComma": "all",
"overrides": [
{
"files": "*.sol",
Expand Down
2 changes: 1 addition & 1 deletion foundry/test/StakingTestUpgrades.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ contract FoxStakingTestUpgrades is Test {
// confrim still on old version
assertEq(foxStakingV1.version(), expectedCurrentVersion);

// Change the owner
// Change the owner
vm.startPrank(owner);
foxStakingV1.transferOwnership(newOwner);
vm.stopPrank();
Expand Down
66 changes: 66 additions & 0 deletions scripts/hotWalletCli/MultiSig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Prerequisites

- Install golang: https://go.dev/doc/install

## Clone and Build

```bash
git clone https://gitlab.com/thorchain/thornode.git
cd thornode/cmd/thornode
go build --tags cgo,ledger
```

## Create MultiSig

- Add your key:
```bash
./thornode keys add {person1} --ledger
```
- Export pubkey:
```bash
./thornode keys show {person1} --pubkey
```
- Import signer pubkeys:
```bash
./thornode keys add {person2} --pubkey {pubkey}
./thornode keys add {person3} --pubkey {pubkey}
```
- Add multisig key:
```bash
./thornode keys add multisig --multisig {person1},{person2},{person3} --multisig-threshold 2
```
- Validate multisig address:
```bash
./thornode keys show multisig --address
```

## Sign Transaction

- Person 1 signs:
```bash
./thornode tx sign --from {person1} --multisig multisig {unsignedTx_epoch-N.json} --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --from ledger --ledger --sign-mode amino-json > signedTx_{person1}.json
```
- Person 2 signs:
```bash
./thornode tx sign --from {person2} --multisig multisig {unsignedTx_epoch-N.json} --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --from ledger --ledger --sign-mode amino-json > signedTx_{person2}.json
```
- Multisign:
```bash
./thornode tx multisign {unsignedTx_epoch-N.json} multisig signedTx_{person1}.json signedTx_{person2}.json --from multisig --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc > signedTx_multisig.json
```

## Send Transaction

- Simulate transaction:

```bash
./thornode tx broadcast signedTx_multisig.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --gas auto --dry-run > simulatedTx.json
```

- Validate contents of `simulatedTx.json` for accuracy before broadcasting

- Broadcast transaction:
```bash
./thornode tx broadcast signedTx_multisig.json --chain-id thorchain-mainnet-v1 --node https://daemon.thorchain.shapeshift.com:443/rpc --gas auto > tx.json
```
- Copy the `txhash` value from `tx.json` to supply to the cli in order to continue
27 changes: 27 additions & 0 deletions scripts/hotWalletCli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Prerequisites

- NodeJS (v18+): https://nodejs.org/en/download/package-manager
- Yarn: https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable

## Setup

- Install dependencies:

```bash
yarn
```

- Copy sample env file:

```bash
cp sample.env .env
```

- Request environment variables and update `.env` with the appropriate values

## Running

- Run script:
```bash
yarn start
```
4 changes: 4 additions & 0 deletions scripts/hotWalletCli/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os from 'node:os'
import path from 'node:path'

export const RFOX_DIR = path.join(os.homedir(), 'rfox')
kaladinlight marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 39 additions & 0 deletions scripts/hotWalletCli/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from 'node:fs'
import { RFOX_DIR } from './constants'
import { error, info, warn } from './logging'

const deleteIfExists = (file: string) => {
try {
fs.accessSync(file, fs.constants.F_OK)
fs.unlinkSync(file)
} catch {}
}

export const write = (file: string, data: string) => {
try {
deleteIfExists(file)
fs.writeFileSync(file, data, { mode: 0o400, encoding: 'utf8' })
} catch {
error(`Failed to write file ${file}, exiting.`)
warn('Manually save the contents at the specified file location if possible, or a temporary file for recovery!!!')
info(data)
process.exit(1)
}
}

export const read = (file: string): string | undefined => {
try {
return fs.readFileSync(file, 'utf8')
} catch {}
}

export const isEpochDistributionStarted = (epoch: number): boolean => {
const regex = new RegExp(`epoch-${epoch}`)

try {
const files = fs.readdirSync(RFOX_DIR)
return Boolean(files.filter(file => regex.test(file)).length)
} catch {
return false
}
}
120 changes: 120 additions & 0 deletions scripts/hotWalletCli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'dotenv/config'
import * as prompts from '@inquirer/prompts'
import fs from 'node:fs'
import path from 'node:path'
import { Epoch } from '../types'
import { RFOX_DIR } from './constants'
import { isEpochDistributionStarted } from './file'
import { IPFS } from './ipfs'
import { error, info, success, warn } from './logging'
import { create, recoverKeystore } from './mnemonic'
import { Wallet } from './wallet'

const run = async () => {
const ipfs = await IPFS.new()

const epoch = await ipfs.getEpoch()

if (isEpochDistributionStarted(epoch.number)) {
const cont = await prompts.confirm({
message: 'It looks like you have already started a distribution for this epoch. Do you want to continue? ',
})

if (cont) return recover(epoch)

info(`Please move or delete all existing files for epoch-${epoch.number} from ${RFOX_DIR} before re-running.`)
warn('This action should never be taken unless you are absolutely sure you know what you are doing!!!')

process.exit(0)
}

const mnemonic = await create(epoch.number)

const confirmed = await prompts.confirm({
message: 'Have you securely backed up your mnemonic? ',
})

if (!confirmed) {
error('Unable to proceed knowing you have not securely backed up your mnemonic, exiting.')
process.exit(1)
}

const wallet = await Wallet.new(mnemonic)

await processEpoch(epoch, wallet, ipfs)
}

const recover = async (epoch?: Epoch) => {
const ipfs = await IPFS.new()

if (!epoch) epoch = await ipfs.getEpoch()

const keystoreFile = path.join(RFOX_DIR, `keystore_epoch-${epoch.number}.txt`)
const mnemonic = await recoverKeystore(keystoreFile)

const wallet = await Wallet.new(mnemonic)

await processEpoch(epoch, wallet, ipfs)
}

const processEpoch = async (epoch: Epoch, wallet: Wallet, ipfs: IPFS) => {
await wallet.fund(epoch)
const processedEpoch = await wallet.distribute(epoch)

const processedEpochHash = await ipfs.addEpoch(processedEpoch)
await ipfs.updateMetadata({ number: processedEpoch.number, hash: processedEpochHash })

success(`rFOX reward distribution for Epoch #${processedEpoch.number} has been completed!`)

info(
'Please update the rFOX Wiki (https://github.com/shapeshift/rFOX/wiki/rFOX-Metadata) and notify the DAO accordingly. Thanks!',
)
}

const shutdown = () => {
console.log()
warn('Received shutdown signal, exiting.')
process.exit(0)
}

const main = async () => {
try {
fs.mkdirSync(RFOX_DIR)
} catch (err) {
if (err instanceof Error) {
const fsError = err as NodeJS.ErrnoException
if (fsError.code !== 'EEXIST') throw err
}
}

const choice = await prompts.select<'run' | 'recover'>({
message: 'What do you want to do?',
choices: [
{
name: 'Run rFox distribution',
value: 'run',
description: 'Start here to process a new rFox distribution epoch',
},
{
name: 'Recover rFox distribution',
value: 'recover',
description: 'Use this to recover from an error during an rFox distribution epoch',
},
],
})

switch (choice) {
case 'run':
return run()
case 'recover':
return recover()
default:
error(`Invalid choice: ${choice}, exiting.`)
process.exit(1)
}
}

process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)

main()
Loading