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 part 4 #78

Merged
merged 1 commit into from
Jul 8, 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
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