Skip to content

Commit f387285

Browse files
authored
feat(wallet-cli): Catch up standalone wallet (#67)
* feat(wallet-cli): Catch up standalone wallet * Properly close clients * fix faucet
1 parent d9836a9 commit f387285

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+8747
-5749
lines changed

package.json

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@
2222
"node": ">=18"
2323
},
2424
"devDependencies": {
25+
"@oclif/test": "4.0.4",
2526
"@types/blessed": "0.1.22",
27+
"@types/cli-progress": "3.11.6",
28+
"@types/inquirer": "9.0.3",
29+
"@types/keccak": "3.0.4",
2630
"@types/node": "18.11.16",
31+
"@types/tar": "6.1.13",
32+
"@types/uuid": "10.0.0",
2733
"@types/yup": "0.29.10",
2834
"@typescript-eslint/eslint-plugin": "5.62.0",
2935
"@typescript-eslint/parser": "5.62.0",
@@ -36,7 +42,7 @@
3642
"eslint-plugin-simple-import-sort": "10.0.0",
3743
"jest": "29.3.1",
3844
"jest-jasmine2": "29.3.1",
39-
"oclif": "2.6.0",
45+
"oclif": "4.14.0",
4046
"prettier": "2.8.8",
4147
"typescript": "5.0.4",
4248
"yarn": "1.22.10"
@@ -55,18 +61,24 @@
5561
"oclif:version": "oclif readme && git add README.md"
5662
},
5763
"dependencies": {
58-
"@ironfish/rust-nodejs": "2.3.0",
59-
"@ironfish/sdk": "2.3.0",
60-
"@oclif/core": "1.23.1",
61-
"@oclif/plugin-help": "5.1.12",
62-
"@oclif/plugin-not-found": "2.3.1",
63-
"@oclif/plugin-warn-if-update-available": "2.0.40",
64-
"@types/inquirer": "9.0.3",
64+
"@ironfish/rust-nodejs": "2.6.0",
65+
"@ironfish/sdk": "2.6.0",
66+
"@ledgerhq/hw-transport-node-hid": "6.29.4",
67+
"@oclif/core": "4.0.11",
68+
"@oclif/plugin-help": "6.2.5",
69+
"@oclif/plugin-not-found": "3.2.10",
70+
"@oclif/plugin-warn-if-update-available": "3.1.8",
71+
"@zondax/ledger-ironfish": "0.1.2",
6572
"blessed": "0.1.81",
73+
"cli-progress": "3.12.0",
6674
"cross-env": "7.0.3",
6775
"inquirer": "8.2.5",
6876
"json-colorizer": "2.2.2",
77+
"keccak": "3.0.4",
78+
"natural-orderby": "4.0.0",
6979
"supports-hyperlinks": "2.2.0",
80+
"tar": "7.4.3",
81+
"uuid": "10.0.0",
7082
"yup": "0.29.3"
7183
},
7284
"oclif": {
@@ -83,7 +95,11 @@
8395
"@oclif/plugin-not-found",
8496
"@oclif/plugin-warn-if-update-available"
8597
],
86-
"topics": {}
98+
"topics": {
99+
"chainport": {
100+
"description": "Commands for chainport"
101+
}
102+
}
87103
},
88104
"bin": {
89105
"ironfishw": "./bin/run"

src/args.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,24 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44

5-
export function parseNumber(input: string): number | null {
6-
const parsed = Number(input)
7-
return isNaN(parsed) ? null : parsed
5+
import { parseUrl as parseUrlSdk } from '@ironfish/sdk'
6+
import { Args } from '@oclif/core'
7+
8+
type Url = {
9+
protocol: string | null
10+
hostname: string
11+
port: number | null
12+
}
13+
14+
export function parseUrl(input: string): Promise<Url> {
15+
const parsed = parseUrlSdk(input)
16+
if (parsed.hostname != null) {
17+
return Promise.resolve(parsed as Url)
18+
} else {
19+
return Promise.reject(new Error(`Invalid URL: ${input}`))
20+
}
821
}
22+
23+
export const UrlArg = Args.custom<Url>({
24+
parse: async (input: string) => parseUrl(input),
25+
})

src/command.ts

Lines changed: 133 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
44
import {
5+
ApiNamespace,
56
Config as IronfishConfig,
67
ConfigOptions,
78
createRootLogger,
@@ -10,14 +11,16 @@ import {
1011
InternalOptions,
1112
IronfishSdk,
1213
Logger,
14+
RpcClient,
1315
RpcConnectionError,
16+
RpcMemoryClient,
1417
} from '@ironfish/sdk'
15-
import { Command, Config } from '@oclif/core'
16-
import { CLIError, ExitError } from '@oclif/core/lib/errors'
18+
import { Command, Config, Errors, ux } from '@oclif/core'
1719
import {
20+
ConfigFlag,
1821
ConfigFlagKey,
22+
DataDirFlag,
1923
DataDirFlagKey,
20-
NetworkIdFlagKey,
2124
RpcAuthFlagKey,
2225
RpcHttpHostFlagKey,
2326
RpcHttpPortFlagKey,
@@ -42,15 +45,16 @@ import {
4245
WalletNodeUseTcpFlagKey,
4346
} from './flags'
4447
import { IronfishCliPKG } from './package'
48+
import * as ui from './ui'
4549
import { hasUserResponseError } from './utils'
4650
import { WalletConfig, WalletConfigOptions } from './walletConfig'
51+
import { walletNode } from './walletNode'
4752

4853
export type SIGNALS = 'SIGTERM' | 'SIGINT' | 'SIGUSR2'
4954

5055
export type FLAGS =
5156
| typeof DataDirFlagKey
5257
| typeof ConfigFlagKey
53-
| typeof NetworkIdFlagKey
5458
| typeof RpcUseIpcFlagKey
5559
| typeof RpcUseTcpFlagKey
5660
| typeof RpcTcpHostFlagKey
@@ -75,8 +79,6 @@ export abstract class IronfishCommand extends Command {
7579
// run() is called and it provides a lot of value
7680
sdk!: IronfishSdk
7781

78-
walletConfig!: WalletConfig
79-
8082
/**
8183
* Use this logger instance for debug/error output.
8284
* Actual command output should use `this.log` instead.
@@ -88,16 +90,26 @@ export abstract class IronfishCommand extends Command {
8890
*/
8991
closing = false
9092

93+
client: RpcClient | null = null
94+
95+
walletConfig!: WalletConfig
96+
97+
public static baseFlags = {
98+
[VerboseFlagKey]: VerboseFlag,
99+
[ConfigFlagKey]: ConfigFlag,
100+
[DataDirFlagKey]: DataDirFlag,
101+
}
102+
91103
constructor(argv: string[], config: Config) {
92104
super(argv, config)
93105
this.logger = createRootLogger().withTag(this.ctor.id)
94106
}
95107

96-
abstract start(): Promise<void> | void
108+
abstract start(): Promise<unknown> | void
97109

98-
async run(): Promise<void> {
110+
async run(): Promise<unknown> {
99111
try {
100-
await this.start()
112+
return await this.start()
101113
} catch (error: unknown) {
102114
if (hasUserResponseError(error)) {
103115
this.log(error.codeMessage)
@@ -107,9 +119,9 @@ export abstract class IronfishCommand extends Command {
107119
}
108120

109121
this.exit(1)
110-
} else if (error instanceof ExitError) {
122+
} else if (error instanceof Errors.ExitError) {
111123
throw error
112-
} else if (error instanceof CLIError) {
124+
} else if (error instanceof Errors.CLIError) {
113125
throw error
114126
} else if (error instanceof RpcConnectionError) {
115127
this.log(`Cannot connect to your node, start your node first.`)
@@ -123,16 +135,15 @@ export abstract class IronfishCommand extends Command {
123135
} else {
124136
throw error
125137
}
138+
} finally {
139+
this.client?.close()
126140
}
127141

128142
this.exit(0)
129143
}
130144

131145
async init(): Promise<void> {
132-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
133-
const commandClass = this.constructor as any
134-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
135-
const { flags } = await this.parse(commandClass)
146+
const { flags } = await this.parse(this.ctor)
136147

137148
// Get the flags from the flag object which is unknown
138149
const dataDirFlag = getFlag(flags, DataDirFlagKey)
@@ -160,11 +171,13 @@ export abstract class IronfishCommand extends Command {
160171
const rpcTcpHostFlag = getFlag(flags, RpcTcpHostFlagKey)
161172
if (typeof rpcTcpHostFlag === 'string') {
162173
configOverrides.rpcTcpHost = rpcTcpHostFlag
174+
configOverrides.enableRpcTcp = true
163175
}
164176

165177
const rpcTcpPortFlag = getFlag(flags, RpcTcpPortFlagKey)
166178
if (typeof rpcTcpPortFlag === 'number') {
167179
configOverrides.rpcTcpPort = rpcTcpPortFlag
180+
configOverrides.enableRpcTcp = true
168181
}
169182

170183
const rpcConnectHttpFlag = getFlag(flags, RpcUseHttpFlagKey)
@@ -178,11 +191,13 @@ export abstract class IronfishCommand extends Command {
178191
const rpcHttpHostFlag = getFlag(flags, RpcHttpHostFlagKey)
179192
if (typeof rpcHttpHostFlag === 'string') {
180193
configOverrides.rpcHttpHost = rpcHttpHostFlag
194+
configOverrides.enableRpcHttp = true
181195
}
182196

183197
const rpcHttpPortFlag = getFlag(flags, RpcHttpPortFlagKey)
184198
if (typeof rpcHttpPortFlag === 'number') {
185199
configOverrides.rpcHttpPort = rpcHttpPortFlag
200+
configOverrides.enableRpcHttp = true
186201
}
187202

188203
const rpcTcpTlsFlag = getFlag(flags, RpcTcpTlsFlagKey)
@@ -206,11 +221,6 @@ export abstract class IronfishCommand extends Command {
206221
internalOverrides.rpcAuthToken = rpcAuthFlag
207222
}
208223

209-
const networkId = getFlag(flags, NetworkIdFlagKey)
210-
if (typeof networkId === 'number') {
211-
internalOverrides.networkId = networkId
212-
}
213-
214224
this.sdk = await IronfishSdk.init({
215225
pkg: IronfishCliPKG,
216226
configOverrides: configOverrides,
@@ -326,9 +336,111 @@ export abstract class IronfishCommand extends Command {
326336
closeFromSignal(signal: NodeJS.Signals): Promise<unknown> {
327337
throw new Error(`Not implemented closeFromSignal: ${signal}`)
328338
}
339+
340+
// Override the built-in logJson method to implement our own colorizer that
341+
// works with default terminal colors instead of requiring a theme to be
342+
// configured.
343+
logJson(json: unknown): void {
344+
ux.stdout(ui.json(json))
345+
}
346+
347+
async connectRpcConfig(
348+
forceLocal = false,
349+
forceRemote = false,
350+
): Promise<Pick<RpcClient, 'config'>> {
351+
forceRemote = forceRemote || this.sdk.config.get('enableRpcTcp')
352+
353+
if (!forceLocal) {
354+
if (forceRemote) {
355+
await this.sdk.client.connect()
356+
return this.sdk.client
357+
}
358+
359+
const connected = await this.sdk.client.tryConnect()
360+
if (connected) {
361+
return this.sdk.client
362+
}
363+
}
364+
365+
// This connection uses a wallet node since that is the most granular type
366+
// of node available. This can be refactored in the future if needed.
367+
const node = await walletNode({
368+
connectNodeClient: false,
369+
walletConfig: this.walletConfig,
370+
sdk: this.sdk,
371+
})
372+
373+
const clientMemory = new RpcMemoryClient(
374+
this.sdk.logger,
375+
node.rpc.getRouter([ApiNamespace.config]),
376+
)
377+
this.client = clientMemory
378+
return clientMemory
379+
}
380+
381+
async connectRpcWallet(
382+
options: {
383+
forceLocal?: boolean
384+
forceRemote?: boolean
385+
connectNodeClient?: boolean
386+
} = {
387+
forceLocal: false,
388+
forceRemote: false,
389+
connectNodeClient: false,
390+
},
391+
): Promise<RpcClientWallet> {
392+
const forceRemote =
393+
options.forceRemote || this.sdk.config.get('enableRpcTcp')
394+
395+
if (!options.forceLocal) {
396+
if (forceRemote) {
397+
await this.sdk.client.connect()
398+
this.client = this.sdk.client
399+
return this.sdk.client
400+
}
401+
402+
const connected = await this.sdk.client.tryConnect()
403+
if (connected) {
404+
this.client = this.sdk.client
405+
return this.sdk.client
406+
}
407+
}
408+
409+
const namespaces = [
410+
ApiNamespace.config,
411+
ApiNamespace.faucet,
412+
ApiNamespace.rpc,
413+
ApiNamespace.wallet,
414+
ApiNamespace.worker,
415+
]
416+
417+
const node = await walletNode({
418+
connectNodeClient: !!options.connectNodeClient,
419+
sdk: this.sdk,
420+
walletConfig: this.walletConfig,
421+
})
422+
423+
const clientMemory = new RpcMemoryClient(
424+
this.sdk.logger,
425+
node.rpc.getRouter(namespaces),
426+
)
427+
428+
await node.waitForOpen()
429+
if (options.connectNodeClient) {
430+
await node.connectRpc()
431+
}
432+
433+
this.client = clientMemory
434+
return clientMemory
435+
}
329436
}
330437

331-
function getFlag(flags: unknown, flag: FLAGS): unknown | null {
438+
export type RpcClientWallet = Pick<
439+
RpcClient,
440+
'config' | 'rpc' | 'wallet' | 'worker' | 'faucet'
441+
>
442+
443+
function getFlag(flags: unknown, flag: FLAGS): unknown {
332444
return typeof flags === 'object' && flags !== null && flag in flags
333445
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
334446
(flags as any)[flag]

0 commit comments

Comments
 (0)