diff --git a/modules/account-lib/src/coin/near/transaction.ts b/modules/account-lib/src/coin/near/transaction.ts index 93bbc192b8..a2fac3539f 100644 --- a/modules/account-lib/src/coin/near/transaction.ts +++ b/modules/account-lib/src/coin/near/transaction.ts @@ -62,7 +62,7 @@ export class Transaction extends BaseTransaction { parsedAction = { functionCall: { methodName: functionCallObject.methodName, - args: JSON.parse(functionCallObject.args.toString()), + args: JSON.parse(Buffer.from(functionCallObject.args).toString()), gas: functionCallObject.gas.toString(), deposit: functionCallObject.deposit.toString(), }, diff --git a/modules/bitgo/src/v2/coins/near.ts b/modules/bitgo/src/v2/coins/near.ts index 5061bb6ee9..409a2aafd5 100644 --- a/modules/bitgo/src/v2/coins/near.ts +++ b/modules/bitgo/src/v2/coins/near.ts @@ -1,3 +1,8 @@ +/** + * @prettier + */ + +import BigNumber from 'bignumber.js'; import * as accountLib from '@bitgo/account-lib'; import * as _ from 'lodash'; import { BitGo } from '../../bitgo'; @@ -109,9 +114,7 @@ export class Near extends BaseCoin { * @returns {Object} object with generated pub, prv */ generateKeyPair(seed?: Buffer): KeyPair { - const keyPair = seed - ? new accountLib.Near.KeyPair({ seed }) - : new accountLib.Near.KeyPair(); + const keyPair = seed ? new accountLib.Near.KeyPair({ seed }) : new accountLib.Near.KeyPair(); const keys = keyPair.getKeys(); if (!keys.prv) { throw new Error('Missing prv in key generation.'); @@ -166,9 +169,7 @@ export class Near extends BaseCoin { * Explain/parse transaction * @param params */ - async explainTransaction( - params: ExplainTransactionOptions, - ): Promise { + async explainTransaction(params: ExplainTransactionOptions): Promise { const factory = accountLib.register(this.getChain(), accountLib.Near.TransactionBuilderFactory); let rebuiltTransaction: accountLib.BaseCoin.BaseTransaction; const txRaw = params.txPrebuild.txHex; @@ -208,7 +209,6 @@ export class Near extends BaseCoin { throw new Error('missing public key parameter to sign transaction'); } - // if we are receiving addresses do not try to convert them const signer = !nearUtils.isValidAddress(params.txPrebuild.key) ? new accountLib.Near.KeyPair({ pub: params.txPrebuild.key }).getAddress() @@ -225,9 +225,7 @@ export class Near extends BaseCoin { * @param callback * @returns {Bluebird} */ - async signTransaction( - params: SignTransactionOptions, - ): Promise { + async signTransaction(params: SignTransactionOptions): Promise { const factory = accountLib.register(this.getChain(), accountLib.Near.TransactionBuilderFactory); const txBuilder = factory.from(params.txPrebuild.txHex); txBuilder.sign({ key: params.prv }); @@ -252,9 +250,7 @@ export class Near extends BaseCoin { throw new MethodNotImplementedError('Near recovery not implemented'); } - parseTransaction( - params: ParseTransactionOptions, - ): Promise { + parseTransaction(params: ParseTransactionOptions): Promise { throw new MethodNotImplementedError('Near parse transaction not implemented'); } @@ -263,8 +259,9 @@ export class Near extends BaseCoin { } async verifyTransaction(params: VerifyTransactionOptions): Promise { + let totalAmount = new BigNumber(0); const coinConfig = coins.get(this.getChain()); - const { txPrebuild: txPrebuild } = params; + const { txPrebuild: txPrebuild, txParams: txParams } = params; const transaction = new accountLib.Near.Transaction(coinConfig); const rawTx = txPrebuild.txHex; if (!rawTx) { @@ -272,13 +269,23 @@ export class Near extends BaseCoin { } transaction.fromRawTransaction(rawTx); - - // TO-DO: new explainTransaction to be implemented in account-lib - + const explainedTx = transaction.explainTransaction(); + + // users do not input recipients for consolidation requests as they are generated by the server + if (txParams.recipients !== undefined) { + const filteredRecipients = txParams.recipients?.map((recipient) => _.pick(recipient, ['address', 'amount'])); + const filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount'])); + + if (!_.isEqual(filteredOutputs, filteredRecipients)) { + throw new Error('Tx outputs does not match with expected txParams recipients'); + } + for (const recipients of txParams.recipients) { + totalAmount = totalAmount.plus(recipients.amount); + } + if (!totalAmount.isEqualTo(explainedTx.outputAmount)) { + throw new Error('Tx total amount does not match with expected total amount field'); + } + } return true; } - - getAddressFromPublicKey(Pubkey: string): string { - return new accountLib.Near.KeyPair({ pub: Pubkey }).getAddress(); - } } diff --git a/modules/bitgo/test/v2/unit/coins/near.ts b/modules/bitgo/test/v2/unit/coins/near.ts index 394e25a67e..f2fbbc7722 100644 --- a/modules/bitgo/test/v2/unit/coins/near.ts +++ b/modules/bitgo/test/v2/unit/coins/near.ts @@ -3,16 +3,40 @@ import * as accountLib from '@bitgo/account-lib'; import { TestBitGo } from '../../../lib/test_bitgo'; import { randomBytes } from 'crypto'; import { rawTx, accounts, validatorContractAddress, blockHash } from '../../fixtures/coins/near'; +import * as _ from 'lodash'; +import * as sinon from 'sinon'; describe('NEAR:', function () { let bitgo; let basecoin; + let newTxPrebuild; + let newTxParams; const factory = accountLib.register('tnear', accountLib.Near.TransactionBuilderFactory); + const txPrebuild = { + txHex: rawTx.transfer.unsigned, + txInfo: {}, + }; + + const txParams = { + recipients: [ + { + address: '9f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f0254', + amount: '1000000000000000000000000', + }, + ], + }; + before(function () { bitgo = new TestBitGo({ env: 'mock' }); bitgo.initializeTestVars(); basecoin = bitgo.coin('tnear'); + newTxPrebuild = () => { + return _.cloneDeep(txPrebuild); + }; + newTxParams = () => { + return _.cloneDeep(txParams); + }; }); it('should retun the right info', function () { @@ -106,40 +130,212 @@ describe('NEAR:', function () { }); describe('Verify transaction: ', () => { - it('should succeed to verify transaction in base64 encoding', async () => { - const txParams = { - }; + const amount = '1000000'; + const gas = '125000000000000'; - // TO-DO wait for verifyTransaction using explainTranasaction - const txPrebuild = { - txHex: rawTx.transfer.unsigned, - txInfo: { + it('should succeed to verify unsigned transaction in base64 encoding', async () => { - }, + const txPrebuild = newTxPrebuild(); + const txParams = newTxParams(); + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + isTransactionVerified.should.equal(true); + }); + + it('should succeed to verify signed transaction in base64 encoding', async () => { + + const txPrebuild = { + txHex: rawTx.transfer.signed, + txInfo: {}, }; + + const txParams = newTxParams(); const verification = {}; const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); isTransactionVerified.should.equal(true); }); - it('should succeed to verify transaction in hex encoding', async () => { + it('should fail verify transactions when have different recipients', async () => { + + const txPrebuild = newTxPrebuild(); + const txParams = { + recipients: [ + { + address: '9f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f0254', + amount: '1000000000000000000000000', + }, + { + address: '9f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f0254', + amount: '2000000000000000000000000', + }, + ], }; - // TO-DO wait for verifyTransaction using explainTranasaction - const txPrebuild = { - txHex: rawTx.transfer.hexUnsigned, - txInfo: { + const verification = {}; + await basecoin.verifyTransaction({ txParams, txPrebuild, verification }) + .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients'); + }); + + it('should fail verify transactions when total amount does not match with expected total amount field', async () => { + + const explainedTx = { + id: '5jTEPuDcMCeEgp1iyEbNBKsnhYz4F4c1EPDtRmxm3wCw', + displayOrder: [ + 'outputAmount', + 'changeAmount', + 'outputs', + 'changeOutputs', + 'fee', + 'type', + ], + outputAmount: '90000', + changeAmount: '0', + changeOutputs: [], + outputs: [ + { + address: '9f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f0254', + amount: '1000000000000000000000000', + }, + ], + fee: { + fee: '', }, + type: 0, }; + const stub = sinon.stub(accountLib.Near.Transaction.prototype, 'explainTransaction'); + const txPrebuild = newTxPrebuild(); + const txParams = newTxParams(); + const verification = {}; + stub.returns(explainedTx); + + await basecoin.verifyTransaction({ txParams, txPrebuild, verification }) + .should.be.rejectedWith('Tx total amount does not match with expected total amount field'); + stub.restore(); + }); + + it('should succeed to verify transaction in hex encoding', async () => { + + const txParams = newTxParams(); + const txPrebuild = newTxPrebuild(); const verification = {}; const isTransactionVerified = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); isTransactionVerified.should.equal(true); }); + + it('should convert serialized hex string to base64', async function () { + const txParams = newTxParams(); + const txPrebuild = newTxPrebuild(); + const verification = {}; + txPrebuild.txHex = Buffer.from(txPrebuild.txHex, 'base64').toString('hex'); + const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild, verification }); + validTransaction.should.equal(true); + }); + + it('should verify when input `recipients` is absent', async function () { + const txParams = newTxParams(); + txParams.recipients = undefined; + const txPrebuild = newTxPrebuild(); + const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild }); + validTransaction.should.equal(true); + }); + + it('should fail verify when txHex is invalid', async function () { + const txParams = newTxParams(); + txParams.recipients = undefined; + const txPrebuild = {}; + await basecoin.verifyTransaction({ txParams, txPrebuild }) + .should.rejectedWith('missing required tx prebuild property txHex'); + }); + + it('should succeed to verify transactions when recipients has extra data', async function () { + const txPrebuild = newTxPrebuild(); + const txParams = newTxParams(); + txParams.data = 'data'; + + const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild }); + validTransaction.should.equal(true); + }); + + it('should verify activate staking transaction', async function () { + const txBuilder = factory.getStakingActivateBuilder(); + txBuilder + .amount(amount) + .gas(gas) + .sender(accounts.account1.address, accounts.account1.publicKey) + .receiverId(validatorContractAddress) + .recentBlockHash(blockHash.block1) + .nonce(1); + txBuilder.sign({ key: accounts.account1.secretKey }); + const tx = await txBuilder.build(); + const txToBroadcastFormat = tx.toBroadcastFormat(); + const txPrebuild = { + txHex: txToBroadcastFormat, + }; + const txParams = { + recipients: [ + { + address: 'lavenderfive.pool.f863973.m0', + amount: '1000000', + }, + ], + }; + const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild }); + validTransaction.should.equal(true); + }); + + it('should verify deactivate staking transaction', async function () { + const txBuilder = factory.getStakingDeactivateBuilder(); + txBuilder + .amount(amount) + .gas(gas) + .sender(accounts.account1.address, accounts.account1.publicKey) + .receiverId(validatorContractAddress) + .recentBlockHash(blockHash.block1) + .nonce(1); + txBuilder.sign({ key: accounts.account1.secretKey }); + const tx = await txBuilder.build(); + const txToBroadcastFormat = tx.toBroadcastFormat(); + const txPrebuild = { + txHex: txToBroadcastFormat, + }; + const txParams = { + recipients: [], + }; + const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild }); + validTransaction.should.equal(true); + }); + + it('should verify withdraw staking transaction', async function () { + const txBuilder = factory.getStakingWithdrawBuilder(); + txBuilder + .amount(amount) + .gas(gas) + .sender(accounts.account1.address, accounts.account1.publicKey) + .receiverId(validatorContractAddress) + .recentBlockHash(blockHash.block1) + .nonce(1); + txBuilder.sign({ key: accounts.account1.secretKey }); + const tx = await txBuilder.build(); + const txToBroadcastFormat = tx.toBroadcastFormat(); + const txPrebuild = { + txHex: txToBroadcastFormat, + }; + const txParams = { + recipients: [ + { + address: '61b18c6dc02ddcabdeac56cb4f21a971cc41cc97640f6f85b073480008c53a0d', + amount: '1000000', + }, + ], + }; + const validTransaction = await basecoin.verifyTransaction({ txParams, txPrebuild }); + validTransaction.should.equal(true); + }); }); describe('Explain Transactions:', () => {