From b9bf137d59f370c7d5be820131442bc48fb92825 Mon Sep 17 00:00:00 2001 From: Deepak Jangid Date: Tue, 6 Feb 2024 23:18:08 +0530 Subject: [PATCH] feat(sdk-coin-zeta): zeta redelegate txn support with tests Ticket: WIN-1946 --- modules/abstract-cosmos/src/cosmosCoin.ts | 111 ++++++++++++++++++ .../src/lib/StakingRedelegateBuilder.ts | 31 +++++ modules/abstract-cosmos/src/lib/constants.ts | 1 + modules/abstract-cosmos/src/lib/iface.ts | 10 +- modules/abstract-cosmos/src/lib/index.ts | 1 + .../abstract-cosmos/src/lib/transaction.ts | 28 +++++ modules/abstract-cosmos/src/lib/utils.ts | 56 +++++++++ .../src/lib/transactionBuilderFactory.ts | 7 ++ modules/sdk-coin-zeta/test/resources/zeta.ts | 37 ++++++ .../sdk-coin-zeta/test/unit/transaction.ts | 34 ++++++ .../StakingRedelegateBuilder.ts | 58 +++++++++ modules/sdk-coin-zeta/test/unit/zeta.ts | 32 ++++- .../sdk-core/src/account-lib/baseCoin/enum.ts | 1 + 13 files changed, 402 insertions(+), 5 deletions(-) create mode 100644 modules/abstract-cosmos/src/lib/StakingRedelegateBuilder.ts create mode 100644 modules/sdk-coin-zeta/test/unit/transactionBuilder/StakingRedelegateBuilder.ts diff --git a/modules/abstract-cosmos/src/cosmosCoin.ts b/modules/abstract-cosmos/src/cosmosCoin.ts index 23396cac84..990b12a3b4 100644 --- a/modules/abstract-cosmos/src/cosmosCoin.ts +++ b/modules/abstract-cosmos/src/cosmosCoin.ts @@ -39,6 +39,7 @@ import { FeeData, GasAmountDetails, RecoveryOptions, + RedelegateMessage, SendMessage, } from './lib'; import { ROOT_PATH } from './lib/constants'; @@ -236,6 +237,116 @@ export class CosmosCoin extends BaseCoin { return { serializedTx: serializedTx }; } + /** + * Builds a redelegate transaction + * @param {RecoveryOptions} params parameters needed to construct and + * (maybe) sign the transaction + * + * @returns {CosmosLikeCoinRecoveryOutput} the serialized transaction hex string + */ + async redelegate( + params: RecoveryOptions & { + validatorSrcAddress: string; + validatorDstAddress: string; + amountToRedelegate: string; + } + ): Promise { + if (!params.bitgoKey) { + throw new Error('missing bitgoKey'); + } + + if (!params.validatorSrcAddress || !this.isValidAddress(params.validatorSrcAddress)) { + throw new Error('invalid validatorSrcAddress'); + } + + if (!params.validatorDstAddress || !this.isValidAddress(params.validatorDstAddress)) { + throw new Error('invalid validatorDstAddress'); + } + + if (!params.userKey) { + throw new Error('missing userKey'); + } + + if (!params.backupKey) { + throw new Error('missing backupKey'); + } + + if (!params.walletPassphrase) { + throw new Error('missing wallet passphrase'); + } + + if (!params.amountToRedelegate) { + throw new Error('missing amountToRedelegate'); + } + + const bitgoKey = params.bitgoKey.replace(/\s/g, ''); + + const MPC = new Ecdsa(); + const chainId = await this.getChainId(); + const publicKey = MPC.deriveUnhardened(bitgoKey, ROOT_PATH).slice(0, 66); + const senderAddress = this.getAddressFromPublicKey(publicKey); + + const [accountNumber, sequenceNo] = await this.getAccountDetails(senderAddress); + const gasBudget: FeeData = { + amount: [{ denom: this.getDenomination(), amount: this.getGasAmountDetails().gasAmount }], + gasLimit: this.getGasAmountDetails().gasLimit, + }; + + const amount: Coin = { + denom: this.getDenomination(), + amount: new BigNumber(params.amountToRedelegate).toFixed(), + }; + + const sendMessage: RedelegateMessage[] = [ + { + delegatorAddress: senderAddress, + validatorSrcAddress: params.validatorSrcAddress, + validatorDstAddress: params.validatorDstAddress, + amount: amount, + }, + ]; + + const txnBuilder = this.getBuilder().getStakingRedelegateBuilder(); + txnBuilder + .messages(sendMessage) + .gasBudget(gasBudget) + .publicKey(publicKey) + .sequence(Number(sequenceNo)) + .accountNumber(Number(accountNumber)) + .chainId(chainId); + + const unsignedTransaction = (await txnBuilder.build()) as CosmosTransaction; + let serializedTx = unsignedTransaction.toBroadcastFormat(); + const signableHex = unsignedTransaction.signablePayload.toString('hex'); + const userKey = params.userKey.replace(/\s/g, ''); + const backupKey = params.backupKey.replace(/\s/g, ''); + const [userKeyCombined, backupKeyCombined] = ((): [ + ECDSAMethodTypes.KeyCombined | undefined, + ECDSAMethodTypes.KeyCombined | undefined + ] => { + const [userKeyCombined, backupKeyCombined] = this.getKeyCombinedFromTssKeyShares( + userKey, + backupKey, + params.walletPassphrase + ); + return [userKeyCombined, backupKeyCombined]; + })(); + + if (!userKeyCombined || !backupKeyCombined) { + throw new Error('Missing combined key shares for user or backup'); + } + + const signature = await this.signRecoveryTSS(userKeyCombined, backupKeyCombined, signableHex); + const signableBuffer = Buffer.from(signableHex, 'hex'); + MPC.verify(signableBuffer, signature, this.getHashFunction()); + const cosmosKeyPair = this.getKeyPair(publicKey); + txnBuilder.addSignature({ pub: cosmosKeyPair.getKeys().pub }, Buffer.from(signature.r + signature.s, 'hex')); + const signedTransaction = await txnBuilder.build(); + serializedTx = signedTransaction.toBroadcastFormat(); + + return { serializedTx: serializedTx }; + } + private getKeyCombinedFromTssKeyShares( userPublicOrPrivateKeyShare: string, backupPrivateOrPublicKeyShare: string, diff --git a/modules/abstract-cosmos/src/lib/StakingRedelegateBuilder.ts b/modules/abstract-cosmos/src/lib/StakingRedelegateBuilder.ts new file mode 100644 index 0000000000..77df1621df --- /dev/null +++ b/modules/abstract-cosmos/src/lib/StakingRedelegateBuilder.ts @@ -0,0 +1,31 @@ +import { TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import * as constants from './constants'; +import { RedelegateMessage } from './iface'; +import { CosmosTransactionBuilder } from './transactionBuilder'; +import { CosmosUtils } from './utils'; + +export class StakingRedelegateBuilder extends CosmosTransactionBuilder { + protected _utils: CosmosUtils; + + constructor(_coinConfig: Readonly, utils: CosmosUtils) { + super(_coinConfig, utils); + this._utils = utils; + } + + protected get transactionType(): TransactionType { + return TransactionType.StakingRedelegate; + } + + /** @inheritdoc */ + messages(redelegateMessages: RedelegateMessage[]): this { + this._messages = redelegateMessages.map((redelegateMessage) => { + this._utils.validateRedelegateMessage(redelegateMessage); + return { + typeUrl: constants.redelegateTypeUrl, + value: redelegateMessage, + }; + }); + return this; + } +} diff --git a/modules/abstract-cosmos/src/lib/constants.ts b/modules/abstract-cosmos/src/lib/constants.ts index 29af2d6b63..ed8dd0f2c3 100644 --- a/modules/abstract-cosmos/src/lib/constants.ts +++ b/modules/abstract-cosmos/src/lib/constants.ts @@ -2,6 +2,7 @@ export const DEFAULT_SEED_SIZE_BYTES = 16; export const sendMsgTypeUrl = '/cosmos.bank.v1beta1.MsgSend'; export const delegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgDelegate'; export const undelegateMsgTypeUrl = '/cosmos.staking.v1beta1.MsgUndelegate'; +export const redelegateTypeUrl = '/cosmos.staking.v1beta1.MsgBeginRedelegate'; export const withdrawDelegatorRewardMsgTypeUrl = '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward'; export const executeContractMsgTypeUrl = '/cosmwasm.wasm.v1.MsgExecuteContract'; export const UNAVAILABLE_TEXT = 'UNAVAILABLE'; diff --git a/modules/abstract-cosmos/src/lib/iface.ts b/modules/abstract-cosmos/src/lib/iface.ts index f8b604e424..d1f300a335 100644 --- a/modules/abstract-cosmos/src/lib/iface.ts +++ b/modules/abstract-cosmos/src/lib/iface.ts @@ -46,6 +46,13 @@ export interface DelegateOrUndelegeteMessage { amount: Coin; } +export interface RedelegateMessage { + delegatorAddress: string; + validatorSrcAddress: string; + validatorDstAddress: string; + amount: Coin; +} + export interface WithdrawDelegatorRewardsMessage { delegatorAddress: string; validatorAddress: string; @@ -62,7 +69,8 @@ export type CosmosTransactionMessage = | SendMessage | DelegateOrUndelegeteMessage | WithdrawDelegatorRewardsMessage - | ExecuteContractMessage; + | ExecuteContractMessage + | RedelegateMessage; export interface MessageData { typeUrl: string; diff --git a/modules/abstract-cosmos/src/lib/index.ts b/modules/abstract-cosmos/src/lib/index.ts index 7557a85624..bf5f09a694 100644 --- a/modules/abstract-cosmos/src/lib/index.ts +++ b/modules/abstract-cosmos/src/lib/index.ts @@ -9,5 +9,6 @@ export { CosmosKeyPair } from './keyPair'; export { CosmosTransaction } from './transaction'; export { CosmosTransactionBuilder } from './transactionBuilder'; export { CosmosTransferBuilder } from './transferBuilder'; +export { StakingRedelegateBuilder } from './StakingRedelegateBuilder'; export { CosmosUtils } from './utils'; export { CosmosConstants }; diff --git a/modules/abstract-cosmos/src/lib/transaction.ts b/modules/abstract-cosmos/src/lib/transaction.ts index da0248a2d2..c643bd638f 100644 --- a/modules/abstract-cosmos/src/lib/transaction.ts +++ b/modules/abstract-cosmos/src/lib/transaction.ts @@ -16,6 +16,7 @@ import { CosmosLikeTransaction, DelegateOrUndelegeteMessage, ExecuteContractMessage, + RedelegateMessage, SendMessage, TransactionExplanation, TxData, @@ -232,6 +233,18 @@ export class CosmosTransaction extends BaseTransaction { }; }); break; + case TransactionType.StakingRedelegate: + explanationResult.type = TransactionType.StakingRedelegate; + outputAmount = BigInt(0); + outputs = json.sendMessages.map((message) => { + const redelegateMessage = message.value as RedelegateMessage; + outputAmount = outputAmount + BigInt(redelegateMessage.amount.amount); + return { + address: redelegateMessage.validatorDstAddress, + amount: redelegateMessage.amount.amount, + }; + }); + break; default: throw new InvalidTransactionError('Transaction type not supported'); } @@ -321,6 +334,21 @@ export class CosmosTransaction extends BaseTransaction { }); }); break; + case TransactionType.StakingRedelegate: + this.cosmosLikeTransaction.sendMessages.forEach((message) => { + const redelegateMessage = message.value as RedelegateMessage; + inputs.push({ + address: redelegateMessage.delegatorAddress, + value: redelegateMessage.amount.amount, + coin: this._coinConfig.name, + }); + outputs.push({ + address: redelegateMessage.validatorDstAddress, + value: redelegateMessage.amount.amount, + coin: this._coinConfig.name, + }); + }); + break; default: throw new InvalidTransactionError('Transaction type not supported'); } diff --git a/modules/abstract-cosmos/src/lib/utils.ts b/modules/abstract-cosmos/src/lib/utils.ts index cc10d44984..93b9494bef 100644 --- a/modules/abstract-cosmos/src/lib/utils.ts +++ b/modules/abstract-cosmos/src/lib/utils.ts @@ -31,6 +31,7 @@ import { ExecuteContractMessage, FeeData, MessageData, + RedelegateMessage, SendMessage, WithdrawDelegatorRewardsMessage, } from './iface'; @@ -231,6 +232,26 @@ export class CosmosUtils implements BaseUtils { }); } + /** + * Returns the array of MessageData[] from the decoded transaction + * @param {DecodedTxRaw} decodedTx + * @returns {MessageData[]} Redelegate transaction message data + */ + getRedelegateMessageDataFromDecodedTx(decodedTx: DecodedTxRaw): MessageData[] { + return decodedTx.body.messages.map((message) => { + const value = this.registry.decode(message); + return { + value: { + delegatorAddress: value.delegatorAddress, + validatorSrcAddress: value.validatorSrcAddress, + validatorDstAddress: value.validatorDstAddress, + amount: value.amount, + }, + typeUrl: message.typeUrl, + }; + }); + } + /** * Returns the array of MessageData[] from the decoded transaction * @param {DecodedTxRaw} decodedTx @@ -304,6 +325,8 @@ export class CosmosUtils implements BaseUtils { return TransactionType.StakingWithdraw; case constants.executeContractMsgTypeUrl: return TransactionType.ContractCall; + case constants.redelegateTypeUrl: + return TransactionType.StakingRedelegate; default: return undefined; } @@ -495,6 +518,32 @@ export class CosmosUtils implements BaseUtils { this.validateAmount(delegateMessage.amount); } + /** + * Validates the RedelegateMessage + * @param {DelegateOrUndelegeteMessage} redelegateMessage - The RedelegateMessage to validate. + * @throws {InvalidTransactionError} Throws an error if the validatorSrcAddress, validatorDstAddress, delegatorAddress, or amount is invalid or missing. + */ + validateRedelegateMessage(redelegateMessage: RedelegateMessage) { + this.isObjPropertyNull(redelegateMessage, ['validatorSrcAddress', 'validatorDstAddress', 'delegatorAddress']); + + if (!this.isValidValidatorAddress(redelegateMessage.validatorSrcAddress)) { + throw new InvalidTransactionError( + `Invalid RedelegateMessage validatorSrcAddress: ` + redelegateMessage.validatorSrcAddress + ); + } + if (!this.isValidValidatorAddress(redelegateMessage.validatorDstAddress)) { + throw new InvalidTransactionError( + `Invalid RedelegateMessage validatorDstAddress: ` + redelegateMessage.validatorDstAddress + ); + } + if (!this.isValidAddress(redelegateMessage.delegatorAddress)) { + throw new InvalidTransactionError( + `Invalid DelegateOrUndelegeteMessage delegatorAddress: ` + redelegateMessage.delegatorAddress + ); + } + this.validateAmount(redelegateMessage.amount); + } + /** * Validates the MessageData * @param {MessageData} messageData - The MessageData to validate. @@ -531,6 +580,11 @@ export class CosmosUtils implements BaseUtils { this.validateExecuteContractMessage(value, TransactionType.ContractCall); break; } + case TransactionType.StakingRedelegate: { + const value = messageData.value as RedelegateMessage; + this.validateRedelegateMessage(value); + break; + } default: throw new InvalidTransactionError(`Invalid MessageData TypeUrl is not supported: ` + messageData.typeUrl); } @@ -635,6 +689,8 @@ export class CosmosUtils implements BaseUtils { sendMessageData = this.getWithdrawRewardsMessageDataFromDecodedTx(decodedTx); } else if (type === TransactionType.ContractCall) { sendMessageData = this.getExecuteContractMessageDataFromDecodedTx(decodedTx); + } else if (type === TransactionType.StakingRedelegate) { + sendMessageData = this.getRedelegateMessageDataFromDecodedTx(decodedTx); } else { throw new Error('Transaction type not supported: ' + typeUrl); } diff --git a/modules/sdk-coin-zeta/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-zeta/src/lib/transactionBuilderFactory.ts index 385d73deec..e1fc784c8a 100644 --- a/modules/sdk-coin-zeta/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-zeta/src/lib/transactionBuilderFactory.ts @@ -6,6 +6,7 @@ import { StakingDeactivateBuilder, StakingWithdrawRewardsBuilder, ContractCallBuilder, + StakingRedelegateBuilder, } from '@bitgo/abstract-cosmos'; import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; @@ -32,6 +33,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.getStakingWithdrawRewardsBuilder(tx); case TransactionType.ContractCall: return this.getContractCallBuilder(tx); + case TransactionType.StakingRedelegate: + return this.getStakingRedelegateBuilder(tx); default: throw new InvalidTransactionError('Invalid transaction'); } @@ -64,6 +67,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new ContractCallBuilder(this._coinConfig, zetaUtils)); } + getStakingRedelegateBuilder(tx?: CosmosTransaction): StakingRedelegateBuilder { + return this.initializeBuilder(tx, new StakingRedelegateBuilder(this._coinConfig, zetaUtils)); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-zeta/test/resources/zeta.ts b/modules/sdk-coin-zeta/test/resources/zeta.ts index 716ba50996..1bce2ba559 100644 --- a/modules/sdk-coin-zeta/test/resources/zeta.ts +++ b/modules/sdk-coin-zeta/test/resources/zeta.ts @@ -82,6 +82,43 @@ export const TEST_DELEGATE_TX = { }, }; +export const TEST_REDELEGATE_TX = { + hash: 'B88877498F54E8A87E929DBC809321A0B0E0AF53BE63906B50C19093C2C4BF7A', + signature: 'RUSFcsfx1dMpmSlKxuUgX4qPtlpjFXbvToJe+JVAJHsludowFS6yXHwpcs38SUKlIRlxx3zS8GoNZwsloTN0QA==', + pubKey: 'AzPwxMzrPm6MhzKSAGkvoVlNvR+deyCuBLnxYxm8xQcH', + privateKey: 'w7gsHxeGjJBwHNRE8fUwSJHYPrimZqngjc8hNR9vTLQ=', + signedTxBase64: + 'CtgBCtUBCiovY29zbW9zLnN0YWtpbmcudjFiZXRhMS5Nc2dCZWdpblJlZGVsZWdhdGUSpgEKK3pldGExZGhzazV2NTNoM3h3ZzQycGRnM3Iwdzd6bDgzeXhneWg1NmxzbmYSMnpldGF2YWxvcGVyMWRoc2s1djUzaDN4d2c0MnBkZzNyMHc3emw4M3l4Z3loczY4djdsGjJ6ZXRhdmFsb3BlcjE5djA3d3Z3bTN6dXg5cGF3Y21jY3I3YzRoZmV6YWgwcjh3aHNjNiIPCgVhemV0YRIGMTk1OTk2EnUKUApGCh8vY29zbW9zLmNyeXB0by5zZWNwMjU2azEuUHViS2V5EiMKIQMz8MTM6z5ujIcykgBpL6FZTb0fnXsgrgS58WMZvMUHBxIECgIIARgSEiEKGwoFYXpldGESEjE1MzczMDAwMDAwMDAwMDAwMBCE4hIaQEVEhXLH8dXTKZkpSsblIF+Kj7ZaYxV2706CXviVQCR7JbnaMBUuslx8KXLN/ElCpSEZccd80vBqDWcLJaEzdEA=', + delegator: 'zeta1dhsk5v53h3xwg42pdg3r0w7zl83yxgyh56lsnf', + validator: 'zetavaloper19v07wvwm3zux9pawcmccr7c4hfezah0r8whsc6', + chainId: 'athens_7001-1', + accountNumber: 258033, + sequence: 18, + sendAmount: '10000', + feeAmount: '100000000000000', + sendMessage: { + typeUrl: '/cosmos.staking.v1beta1.MsgBeginRedelegate', + value: { + delegatorAddress: 'zeta1dhsk5v53h3xwg42pdg3r0w7zl83yxgyh56lsnf', + validatorSrcAddress: 'zetavaloper1dhsk5v53h3xwg42pdg3r0w7zl83yxgyhs68v7l', + validatorDstAddress: 'zetavaloper19v07wvwm3zux9pawcmccr7c4hfezah0r8whsc6', + amount: { + denom: 'azeta', + amount: '195996', + }, + }, + }, + gasBudget: { + amount: [ + { + denom: 'azeta', + amount: '153730000000000000', + }, + ], + gasLimit: 307460, + }, +}; + export const TEST_UNDELEGATE_TX = { hash: 'A7FCEA1B657CD3CA0BF7E03CB6B3279D1B737CBE1B8449E920A23453DACE2B1A', signature: 'K69sqHfrCe32lsh18jRZ1ZRlr5E5XqdUS1zI3AIfQrRCiPrp/cTVbkyp1HGVr+geoE04CLRDuT7iLHjAwQNlwg==', diff --git a/modules/sdk-coin-zeta/test/unit/transaction.ts b/modules/sdk-coin-zeta/test/unit/transaction.ts index cfca3f6263..7128381d38 100644 --- a/modules/sdk-coin-zeta/test/unit/transaction.ts +++ b/modules/sdk-coin-zeta/test/unit/transaction.ts @@ -6,6 +6,7 @@ import should from 'should'; import { CosmosTransaction, DelegateOrUndelegeteMessage, + RedelegateMessage, SendMessage, WithdrawDelegatorRewardsMessage, } from '@bitgo/abstract-cosmos'; @@ -159,6 +160,39 @@ describe('Zeta Transaction', () => { ]); }); + it('should build a redelegate txn from raw signed base64', function () { + tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_REDELEGATE_TX.signedTxBase64); + const json = tx.toJson(); + should.equal(json.sequence, testData.TEST_REDELEGATE_TX.sequence); + should.deepEqual(json.gasBudget, testData.TEST_REDELEGATE_TX.gasBudget); + should.equal(Buffer.from(json.publicKey as any, 'hex').toString('base64'), testData.TEST_REDELEGATE_TX.pubKey); + should.equal( + (json.sendMessages[0].value as RedelegateMessage).validatorSrcAddress, + testData.TEST_REDELEGATE_TX.sendMessage.value.validatorSrcAddress + ); + should.deepEqual( + (json.sendMessages[0].value as DelegateOrUndelegeteMessage).amount, + testData.TEST_REDELEGATE_TX.sendMessage.value.amount + ); + should.equal(Buffer.from(json.signature as any).toString('base64'), testData.TEST_REDELEGATE_TX.signature); + should.equal(tx.type, TransactionType.StakingRedelegate); + tx.loadInputsAndOutputs(); + should.deepEqual(tx.inputs, [ + { + address: testData.TEST_REDELEGATE_TX.delegator, + value: testData.TEST_REDELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tzeta', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.TEST_REDELEGATE_TX.validator, + value: testData.TEST_REDELEGATE_TX.sendMessage.value.amount.amount, + coin: 'tzeta', + }, + ]); + }); + it('should build a withdraw rewards from raw signed base64', function () { tx.enrichTransactionDetailsFromRawTransaction(testData.TEST_WITHDRAW_REWARDS_TX.signedTxBase64); const json = tx.toJson(); diff --git a/modules/sdk-coin-zeta/test/unit/transactionBuilder/StakingRedelegateBuilder.ts b/modules/sdk-coin-zeta/test/unit/transactionBuilder/StakingRedelegateBuilder.ts new file mode 100644 index 0000000000..1a138fd334 --- /dev/null +++ b/modules/sdk-coin-zeta/test/unit/transactionBuilder/StakingRedelegateBuilder.ts @@ -0,0 +1,58 @@ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { toHex, TransactionType } from '@bitgo/sdk-core'; +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { fromBase64 } from '@cosmjs/encoding'; +import should from 'should'; +import { Tzeta, Zeta } from '../../../src'; +import * as testData from '../../resources/zeta'; + +describe('Zeta Redelegate txn Builder', () => { + let bitgo: TestBitGoAPI; + let basecoin; + let factory; + let testTx; + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' }); + bitgo.safeRegister('zeta', Zeta.createInstance); + bitgo.safeRegister('tzeta', Tzeta.createInstance); + bitgo.initializeTestVars(); + basecoin = bitgo.coin('tzeta'); + factory = basecoin.getBuilder(); + testTx = testData.TEST_REDELEGATE_TX; + }); + + // https://explorer.zetachain.com/cosmos/tx/B88877498F54E8A87E929DBC809321A0B0E0AF53BE63906B50C19093C2C4BF7A + it('should build a Redelegate tx with signature', async function () { + const txBuilder = factory.getStakingRedelegateBuilder(); + txBuilder.sequence(testTx.sequence); + txBuilder.gasBudget(testTx.gasBudget); + txBuilder.messages([testTx.sendMessage.value]); + txBuilder.memo(''); + txBuilder.publicKey(toHex(fromBase64(testTx.pubKey))); + txBuilder.addSignature({ pub: toHex(fromBase64(testTx.pubKey)) }, Buffer.from(testTx.signature, 'base64')); + + const tx = await txBuilder.build(); + const json = await (await txBuilder.build()).toJson(); + should.equal(tx.type, TransactionType.StakingRedelegate); + should.deepEqual(json.gasBudget, testTx.gasBudget); + should.deepEqual(json.sendMessages, [testTx.sendMessage]); + should.deepEqual(json.publicKey, toHex(fromBase64(testTx.pubKey))); + should.deepEqual(json.sequence, testTx.sequence); + const rawTx = tx.toBroadcastFormat(); + should.equal(rawTx, testTx.signedTxBase64); + should.deepEqual(tx.inputs, [ + { + address: testTx.delegator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testTx.validator, + value: testTx.sendMessage.value.amount.amount, + coin: basecoin.getChain(), + }, + ]); + }); +}); diff --git a/modules/sdk-coin-zeta/test/unit/zeta.ts b/modules/sdk-coin-zeta/test/unit/zeta.ts index e3334270bf..291f2e229e 100644 --- a/modules/sdk-coin-zeta/test/unit/zeta.ts +++ b/modules/sdk-coin-zeta/test/unit/zeta.ts @@ -1,7 +1,7 @@ -import { CosmosTransaction, SendMessage } from '@bitgo/abstract-cosmos'; +import { CosmosTransaction, RedelegateMessage, SendMessage } from '@bitgo/abstract-cosmos'; import { BitGoAPI } from '@bitgo/sdk-api'; import { EcdsaRangeProof, EcdsaTypes } from '@bitgo/sdk-lib-mpc'; -import { mockSerializedChallengeWithProofs, TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { TestBitGo, TestBitGoAPI, mockSerializedChallengeWithProofs } from '@bitgo/sdk-test'; import { coins } from '@bitgo/statics'; import BigNumber from 'bignumber.js'; import { beforeEach } from 'mocha'; @@ -10,13 +10,13 @@ import { Tzeta, Zeta } from '../../src'; import { GAS_AMOUNT } from '../../src/lib/constants'; import utils from '../../src/lib/utils'; import { - address, - mockAccountDetailsResponse, TEST_DELEGATE_TX, TEST_SEND_TX, TEST_TX_WITH_MEMO, TEST_UNDELEGATE_TX, TEST_WITHDRAW_REWARDS_TX, + address, + mockAccountDetailsResponse, wrwUser, } from '../resources/zeta'; import should = require('should'); @@ -398,6 +398,30 @@ describe('Zeta', function () { should.equal(sendMessage.toAddress, destinationAddress); should.equal(sendMessage.amount[0].amount, actualBalance.toFixed()); }); + + it('should redelegate funds to new validator', async function () { + const res = await basecoin.redelegate({ + userKey: wrwUser.userPrivateKey, + backupKey: wrwUser.backupPrivateKey, + bitgoKey: wrwUser.bitgoPublicKey, + walletPassphrase: wrwUser.walletPassphrase, + amountToRedelegate: '10000000000000000', + validatorSrcAddress: 'zetavaloper1dhsk5v53h3xwg42pdg3r0w7zl83yxgyhs68v7l', + validatorDstAddress: 'zetavaloper19v07wvwm3zux9pawcmccr7c4hfezah0r8whsc6', + }); + + res.should.not.be.empty(); + res.should.hasOwnProperty('serializedTx'); + sandBox.assert.calledOnce(basecoin.getChainId); + + const txn = new CosmosTransaction(coin, utils); + txn.enrichTransactionDetailsFromRawTransaction(res.serializedTx); + const txnJson = txn.toJson(); + const redelegateMessage = txnJson.sendMessages[0].value as RedelegateMessage; + should.equal(redelegateMessage.validatorSrcAddress, 'zetavaloper1dhsk5v53h3xwg42pdg3r0w7zl83yxgyhs68v7l'); + should.equal(redelegateMessage.validatorDstAddress, 'zetavaloper19v07wvwm3zux9pawcmccr7c4hfezah0r8whsc6'); + should.equal(redelegateMessage.amount.amount, '10000000000000000'); + }); }); describe('Recover transaction: failure path', () => { diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index 4e41188ca0..256297e49d 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -62,6 +62,7 @@ export enum TransactionType { StakingDelegate, // Custom transaction (e.g. SUI) CustomTx, + StakingRedelegate, } /**