From 4af1b4ced43a9f6af82d333b9572aed783f5e9f1 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Tue, 9 Apr 2024 21:10:44 +1000 Subject: [PATCH] refactor(account)!: move `aecli account spend` to `aecli spend` BREAKING CHANGE: `aecli account spend` renamed to `aecli spend` --- CLI.md | 2 +- src/actions/account.js | 30 +------------ src/aecli-spend.js | 5 +++ src/arguments.js | 4 +- src/commands/account.js | 41 ++---------------- src/commands/main.js | 1 + src/commands/spend.js | 59 ++++++++++++++++++++++++++ test/account.js | 69 ------------------------------ test/chain.js | 3 ++ test/index.js | 9 ++-- test/name.js | 10 ++--- test/spend.js | 93 +++++++++++++++++++++++++++++++++++++++++ 12 files changed, 179 insertions(+), 147 deletions(-) create mode 100755 src/aecli-spend.js create mode 100644 src/commands/spend.js create mode 100644 test/spend.js diff --git a/CLI.md b/CLI.md index 751c5e67..e9354f36 100644 --- a/CLI.md +++ b/CLI.md @@ -171,7 +171,7 @@ Wallet path___________________ /path-to/wallet.json Using this command, you can send coins to another wallet. Just indicate another account's address and an amount which should be sent. ``` -$ aecli account spend ./wallet.json --password top-secret ak_2GN72gRFHYmJd1DD2g2sLADr5ZXa13DPYNtuFajhsZT2y3FiWu 1.23ae +$ aecli spend ./wallet.json --password top-secret ak_2GN72gRFHYmJd1DD2g2sLADr5ZXa13DPYNtuFajhsZT2y3FiWu 1.23ae ``` As an option, you can set _--ttl_ parameter, which limits the lifespan of this transaction. diff --git a/src/actions/account.js b/src/actions/account.js index ff35b1d7..fb1007e7 100644 --- a/src/actions/account.js +++ b/src/actions/account.js @@ -3,12 +3,10 @@ // This script initialize all `account` function import fs from 'fs-extra'; -import { - generateKeyPair, encode, Encoding, verifyMessage as _verifyMessage, -} from '@aeternity/aepp-sdk'; +import { generateKeyPair, verifyMessage as _verifyMessage } from '@aeternity/aepp-sdk'; import { writeWallet } from '../utils/account.js'; import { initSdkByWalletFile, getAccountByWalletFile } from '../utils/cli.js'; -import { print, printTransaction, printUnderscored } from '../utils/print.js'; +import { print, printUnderscored } from '../utils/print.js'; import { PROMPT_TYPE, prompt } from '../utils/prompt.js'; // ## `Sign message` function @@ -67,30 +65,6 @@ export async function sign(walletPath, tx, { networkId: networkIdOpt, json, ...o } } -// ## `Spend` function -// this function allow you to `send` coins to another `account` -export async function spend( - walletPath, - receiverNameOrAddress, - { amount, fraction }, - { - ttl, json, nonce, fee, payload, ...options - }, -) { - const sdk = await initSdkByWalletFile(walletPath, options); - - const tx = await sdk[amount != null ? 'spend' : 'transferFunds']( - amount ?? fraction / 100, - receiverNameOrAddress, - { - ttl, nonce, payload: encode(Buffer.from(payload), Encoding.Bytearray), fee, - }, - ); - - if (!json) print('Transaction mined'); - printTransaction(tx, json); -} - // ## Get `address` function // This function allow you retrieve account `public` and `private` keys export async function getAddress(walletPath, options) { diff --git a/src/aecli-spend.js b/src/aecli-spend.js new file mode 100755 index 00000000..fa9d32b7 --- /dev/null +++ b/src/aecli-spend.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +import program from './commands/spend.js'; +import { runProgram } from './utils/CliError.js'; + +await runProgram(program); diff --git a/src/arguments.js b/src/arguments.js index 32e4d5c2..e5c26d9b 100644 --- a/src/arguments.js +++ b/src/arguments.js @@ -34,7 +34,7 @@ export const forceOption = new Option('-f, --force', 'Ignore node version compat export const passwordOption = new Option('-P, --password [password]', 'Wallet Password'); -export const ttlOption = (usingNode) => new Option('-T, --ttl [ttl]', 'Validity of the transaction in number of blocks') - .default(noValue, usingNode ? 'current height increased by 3' : 'infinity'); +export const ttlOption = (usingNode) => new Option('-T, --ttl [ttl]', 'Validity of the transaction in number of keyblocks, or without this limit if 0') + .default(noValue, usingNode ? 3 : 0); export const networkIdOption = new Option('--networkId [networkId]', 'Network id'); diff --git a/src/commands/account.js b/src/commands/account.js index bd63ffe3..b8b846a0 100644 --- a/src/commands/account.js +++ b/src/commands/account.js @@ -5,53 +5,16 @@ import { Command } from 'commander'; import * as Account from '../actions/account.js'; import { - nodeOption, - jsonOption, - coinAmountParser, - feeOption, - forceOption, - passwordOption, - ttlOption, - networkIdOption, + nodeOption, jsonOption, forceOption, passwordOption, networkIdOption, } from '../arguments.js'; const program = new Command().name('aecli account'); // ## Initialize `options` const addCommonOptions = (p) => p - .addOption(nodeOption) .addOption(passwordOption) - .addOption(forceOption) .addOption(jsonOption); -// ## Initialize `spend` command -// -// You can use this command to send tokens to another account -// -// Example: `aecli account spend ./myWalletKeyFile ak_1241rioefwj23f2wfdsfsdsdfsasdf 100 --password testpassword` -// -// Example: `aecli account spend ./myWalletKeyFile aensAccountName.chain 100 --password testpassword` -// -// You can set transaction `ttl(Time to leave)`. If not set use default. -// -// Example: `aecli account spend ./myWalletKeyFile ak_1241rioefwj23f2wfdsfsdsdfsasdf 100 --password testpassword --ttl 20` --> this tx will leave for 20 blocks -addCommonOptions(program - .command('spend ') - .argument('', 'Address or name of recipient account') - .argument( - '', - 'Amount of coins to send in aettos/ae (example 1.2ae), or percent of sender balance (example 42%)', - (amount) => { - if (amount.endsWith('%')) return { fraction: +amount.slice(0, -1) }; - return { amount: coinAmountParser(amount) }; - }, - ) - .option('--payload [payload]', 'Transaction payload.', '') - .addOption(feeOption) - .addOption(ttlOption(true)) - .option('-N, --nonce [nonce]', 'Override the nonce that the transaction is going to be sent with') - .action(Account.spend)); - // ## Initialize `sign` command // // You can use this command to sign your transaction's @@ -59,6 +22,8 @@ addCommonOptions(program // Example: `aecli account sign ./myWalletKeyFile tx_1241rioefwj23f2wfdsfsdsdfsasdf --password testpassword` addCommonOptions(program .command('sign ') + .addOption(nodeOption) + .addOption(forceOption) .addOption(networkIdOption) .description('Sign a transaction using wallet') .action(Account.sign)); diff --git a/src/commands/main.js b/src/commands/main.js index 8978a3db..8ae8d49d 100644 --- a/src/commands/main.js +++ b/src/commands/main.js @@ -25,6 +25,7 @@ const EXECUTABLE_CMD = [ { name: 'name', desc: 'AENS system' }, { name: 'tx', desc: 'Transaction builder' }, { name: 'oracle', desc: 'Interact with oracles' }, + { name: 'spend', desc: 'Send coins to account or contract' }, ]; (() => { diff --git a/src/commands/spend.js b/src/commands/spend.js new file mode 100644 index 00000000..ea200e5e --- /dev/null +++ b/src/commands/spend.js @@ -0,0 +1,59 @@ +import { Command } from 'commander'; +import { encode, Encoding } from '@aeternity/aepp-sdk'; +import { initSdkByWalletFile } from '../utils/cli.js'; +import { print, printTransaction } from '../utils/print.js'; +import { + nodeOption, + jsonOption, + coinAmountParser, + feeOption, + forceOption, + passwordOption, + ttlOption, +} from '../arguments.js'; + +export default new Command('aecli spend') + .description('Sends coins to another account or contract.') + .addHelpText('after', ` + +Example call: + $ aecli spend ./wallet.json ak_2GN72... 100 --password top-secret + $ aecli spend ./wallet.json aens-name.chain 1.23ae --password top-secret + $ aecli spend ./wallet.json ak_2GN72... 20% --password top-secret --ttl 20`) + .argument('', 'A path to wallet file') + .argument('', 'Address or name of recipient account') + .argument( + '', + 'Amount of coins to send in aettos/ae (example: 1.2ae), or percent of sender balance (example: 42%)', + (amount) => { + if (amount.endsWith('%')) return { fraction: +amount.slice(0, -1) }; + return { amount: coinAmountParser(amount) }; + }, + ) + .option('--payload [payload]', 'Transaction payload as text', '') + .addOption(feeOption) + .addOption(ttlOption(true)) + .option('-N, --nonce [nonce]', 'Override the nonce that the transaction is going to be sent with') + .addOption(nodeOption) + .addOption(passwordOption) + .addOption(forceOption) + .addOption(jsonOption) + .action(async ( + walletPath, + receiverNameOrAddress, + { amount, fraction }, + { + ttl, json, nonce, fee, payload, ...options + }, + ) => { + const sdk = await initSdkByWalletFile(walletPath, options); + const tx = await sdk[amount != null ? 'spend' : 'transferFunds']( + amount ?? fraction / 100, + receiverNameOrAddress, + { + ttl, nonce, payload: encode(Buffer.from(payload), Encoding.Bytearray), fee, + }, + ); + if (!json) print('Transaction mined'); + printTransaction(tx, json); + }); diff --git a/test/account.js b/test/account.js index e0962f2d..ab517e9c 100644 --- a/test/account.js +++ b/test/account.js @@ -74,75 +74,6 @@ Secret Key ______________________________ ${keypair.secretKey} .to.be.a('string'); }); - it('Spend coins to another wallet', async () => { - const amount = 100; - const { publicKey } = generateKeyPair(); - const resJson = await executeAccount([ - 'spend', WALLET_NAME, '--password', 'test', publicKey, amount, '--json', - ]); - const receiverBalance = await sdk.getBalance(publicKey); - expect(+receiverBalance).to.be.equal(amount); - - expect(resJson.tx.fee).to.be.a('string'); - expect(resJson).to.eql({ - blockHash: resJson.blockHash, - blockHeight: resJson.blockHeight, - encodedTx: resJson.encodedTx, - hash: resJson.hash, - rawTx: resJson.rawTx, - signatures: [resJson.signatures[0]], - tx: { - amount: '100', - fee: resJson.tx.fee, - nonce: 1, - payload: 'ba_Xfbg4g==', - recipientId: resJson.tx.recipientId, - senderId: resJson.tx.senderId, - ttl: resJson.tx.ttl, - type: 'SpendTx', - version: 1, - }, - }); - - const res = await executeAccount([ - 'spend', WALLET_NAME, '--password', 'test', publicKey, amount, - ]); - const lineEndings = res.split('\n').map((l) => l.split(' ').at(-1)); - expect(res).to.be.equal(` -Transaction mined -Tx hash _________________________________ ${lineEndings[1]} -Block hash ______________________________ ${lineEndings[2]} -Block height ____________________________ ${lineEndings[3]} -Signatures ______________________________ ${lineEndings[4]} -Tx Type _________________________________ SpendTx -Sender account __________________________ ${resJson.tx.senderId} -Recipient account _______________________ ${resJson.tx.recipientId} -Amount __________________________________ 100 -Payload _________________________________ ba_Xfbg4g== -Fee _____________________________________ ${resJson.tx.fee} -Nonce ___________________________________ 2 -TTL _____________________________________ ${lineEndings[12]} -Version _________________________________ 1 - `.trim()); - }); - - it('Spend coins to another wallet in ae', async () => { - const receiverKeys = generateKeyPair(); - const { tx: { fee } } = await executeAccount([ - 'spend', WALLET_NAME, '--password', 'test', '--json', - receiverKeys.publicKey, '1ae', '--fee', '0.02ae', - ]); - expect(await sdk.getBalance(receiverKeys.publicKey)).to.be.equal('1000000000000000000'); - expect(fee).to.be.equal('20000000000000000'); - }); - - it('Spend percent of coins to account', async () => { - const { publicKey } = generateKeyPair(); - const balanceBefore = await sdk.getBalance(sdk.address); - await executeAccount(['spend', WALLET_NAME, '--password', 'test', publicKey, '42%']); - expect(+await sdk.getBalance(publicKey)).to.be.equal(balanceBefore * 0.42); - }); - it('Sign message', async () => { const data = 'Hello world'; const signedMessage = await executeAccount(['sign-message', WALLET_NAME, data, '--json', '--password', 'test']); diff --git a/test/chain.js b/test/chain.js index 77cfe2cc..5627504e 100644 --- a/test/chain.js +++ b/test/chain.js @@ -10,6 +10,9 @@ describe('Chain Module', () => { before(async () => { sdk = await getSdk(); + for (let i = 0; i < 5; i += 1) { + await sdk.spend(0, sdk.address); // eslint-disable-line no-await-in-loop + } }); it('prints top', async () => { diff --git a/test/index.js b/test/index.js index a3770f2d..6ab35f8d 100644 --- a/test/index.js +++ b/test/index.js @@ -84,9 +84,12 @@ export async function executeProgram(program, args) { try { const allArgs = [ ...args.map((arg) => arg.toString()), - ...[ - 'config', 'select-node', 'select-compiler', - ].includes(args[0]) ? [] : ['--url', url], + ...['config', 'select-node', 'select-compiler'].includes(args[0]) + || ( + // eslint-disable-next-line no-underscore-dangle + program._name === 'aecli account' + && ['save', 'create', 'address', 'sign-message', 'verify-message'].includes(args[0]) + ) ? [] : ['--url', url], ...[ 'compile', 'deploy', 'call', 'encode-calldata', 'decode-call-result', ].includes(args[0]) && !args.includes('--compilerUrl') ? ['--compilerUrl', compilerUrl] : [], diff --git a/test/name.js b/test/name.js index 0192fd01..cea6c0b2 100644 --- a/test/name.js +++ b/test/name.js @@ -6,11 +6,11 @@ import { } from './index.js'; import nameProgram from '../src/commands/name.js'; import inspectProgram from '../src/commands/inspect.js'; -import accountProgram from '../src/commands/account.js'; +import spendProgram from '../src/commands/spend.js'; const executeName = (args) => executeProgram(nameProgram, args); const executeInspect = (args) => executeProgram(inspectProgram, args); -const executeAccount = (args) => executeProgram(accountProgram, args); +const executeSpend = (args) => executeProgram(spendProgram, args); describe('AENS Module', () => { const { publicKey } = generateKeyPair(); @@ -138,8 +138,7 @@ describe('AENS Module', () => { it('Fail spend by name on invalid input', async () => { const amount = 100000009; - await executeAccount([ - 'spend', + await executeSpend([ WALLET_NAME, '--password', 'test', @@ -151,8 +150,7 @@ describe('AENS Module', () => { it('Spend by name', async () => { const amount = 100000009; - const { tx: { recipientId } } = await executeAccount([ - 'spend', + const { tx: { recipientId } } = await executeSpend([ WALLET_NAME, '--password', 'test', diff --git a/test/spend.js b/test/spend.js new file mode 100644 index 00000000..127703e7 --- /dev/null +++ b/test/spend.js @@ -0,0 +1,93 @@ +import { before, describe, it } from 'mocha'; +import { expect } from 'chai'; +import { generateKeyPair } from '@aeternity/aepp-sdk'; +import { getSdk, executeProgram, WALLET_NAME } from './index.js'; +import spendProgram from '../src/commands/spend.js'; + +const executeSpend = (args) => ( + executeProgram(spendProgram, [WALLET_NAME, '--password', 'test', ...args]) +); + +describe('Spend', () => { + let sdk; + + before(async () => { + sdk = await getSdk(); + }); + + it('spends', async () => { + const amount = 100; + const { publicKey } = generateKeyPair(); + const resJson = await executeSpend([publicKey, amount, '--json']); + const receiverBalance = await sdk.getBalance(publicKey); + expect(+receiverBalance).to.be.equal(amount); + + expect(resJson.tx.fee).to.be.a('string'); + expect(resJson).to.eql({ + blockHash: resJson.blockHash, + blockHeight: resJson.blockHeight, + encodedTx: resJson.encodedTx, + hash: resJson.hash, + rawTx: resJson.rawTx, + signatures: [resJson.signatures[0]], + tx: { + amount: '100', + fee: resJson.tx.fee, + nonce: 1, + payload: 'ba_Xfbg4g==', + recipientId: resJson.tx.recipientId, + senderId: resJson.tx.senderId, + ttl: resJson.tx.ttl, + type: 'SpendTx', + version: 1, + }, + }); + + const res = await executeSpend([publicKey, amount]); + const lineEndings = res.split('\n').map((l) => l.split(' ').at(-1)); + expect(res).to.be.equal(` +Transaction mined +Tx hash _________________________________ ${lineEndings[1]} +Block hash ______________________________ ${lineEndings[2]} +Block height ____________________________ ${lineEndings[3]} +Signatures ______________________________ ${lineEndings[4]} +Tx Type _________________________________ SpendTx +Sender account __________________________ ${resJson.tx.senderId} +Recipient account _______________________ ${resJson.tx.recipientId} +Amount __________________________________ 100 +Payload _________________________________ ba_Xfbg4g== +Fee _____________________________________ ${resJson.tx.fee} +Nonce ___________________________________ 2 +TTL _____________________________________ ${lineEndings[12]} +Version _________________________________ 1 + `.trim()); + }); + + it('spends in ae', async () => { + const receiverKeys = generateKeyPair(); + const { tx: { fee } } = await executeSpend([ + '--json', receiverKeys.publicKey, '1ae', '--fee', '0.02ae', + ]); + expect(await sdk.getBalance(receiverKeys.publicKey)).to.be.equal('1000000000000000000'); + expect(fee).to.be.equal('20000000000000000'); + }); + + it('spends percent of balance', async () => { + const { publicKey } = generateKeyPair(); + const balanceBefore = await sdk.getBalance(sdk.address); + await executeSpend([publicKey, '42%']); + expect(+await sdk.getBalance(publicKey)).to.be.equal(balanceBefore * 0.42); + }); + + it('spends to contract', async () => { + const contract = await sdk.initializeContract({ + sourceCode: '' + + 'payable contract Main =\n' + + ' record state = { key: int }\n' + + ' entrypoint init() = { key = 0 }\n', + }); + const { address } = await contract.$deploy([]); + await executeSpend([address, 100]); + expect(await sdk.getBalance(address)).to.be.equal('100'); + }); +});