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

Add support for mutually exclusive arguments #109

Merged
merged 2 commits into from
Nov 18, 2021
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"trezor-connect": "https://github.com/vacuumlabs/connect/releases/download/8.2.2-rc.1-multisig/trezor-connect-v8.2.2-rc.1-multisig-extended.tgz"
},
"devDependencies": {
"@types/argparse": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^4.2.0",
"@typescript-eslint/parser": "^4.2.0",
"eslint": "^7.9.0",
Expand Down
61 changes: 29 additions & 32 deletions src/command-parser/commandParser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ArgumentGroup, ArgumentParser, SubParser } from 'argparse'
import { ParsedArguments } from '../types'
import { parserConfig } from './parserConfig'

const { ArgumentParser } = require('argparse')

export enum CommandType {
APP_VERSION = 'version',
DEVICE_VERSION = 'device.version',
Expand All @@ -17,36 +16,29 @@ export enum CommandType {
CATALYST_VOTING_KEY_REGISTRATION_METADATA = 'catalyst.voting-key-registration-metadata',
}

const makeParser = () => {
const initParser = (parser: any, config: any) => {
const isCommand = (str: string) => !str.startsWith('--')
const commandType = (parent: string, current: string) => (parent ? `${parent}.${current}` : current)
parser.set_defaults({ parser })
const subparsers = parser.add_subparsers()

Object.keys(config).reduce((acc, key) => {
if (isCommand(key)) {
const subparser = acc.add_parser(key)
subparser.set_defaults({ command: commandType(parser.get_default('command'), key) })
initParser(subparser, config[key])
} else {
parser.add_argument(key, config[key])
}
return acc
}, subparsers)

return parser
}

return initParser(new ArgumentParser(
{
description: 'Command line tool for ledger/trezor transaction signing',
prog: 'cardano-hw-cli',
},
), parserConfig)
const initParser = (parser: ArgumentParser | ArgumentGroup, config: any): void => {
const MUTUALLY_EXCLUSIVE_GROUP_KEY = '_mutually-exclusive-group'
const isMutuallyExclusiveGroup = (str: string) => str.startsWith(MUTUALLY_EXCLUSIVE_GROUP_KEY)
const isOneOfGroupRequired = (str: string) => str.startsWith(`${MUTUALLY_EXCLUSIVE_GROUP_KEY}-required`)
const isCommand = (str: string) => !str.startsWith('--') && !isMutuallyExclusiveGroup(str)
const commandType = (parent: string, current: string) => (parent ? `${parent}.${current}` : current)

const subparsers = 'add_subparsers' in parser ? parser.add_subparsers() : null
Object.keys(config).forEach((key) => {
if (isCommand(key)) {
const subparser = (subparsers as SubParser).add_parser(key)
subparser.set_defaults({ command: commandType(parser.get_default('command'), key) })
initParser(subparser, config[key])
} else if (isMutuallyExclusiveGroup(key)) {
const group = parser.add_mutually_exclusive_group({ required: isOneOfGroupRequired(key) })
initParser(group, config[key])
} else {
parser.add_argument(key, config[key])
}
})
}

const preProcessArgs = (inputArgs: string[]) => {
const preProcessArgs = (inputArgs: string[]): string[] => {
// First 2 args are node version and script name
const commandArgs = inputArgs.slice(2)
if (commandArgs[0] === 'shelley') {
Expand All @@ -55,7 +47,12 @@ const preProcessArgs = (inputArgs: string[]) => {
return commandArgs
}

export const parse = (inputArgs: string[]): { parser: any, parsedArgs: ParsedArguments } => {
const { parser, ...parsedArgs } = makeParser().parse_args(preProcessArgs(inputArgs))
export const parse = (inputArgs: string[]): { parser: ArgumentParser, parsedArgs: ParsedArguments } => {
const parser = new ArgumentParser({
description: 'Command line tool for ledger/trezor transaction signing',
prog: 'cardano-hw-cli',
})
initParser(parser, parserConfig)
const { ...parsedArgs } = parser.parse_args(preProcessArgs(inputArgs))
return { parser, parsedArgs }
}
47 changes: 29 additions & 18 deletions src/command-parser/parserConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ const opCertSigningArgs = {
},
}

// If you want to define a group of mutually exclusive CLI arguments (eg. see address.show below),
// bundle these arguments under a key prefixed with '_mutually-exclusive-group'. Several such groups
// may be present next to each other, an optional key suffix can be added to enable this (JS objects
// cannot have duplicate keys).
// If you want argparse to ensure that one of the arguments is present, use
// '_mutually-exclusive-group-required' prefix instead.

janmazak marked this conversation as resolved.
Show resolved Hide resolved
// based on cardano-cli interface
// https://docs.cardano.org/projects/cardano-node/en/latest/reference/cardano-node-cli-reference.html
export const parserConfig = {
Expand Down Expand Up @@ -161,25 +168,29 @@ export const parserConfig = {
'key-gen': keyGenArgs,

'show': { // hw-specific subpath
'--payment-path': {
type: (path: string) => parseBIP32Path(path),
dest: 'paymentPath',
help: 'Payment derivation path. (specify only path or script hash)',
},
'--payment-script-hash': {
type: (hashHex: string) => parseScriptHashHex(hashHex),
dest: 'paymentScriptHash',
help: 'Payment derivation script hash in hex format.',
},
'--staking-path': {
type: (path: string) => parseBIP32Path(path),
dest: 'stakingPath',
help: 'Stake derivation path. (specify only path or script hash)',
'_mutually-exclusive-group-required-payment': {
'--payment-path': {
type: (path: string) => parseBIP32Path(path),
dest: 'paymentPath',
help: 'Payment derivation path. Either this or payment script hash has to be specified.',
},
'--payment-script-hash': {
type: (hashHex: string) => parseScriptHashHex(hashHex),
dest: 'paymentScriptHash',
help: 'Payment derivation script hash in hex format.',
},
},
'--staking-script-hash': {
type: (hashHex: string) => parseScriptHashHex(hashHex),
dest: 'stakingScriptHash',
help: 'Stake derivation script hash in hex format',
'_mutually-exclusive-group-required-staking': {
'--staking-path': {
type: (path: string) => parseBIP32Path(path),
dest: 'stakingPath',
help: 'Stake derivation path. Either this or staking script hash has to be specified.',
},
'--staking-script-hash': {
type: (hashHex: string) => parseScriptHashHex(hashHex),
dest: 'stakingScriptHash',
help: 'Stake derivation script hash in hex format',
},
},
'--address-file': {
required: true,
Expand Down
16 changes: 3 additions & 13 deletions src/commandExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,10 @@ const CommandExecutor = async () => {
// eslint-disable-next-line no-console
const printDeviceVersion = async () => console.log(await cryptoProvider.getVersion())

const showAddress = async (
{
paymentPath, paymentScriptHash, stakingPath, stakingScriptHash, address,
}: ParsedShowAddressArguments,
) => {
const showAddress = async (args: ParsedShowAddressArguments) => {
// eslint-disable-next-line no-console
console.log(`address: ${address}`)
return cryptoProvider.showAddress(
paymentPath,
paymentScriptHash,
stakingPath,
stakingScriptHash,
address,
)
console.log(`address: ${args.address}`)
return cryptoProvider.showAddress(args)
}

const createSigningKeyFile = async (
Expand Down
10 changes: 4 additions & 6 deletions src/crypto-providers/ledgerCryptoProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
StakeCredentialType,
} from '../transaction/types'
import {
Address,
BIP32Path,
HexString,
HwSigningData,
Expand All @@ -46,6 +45,7 @@ import {
NativeScriptHashKeyHex,
NativeScriptType,
Network,
ParsedShowAddressArguments,
VotePublicKeyHex,
XPubKeyHex,
} from '../types'
Expand Down Expand Up @@ -93,11 +93,9 @@ export const LedgerCryptoProvider: () => Promise<CryptoProvider> = async () => {
): boolean => LEDGER_VERSIONS[feature] && isDeviceVersionGTE(deviceVersion, LEDGER_VERSIONS[feature])

const showAddress = async (
paymentPath: BIP32Path,
paymentScriptHash: string,
stakingPath: BIP32Path,
stakingScriptHash: string,
address: Address,
{
paymentPath, paymentScriptHash, stakingPath, stakingScriptHash, address,
}: ParsedShowAddressArguments,
): Promise<void> => {
try {
const { addressType, networkId, protocolMagic } = getAddressAttributes(address)
Expand Down
26 changes: 12 additions & 14 deletions src/crypto-providers/trezorCryptoProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
TxSigned,
} from '../transaction/transaction'
import {
Address,
BIP32Path,
HexString,
HwSigningData,
Expand All @@ -49,6 +48,7 @@ import {
VotePublicKeyHex,
XPubKeyHex,
NativeScriptType,
ParsedShowAddressArguments,
} from '../types'
import {
encodeAddress,
Expand Down Expand Up @@ -128,22 +128,20 @@ const TrezorCryptoProvider: () => Promise<CryptoProvider> = async () => {
): boolean => TREZOR_VERSIONS[feature] && isDeviceVersionGTE(deviceVersion, TREZOR_VERSIONS[feature])

const showAddress = async (
paymentPath: BIP32Path,
paymentScriptHash: string,
stakingPath: BIP32Path,
stakingScriptHash: string,
address: Address,
{
paymentPath, paymentScriptHash, stakingPath, stakingScriptHash, address,
}: ParsedShowAddressArguments,
): Promise<void> => {
const { addressType, networkId, protocolMagic } = getAddressAttributes(address)
const addressParameters = {
addressType,
path: paymentPath,
paymentScriptHash: paymentScriptHash || '',
stakingPath,
stakingScriptHash: stakingScriptHash || '',
}

const response = await TrezorConnect.cardanoGetAddress({
addressParameters,
addressParameters: {
addressType,
path: paymentPath || '',
paymentScriptHash: paymentScriptHash || '',
stakingPath: stakingPath || '',
stakingScriptHash: stakingScriptHash || '',
},
networkId,
protocolMagic,
showOnTrezor: true,
Expand Down
8 changes: 2 additions & 6 deletions src/crypto-providers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
HwSigningData,
BIP32Path,
Network,
Address,
XPubKeyHex,
VotePublicKeyHex,
NativeScript,
NativeScriptHashKeyHex,
NativeScriptDisplayFormat,
ParsedShowAddressArguments,
} from '../types'

export enum SigningMode {
Expand All @@ -35,11 +35,7 @@ export type SigningParameters = {
export type CryptoProvider = {
getVersion: () => Promise<string>,
showAddress: (
paymentPath: BIP32Path,
paymentScriptHash: string,
stakingPath: BIP32Path,
stakingScriptHash: string,
address: Address,
args: ParsedShowAddressArguments,
) => Promise<void>,
signTx: (
params: SigningParameters,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export type ParsedDeviceVersionArguments = {
command: CommandType.DEVICE_VERSION,
}

// only one of paymentPath vs. paymentScriptHash and stakingPath vs. stakingScriptHash
// should be present (the result of parse() complies to this)
export type ParsedShowAddressArguments = {
command: CommandType.SHOW_ADDRESS,
paymentPath: BIP32Path,
Expand Down
24 changes: 24 additions & 0 deletions test/unit/commandParser/commandParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ const prefix = (filename) => `${resFolder}${filename}`
const pad = (args) => [undefined, undefined, ...args]

describe('Command parser', () => {
it('Should parse address show command', () => {
const args = pad([
'shelley',
'address',
'show',
'--payment-path',
'1852H/1815H/0H/0/0',
'--staking-script-hash',
'14c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f1124',
'--address-file',
prefix('payment.addr'),
])
const { parsedArgs } = parse(args)
const expectedResult = {
command: CommandType.SHOW_ADDRESS,
paymentPath: [2147485500, 2147485463, 2147483648, 0, 0],
paymentScriptHash: undefined,
stakingPath: undefined,
stakingScriptHash: '14c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f1124',
address: 'addr1qxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsl3s9zt',
}
assert.deepStrictEqual(parsedArgs, expectedResult)
})

it('Should parse key-gen command', () => {
const args = pad([
'shelley',
Expand Down
1 change: 1 addition & 0 deletions test/unit/commandParser/res/payment.addr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addr1qxq0nckg3ekgzuqg7w5p9mvgnd9ym28qh5grlph8xd2z92sj922xhxkn6twlq2wn4q50q352annk3903tj00h45mgfmsl3s9zt
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@
optionalDependencies:
secp256k1 "^3.5.2"

"@types/argparse@^2.0.10":
version "2.0.10"
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.10.tgz#664e84808accd1987548d888b9d21b3e9c996a6c"
integrity sha512-C4wahC3gz3vQtvPazrJ5ONwmK1zSDllQboiWvpMM/iOswCYfBREFnjFbq/iWKIVOCl8+m5Pk6eva6/ZSsDuIGA==

"@types/component-emitter@^1.2.10":
version "1.2.10"
resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
Expand Down