Skip to content

Commit

Permalink
Merge pull request #78 from shapeshift/hot-wallet-cli-part-4
Browse files Browse the repository at this point in the history
  • Loading branch information
kaladinlight authored Jul 8, 2024
2 parents 56a9030 + a9f7557 commit 7eb6db3
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 41 deletions.
9 changes: 9 additions & 0 deletions scripts/hotWalletCli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@
## 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:
Expand Down
3 changes: 2 additions & 1 deletion scripts/hotWalletCli/file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs'
import { RFOX_DIR } from './constants'
import { error, info } from './logging'
import { error, info, warn } from './logging'

const deleteIfExists = (file: string) => {
try {
Expand All @@ -15,6 +15,7 @@ export const write = (file: string, data: string) => {
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)
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/hotWalletCli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const run = async () => {

if (cont) return recover(epoch)

warn(`Please move or delete all existing files for epoch-${epoch.number} from ${RFOX_DIR} before re-running.`)
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)
Expand Down
37 changes: 27 additions & 10 deletions scripts/hotWalletCli/ipfs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as prompts from '@inquirer/prompts'
import PinataClient from '@pinata/sdk'
import axios from 'axios'
import BigNumber from 'bignumber.js'
import { Epoch, RFOXMetadata, RewardDistribution } from '../types'
import { error, info } from './logging'

Expand Down Expand Up @@ -76,25 +77,30 @@ export class IPFS {
const client = new PinataClient({ pinataApiKey: PINATA_API_KEY, pinataSecretApiKey: PINATA_SECRET_API_KEY })
await client.testAuthentication()
return new IPFS(client)
} catch (err) {
} catch {
error('Failed to connect to IPFS, exiting.')
process.exit(1)
}
}

async addEpoch(epoch: Epoch): Promise<string> {
const { IpfsHash } = await this.client.pinJSONToIPFS(epoch, {
pinataMetadata: { name: `rFoxEpoch${epoch.number}.json` },
})
try {
const { IpfsHash } = await this.client.pinJSONToIPFS(epoch, {
pinataMetadata: { name: `rFoxEpoch${epoch.number}.json` },
})

info(`rFOX Epoch #${epoch.number} IPFS hash: ${IpfsHash}`)
info(`rFOX Epoch #${epoch.number} IPFS hash: ${IpfsHash}`)

return IpfsHash
return IpfsHash
} catch {
error('Failed to add epoch to IPFS, exiting.')
process.exit(1)
}
}

async getEpoch(): Promise<Epoch> {
const hash = await prompts.input({
message: 'What is the IPFS hash for the rFOX distribution you wish to process? ',
message: 'What is the IPFS hash for the rFOX reward distribution you want to process? ',
})

try {
Expand All @@ -105,7 +111,18 @@ export class IPFS {
})

if (isEpoch(data)) {
info(`Processing rFOX distribution for Epoch #${data.number}.`)
const totalAddresses = Object.keys(data.distributionsByStakingAddress).length
const totalRewards = Object.values(data.distributionsByStakingAddress)
.reduce((prev, distribution) => {
return prev.plus(distribution.amount)
}, BigNumber(0))
.div(100000000)
.toFixed()

info(
`Processing rFOX reward distribution for Epoch #${data.number}:\n - Total Rewards: ${totalRewards} RUNE\n - Total Addresses: ${totalAddresses}`,
)

return data
} else {
error(`The contents of IPFS hash (${hash}) are not valid, exiting.`)
Expand All @@ -127,7 +144,7 @@ export class IPFS {
info(`The metadata already contains an IPFS hash for this epoch: ${hash}`)

const confirmed = await prompts.confirm({
message: `Do you wish to update the metadata with the new IPFS hash: ${epoch.hash}?`,
message: `Do you want to update the metadata with the new IPFS hash: ${epoch.hash}?`,
})

if (!confirmed) return
Expand All @@ -151,7 +168,7 @@ export class IPFS {

async getMetadata(): Promise<RFOXMetadata> {
const hash = await prompts.input({
message: 'What is the IPFS hash for the rFOX metadata you wish to update? ',
message: 'What is the IPFS hash for the rFOX metadata you want to update? ',
})

try {
Expand Down
9 changes: 7 additions & 2 deletions scripts/hotWalletCli/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ const decryptMnemonic = (encryptedMnemonic: string, password: string): string |
}
}

export const create = async (epoch: number): Promise<string> => {
export const create = async (epoch: number, attempt = 0): Promise<string> => {
if (attempt >= 3) {
error('Failed to create hot wallet, exiting.')
process.exit(1)
}

const password = await prompts.password({
message: 'Enter a password for encrypting keystore file: ',
mask: true,
Expand All @@ -59,7 +64,7 @@ export const create = async (epoch: number): Promise<string> => {

if (password !== password2) {
error(`Your passwords don't match.`)
process.exit(1)
return create(epoch, ++attempt)
}

const mnemonic = generateMnemonic()
Expand Down
1 change: 1 addition & 0 deletions scripts/hotWalletCli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@shapeshiftoss/hdwallet-core": "^1.54.0",
"@shapeshiftoss/hdwallet-native": "^1.54.0",
"axios": "^1.7.2",
"bignumber.js": "^9.1.2",
"bip39": "^3.1.0",
"chalk": "^5.3.0",
"dotenv": "^16.4.5",
Expand Down
95 changes: 69 additions & 26 deletions scripts/hotWalletCli/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { bip32ToAddressNList } from '@shapeshiftoss/hdwallet-core'
import { NativeHDWallet } from '@shapeshiftoss/hdwallet-native'
import axios, { isAxiosError } from 'axios'
import chalk from 'chalk'
import symbols from 'log-symbols'
import path from 'node:path'
import ora, { Ora } from 'ora'
import { Epoch } from '../types'
Expand All @@ -16,6 +18,10 @@ const addressNList = bip32ToAddressNList(BIP32_PATH)

type TxsByStakingAddress = Record<string, { signedTx: string; txId: string }>

const suffix = (text: string): string => {
return `\n${symbols.error} ${chalk.bold.red(text)}`
}

export class Wallet {
private hdwallet: NativeHDWallet

Expand All @@ -24,34 +30,44 @@ export class Wallet {
}

static async new(mnemonic: string): Promise<Wallet> {
const wallet = new Wallet(mnemonic)
const initialized = await wallet.initialize()
try {
const wallet = new Wallet(mnemonic)
const initialized = await wallet.initialize()

if (!initialized) {
error('Failed to initialize hot wallet, exiting.')
process.exit(1)
}
if (!initialized) {
error('Failed to initialize hot wallet, exiting.')
process.exit(1)
}

const { address, path } = await wallet.getAddress()
const { address, path } = await wallet.getAddress()

info(`Hot wallet address: ${address} (${path})`)
info(`Hot wallet address: ${address} (${path})`)

return wallet
return wallet
} catch {
error('Failed to create hot wallet, exiting.')
process.exit(1)
}
}

private async initialize(): Promise<boolean | null> {
return this.hdwallet.initialize()
}

private async getAddress() {
const address = await this.hdwallet.thorchainGetAddress({ addressNList })
try {
const address = await this.hdwallet.thorchainGetAddress({ addressNList })

if (!address) {
if (!address) {
error('Failed to get address from hot wallet, exiting.')
process.exit(1)
}

return { address, path: BIP32_PATH }
} catch {
error('Failed to get address from hot wallet, exiting.')
process.exit(1)
}

return { address, path: BIP32_PATH }
}

private async buildFundingTransaction(amount: string, epoch: number) {
Expand Down Expand Up @@ -119,11 +135,13 @@ export class Wallet {
resolve && resolve()

return true
} catch (err) {
} catch (err: any) {
spinner?.fail()

if (isAxiosError(err)) {
error(err.request?.data?.message || err.response?.data?.message || err.message)
error(
`Failed to verify if hot wallet is funded: ${err.request?.data?.message || err.response?.data?.message || err.message}, exiting.`,
)
} else {
error('Failed to verify if hot wallet is funded, exiting.')
}
Expand Down Expand Up @@ -177,7 +195,9 @@ export class Wallet {
spinner.fail()

if (isAxiosError(err)) {
error(err.request?.data?.message || err.response?.data?.message || err.message)
error(
`Failed to get account details: ${err.request?.data?.message || err.response?.data?.message || err.message}, exiting.`,
)
} else {
error('Failed to get account details, exiting.')
}
Expand Down Expand Up @@ -217,7 +237,10 @@ export class Wallet {

const signedTx = await this.hdwallet.thorchainSignTx(unsignedTx)

if (!signedTx?.serialized) break
if (!signedTx?.serialized) {
spinner.suffixText = suffix('Failed to sign transaction.')
break
}

txsByStakingAddress[stakingAddress] = {
signedTx: signedTx.serialized,
Expand All @@ -226,7 +249,13 @@ export class Wallet {

i++
}
} catch {}
} catch (err) {
if (err instanceof Error) {
spinner.suffixText = suffix(`Failed to sign transaction: ${err.message}.`)
} else {
spinner.suffixText = suffix('Failed to sign transaction.')
}
}

return txsByStakingAddress
})()
Expand Down Expand Up @@ -255,21 +284,35 @@ export class Wallet {
continue
}

const { data } = await axios.post<{ result: { code: number; hash: string } }>(`${THORNODE_URL}/rpc`, {
jsonrpc: '2.0',
id: stakingAddress,
method: 'broadcast_tx_sync',
params: { tx: signedTx },
})
const { data } = await axios.post<{ result: { code: number; data: string; log: string; hash: string } }>(
`${THORNODE_URL}/rpc`,
{
jsonrpc: '2.0',
id: stakingAddress,
method: 'broadcast_tx_sync',
params: { tx: signedTx },
},
)

if (!data.result.hash || data.result.code !== 0) continue
if (!data.result.hash || data.result.code !== 0) {
spinner.suffixText = suffix(`Failed to broadcast transaction: ${data.result.data || data.result.log}.`)
break
}

txsByStakingAddress[stakingAddress].txId = data.result.hash
epoch.distributionsByStakingAddress[stakingAddress].txId = data.result.hash

await new Promise(resolve => setTimeout(resolve, 1_000))
}
} catch {}
} catch (err) {
if (isAxiosError(err)) {
spinner.suffixText = suffix(
`Failed to broadcast transaction: ${err.request?.data?.message || err.response?.data?.message || err.message}.`,
)
} else {
spinner.suffixText = suffix('Failed to broadcast transaction.')
}
}

const txsFile = path.join(RFOX_DIR, `txs_epoch-${epoch.number}.json`)
write(txsFile, JSON.stringify(txsByStakingAddress, null, 2))
Expand Down
2 changes: 1 addition & 1 deletion scripts/hotWalletCli/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ bigi@^1.1.0, bigi@^1.4.2:
resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825"
integrity sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw==

bignumber.js@^9.0.1:
bignumber.js@^9.0.1, bignumber.js@^9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c"
integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==
Expand Down

0 comments on commit 7eb6db3

Please sign in to comment.