Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Create an unlocking transaction command - Closes #4929 #5069

Merged
merged 9 commits into from
Mar 31, 2020
9 changes: 4 additions & 5 deletions commander/src/commands/transaction/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ import { flags as commonFlags } from '../../utils/flags';
import DelegateCommand from './create/delegate';
import MultisignatureCommand from './create/multisignature';
import TransferCommand from './create/transfer';
import UnlockCommand from './create/unlock';
import VoteCommand from './create/vote';

const MAX_ARG_NUM = 5;

interface TypeNumberMap {
readonly [key: string]: string;
}
Expand All @@ -34,6 +33,7 @@ const typeNumberMap: TypeNumberMap = {
'10': 'delegate',
'11': 'vote',
'12': 'multisignature',
'14': 'unlock',
};

const options = Object.entries(typeNumberMap).reduce(
Expand All @@ -54,6 +54,7 @@ const typeClassMap: TypeClassMap = {
vote: VoteCommand,
delegate: DelegateCommand,
multisignature: MultisignatureCommand,
unlock: UnlockCommand,
};

const resolveFlags = (
Expand All @@ -72,9 +73,7 @@ const resolveFlags = (
};

export default class CreateCommand extends BaseCommand {
static args = new Array(MAX_ARG_NUM).fill(0).map(i => ({
name: `${i}_arg`,
}));
static strict = false;

static description = `
Creates a transaction object.
Expand Down
193 changes: 193 additions & 0 deletions commander/src/commands/transaction/create/unlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* LiskHQ/lisk-commander
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*
*/
import {
unlockToken,
utils as transactionUtils,
} from '@liskhq/lisk-transactions';
import {
isNumberString,
isValidFee,
isValidNonce,
validateAddress,
} from '@liskhq/lisk-validator';
import { flags as flagParser } from '@oclif/command';

import BaseCommand from '../../../base';
import { ValidationError } from '../../../utils/error';
import { flags as commonFlags } from '../../../utils/flags';
import { getNetworkIdentifierWithInput } from '../../../utils/network_identifier';
import { getPassphraseFromPrompt } from '../../../utils/reader';

interface RawAssetUnlock {
readonly delegateAddress: string;
readonly amount: string;
readonly unvoteHeight: number;
}

const createUnlockTransaction = (
nonce: string,
fee: string,
networkIdentifier: string,
unlockingObjects: ReadonlyArray<RawAssetUnlock>,
passphrase?: string,
) =>
unlockToken({
nonce,
fee,
networkIdentifier,
passphrase,
unlockingObjects,
});

interface Args {
readonly nonce: string;
readonly fee: string;
}

const splitInputs = (unlock: string) =>
unlock
.split(',')
.filter(Boolean)
.map(u => u.trim());

const validateUnlocks = (
unlocks: ReadonlyArray<string>,
): ReadonlyArray<RawAssetUnlock> => {
const rawAssetUnlock = [];
for (const unlock of unlocks) {
const [delegateAddress, amount, unvoteHeight] = splitInputs(unlock);
if (!validateAddress(delegateAddress)) {
throw new ValidationError('Enter a valid address in LSK string format.');
}
const normalizedAmount = transactionUtils.convertLSKToBeddows(amount);

if (!isValidFee(normalizedAmount)) {
throw new ValidationError(
'Enter a valid amount in number string format.',
);
}

if (!isNumberString(unvoteHeight)) {
throw new ValidationError(
'Enter the unvoteHeight in valid number format.',
);
}

rawAssetUnlock.push({
delegateAddress,
amount: normalizedAmount,
unvoteHeight: parseInt(unvoteHeight, 10),
});
}

return rawAssetUnlock;
};

export default class UnlockCommand extends BaseCommand {
static args = [
{
name: 'nonce',
required: true,
description: 'Nonce of the transaction.',
},
{
name: 'fee',
required: true,
description: 'Transaction fee in LSK.',
},
];
static description = `
Creates a transaction which will unlock tokens voted for delegates and add them back to the sender balance.
`;

static examples = [
'transaction:create:unlock 1 100 --unlock="123L,1000000000,500"',
'transaction:create:unlock 1 100 --unlock="123L,1000000000,500" --unlock="456L,1000000000,500"',
];

static flags = {
...BaseCommand.flags,
unlock: flagParser.string({ ...commonFlags.unlock, multiple: true }),
'no-signature': flagParser.boolean(commonFlags.noSignature),
networkIdentifier: flagParser.string(commonFlags.networkIdentifier),
passphrase: flagParser.string(commonFlags.passphrase),
};

async run(): Promise<void> {
const {
args,
flags: {
unlock: unlocks,
networkIdentifier: networkIdentifierSource,
passphrase: passphraseSource,
'no-signature': noSignature,
},
} = this.parse(UnlockCommand);

const { nonce, fee } = args as Args;

if (!isValidNonce(nonce)) {
throw new ValidationError('Enter a valid nonce in number string format.');
}

if (Number.isNaN(Number(fee))) {
throw new ValidationError('Enter a valid fee in number string format.');
}

const normalizedFee = transactionUtils.convertLSKToBeddows(fee);

if (!isValidFee(normalizedFee)) {
throw new ValidationError('Enter a valid fee in number string format.');
}

if (!unlocks?.length) {
throw new ValidationError(
'At least one unlock object options must be provided.',
);
}

const unlockingObjects = validateUnlocks(unlocks);

const networkIdentifier = getNetworkIdentifierWithInput(
networkIdentifierSource,
this.userConfig.api.network,
);

if (noSignature) {
const noSignatureResult = createUnlockTransaction(
nonce,
normalizedFee,
networkIdentifier,
unlockingObjects,
);
this.print(noSignatureResult);

return;
}

const passphrase =
passphraseSource ?? (await getPassphraseFromPrompt('passphrase', true));

const result = createUnlockTransaction(
nonce,
normalizedFee,
networkIdentifier,
unlockingObjects,
passphrase,
);
this.print(result);
}
}
8 changes: 8 additions & 0 deletions commander/src/utils/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ const votesDescription = `Specifies the public keys for the delegate candidates
- --votes=publickey1,publickey2
`;

const unlockDescription = `Specifies the unlock objects for the delegate candidates to unlock from. Takes a string of address amount unvoteHeight separated by commas.
Examples:
- --unlock=123L,1000000,500
`;

const unvotesDescription = `Specifies the public keys for the delegate candidates you want to remove your vote from. Takes a string of public keys separated by commas.
Examples:
- --unvotes=publickey1,publickey2
Expand Down Expand Up @@ -121,6 +126,9 @@ export const flags: FlagMap = {
votes: {
description: votesDescription,
},
unlock: {
description: unlockDescription,
},
networkIdentifier: {
description: networkIdentifierDescription,
},
Expand Down
54 changes: 47 additions & 7 deletions commander/test/commands/transaction/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import TransferCommand from '../../../src/commands/transaction/create/transfer';
import MultisignatureCommand from '../../../src/commands/transaction/create/multisignature';
import DelegateCommand from '../../../src/commands/transaction/create/delegate';
import VoteCommand from '../../../src/commands/transaction/create/vote';
import UnlockCommand from '../../../src/commands/transaction/create/unlock';

describe('transaction:create', () => {
const printMethodStub = sandbox.stub();
Expand All @@ -35,7 +36,8 @@ describe('transaction:create', () => {
.stub(TransferCommand, 'run', sandbox.stub())
.stub(DelegateCommand, 'run', sandbox.stub())
.stub(VoteCommand, 'run', sandbox.stub())
.stub(MultisignatureCommand, 'run', sandbox.stub());
.stub(MultisignatureCommand, 'run', sandbox.stub())
.stub(UnlockCommand, 'run', sandbox.stub());

describe('transaction:create', () => {
setupTest()
Expand All @@ -58,27 +60,27 @@ describe('transaction:create', () => {

setupTest()
.command(['transaction:create', '--type=8'])
.it('should call type 0 command with flag type=9', () => {
.it('should call type 8 command with flag type=8', () => {
return expect(TransferCommand.run).to.be.calledWithExactly([]);
});

setupTest()
.command(['transaction:create', '--type=transfer'])
.it('should call type 0 command with flag type=transfer', () => {
.it('should call type 8 transfer with flag type=transfer', () => {
return expect(TransferCommand.run).to.be.calledWithExactly([]);
});

setupTest()
.command(['transaction:create', '--type=10', 'username'])
.it('should call type 2 command with flag type=10', () => {
.it('should call type 10 command with flag type=10', () => {
return expect(DelegateCommand.run).to.be.calledWithExactly([
'username',
]);
});

setupTest()
.command(['transaction:create', '-t=delegate', '--json', 'username'])
.it('should call type 2 command with flag type=delegate', () => {
.it('should call type 10 command with flag type=delegate', () => {
return expect(DelegateCommand.run).to.be.calledWithExactly([
'username',
'--json',
Expand All @@ -87,7 +89,7 @@ describe('transaction:create', () => {

setupTest()
.command(['transaction:create', '--type=11', '--votes=xxx,yyy'])
.it('should call type 3 command with flag type=11', () => {
.it('should call type 11 command with flag type=11', () => {
return expect(VoteCommand.run).to.be.calledWithExactly([
'--votes',
'xxx,yyy',
Expand All @@ -96,7 +98,7 @@ describe('transaction:create', () => {

setupTest()
.command(['transaction:create', '-t=vote', '--votes=xxx,xxx'])
.it('should call type 3 command with flag type=vote', () => {
.it('should call type 11 command with flag type=vote', () => {
return expect(VoteCommand.run).to.be.calledWithExactly([
'--votes',
'xxx,xxx',
Expand Down Expand Up @@ -130,5 +132,43 @@ describe('transaction:create', () => {
'--optional-key=yyy',
]);
});

setupTest()
.command(['transaction:create', '--type=14', '--unlock=xxx,yyy,zzz'])
.it('should call type 14 command with flag type=14', () => {
return expect(UnlockCommand.run).to.be.calledWithExactly([
'--unlock=xxx,yyy,zzz',
]);
});

setupTest()
.command(['transaction:create', '--type=unlock', '--unlock=xxx,yyy,zzz'])
.it('should call type 14 command with flag type=unlock', () => {
return expect(UnlockCommand.run).to.be.calledWithExactly([
'--unlock=xxx,yyy,zzz',
]);
});

setupTest()
.command([
'transaction:create',
'--type=unlock',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
])
.it('should allow to use more flags and arguments', () => {
return expect(UnlockCommand.run).to.be.calledWithExactly([
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
'--unlock=xxx,yyy,zzz',
]);
});
});
});
Loading