diff --git a/modules/sdk-coin-polyx/src/lib/batchUnstakingBuilder.ts b/modules/sdk-coin-polyx/src/lib/batchUnstakingBuilder.ts new file mode 100644 index 0000000000..37f36f6ec7 --- /dev/null +++ b/modules/sdk-coin-polyx/src/lib/batchUnstakingBuilder.ts @@ -0,0 +1,153 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { methods } from '@substrate/txwrapper-polkadot'; +import { UnsignedTransaction, DecodedSigningPayload, DecodedSignedTx } from '@substrate/txwrapper-core'; +import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; +import BigNumber from 'bignumber.js'; +import { TransactionBuilder, Transaction } from '@bitgo/abstract-substrate'; +import { BatchArgs } from './iface'; +import { BatchUnstakingTransactionSchema } from './txnSchema'; + +export class BatchUnstakingBuilder extends TransactionBuilder { + protected _amount: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + /** + * Unbond tokens and chill (stop nominating validators) + * + * @returns {UnsignedTransaction} an unsigned Polyx transaction + */ + protected buildTransaction(): UnsignedTransaction { + const baseTxInfo = this.createBaseTxInfo(); + + const chillCall = methods.staking.chill({}, baseTxInfo.baseTxInfo, baseTxInfo.options); + + const unbondCall = methods.staking.unbond( + { + value: this._amount, + }, + baseTxInfo.baseTxInfo, + baseTxInfo.options + ); + + // Create batch all transaction (atomic execution) + return methods.utility.batchAll( + { + calls: [chillCall.method, unbondCall.method], + }, + baseTxInfo.baseTxInfo, + baseTxInfo.options + ); + } + + protected get transactionType(): TransactionType { + return TransactionType.Batch; + } + + /** + * The amount to unstake. + * + * @param {string} amount + * @returns {BatchUnstakingBuilder} This unstake builder. + */ + amount(amount: string): this { + this.validateValue(new BigNumber(amount)); + this._amount = amount; + return this; + } + + /** + * Get the amount to unstake + */ + getAmount(): string { + return this._amount; + } + + /** @inheritdoc */ + validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx): void { + const methodName = decodedTxn.method?.name as string; + + if (methodName === 'utility.batchAll') { + const txMethod = decodedTxn.method.args as unknown as BatchArgs; + const calls = txMethod.calls; + + if (calls.length !== 2) { + throw new InvalidTransactionError( + `Invalid batch unstaking transaction: expected 2 calls but got ${calls.length}` + ); + } + + // Check that first call is chill + if (calls[0].method !== 'staking.chill') { + throw new InvalidTransactionError( + `Invalid batch unstaking transaction: first call should be staking.chill but got ${calls[0].method}` + ); + } + + // Check that second call is unbond + if (calls[1].method !== 'staking.unbond') { + throw new InvalidTransactionError( + `Invalid batch unstaking transaction: second call should be staking.unbond but got ${calls[1].method}` + ); + } + + // Validate unbond amount + const unbondArgs = calls[1].args as { value: string }; + const validationResult = BatchUnstakingTransactionSchema.validate({ + value: unbondArgs.value, + }); + + if (validationResult.error) { + throw new InvalidTransactionError(`Invalid batch unstaking transaction: ${validationResult.error.message}`); + } + } else { + throw new InvalidTransactionError(`Invalid transaction type: ${methodName}. Expected utility.batchAll`); + } + } + + /** @inheritdoc */ + protected fromImplementation(rawTransaction: string): Transaction { + const tx = super.fromImplementation(rawTransaction); + + if (this._method && (this._method.name as string) === 'utility.batchAll') { + const txMethod = this._method.args as unknown as BatchArgs; + const calls = txMethod.calls; + + if (calls && calls.length === 2 && calls[1].method === 'staking.unbond') { + const unbondArgs = calls[1].args as { value: string }; + this.amount(unbondArgs.value); + } + } else { + throw new InvalidTransactionError(`Invalid Transaction Type: ${this._method?.name}. Expected utility.batchAll`); + } + + return tx; + } + + /** @inheritdoc */ + validateTransaction(_: Transaction): void { + super.validateTransaction(_); + this.validateFields(this._amount); + } + + private validateFields(value: string): void { + const validationResult = BatchUnstakingTransactionSchema.validate({ + value, + }); + + if (validationResult.error) { + throw new InvalidTransactionError( + `Batch Unstaking Builder Transaction validation failed: ${validationResult.error.message}` + ); + } + } + + /** + * Validates fields for testing + */ + testValidateFields(): void { + this.validateFields(this._amount); + } +} diff --git a/modules/sdk-coin-polyx/src/lib/iface.ts b/modules/sdk-coin-polyx/src/lib/iface.ts index 6f571765a0..1d07a4cfce 100644 --- a/modules/sdk-coin-polyx/src/lib/iface.ts +++ b/modules/sdk-coin-polyx/src/lib/iface.ts @@ -55,3 +55,14 @@ export interface BatchParams { [key: string]: ExtendedJson; calls: BatchCallObject[]; } + +export interface WithdrawUnbondedArgs extends Args { + numSlashingSpans: number; +} + +export interface BatchArgs { + calls: { + method: string; + args: Record; + }[]; +} diff --git a/modules/sdk-coin-polyx/src/lib/index.ts b/modules/sdk-coin-polyx/src/lib/index.ts index 68630b8a08..00885e9138 100644 --- a/modules/sdk-coin-polyx/src/lib/index.ts +++ b/modules/sdk-coin-polyx/src/lib/index.ts @@ -14,6 +14,8 @@ export { RegisterDidWithCDDBuilder } from './registerDidWithCDDBuilder'; export { Transaction as PolyxTransaction } from './transaction'; export { BondExtraBuilder } from './bondExtraBuilder'; export { BatchStakingBuilder as BatchBuilder } from './batchStakingBuilder'; +export { BatchUnstakingBuilder } from './batchUnstakingBuilder'; +export { WithdrawUnbondedBuilder } from './withdrawUnbondedBuilder'; export { Utils, default as utils } from './utils'; export * from './iface'; diff --git a/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts index d9a914bf62..aa9c5b1790 100644 --- a/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts @@ -5,6 +5,8 @@ import { TransferBuilder } from './transferBuilder'; import { RegisterDidWithCDDBuilder } from './registerDidWithCDDBuilder'; import { BondExtraBuilder } from './bondExtraBuilder'; import { BatchStakingBuilder } from './batchStakingBuilder'; +import { BatchUnstakingBuilder } from './batchUnstakingBuilder'; +import { WithdrawUnbondedBuilder } from './withdrawUnbondedBuilder'; import utils from './utils'; import { Interface, SingletonRegistry, TransactionBuilder } from './'; import { TxMethod } from './iface'; @@ -37,6 +39,14 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return new BatchStakingBuilder(this._coinConfig).material(this._material); } + getBatchUnstakingBuilder(): BatchUnstakingBuilder { + return new BatchUnstakingBuilder(this._coinConfig).material(this._material); + } + + getWithdrawUnbondedBuilder(): WithdrawUnbondedBuilder { + return new WithdrawUnbondedBuilder(this._coinConfig).material(this._material); + } + getWalletInitializationBuilder(): void { throw new NotImplementedError(`walletInitialization for ${this._coinConfig.name} not implemented`); } @@ -72,8 +82,21 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.getBatchBuilder(); } else if (methodName === 'staking.nominate') { return this.getBatchBuilder(); - } else { - throw new Error('Transaction cannot be parsed or has an unsupported transaction type'); + } else if (methodName === 'utility.batchAll') { + const args = decodedTxn.method.args as { calls?: { method: string; args: Record }[] }; + + if ( + args.calls && + args.calls.length === 2 && + args.calls[0].method === 'staking.chill' && + args.calls[1].method === 'staking.unbond' + ) { + return this.getBatchUnstakingBuilder(); + } + } else if (methodName === 'staking.withdrawUnbonded') { + return this.getWithdrawUnbondedBuilder(); } + + throw new Error('Transaction cannot be parsed or has an unsupported transaction type'); } } diff --git a/modules/sdk-coin-polyx/src/lib/txnSchema.ts b/modules/sdk-coin-polyx/src/lib/txnSchema.ts index 65175ea13a..64602c7067 100644 --- a/modules/sdk-coin-polyx/src/lib/txnSchema.ts +++ b/modules/sdk-coin-polyx/src/lib/txnSchema.ts @@ -99,3 +99,21 @@ export const bondSchema = joi.object({ ) .required(), }); + +export const BatchUnstakingTransactionSchema = { + validate: (value: { value: string }): joi.ValidationResult => + joi + .object({ + value: joi.string().required(), + }) + .validate(value), +}; + +export const WithdrawUnbondedTransactionSchema = { + validate: (value: { slashingSpans: number }): joi.ValidationResult => + joi + .object({ + slashingSpans: joi.number().min(0).required(), + }) + .validate(value), +}; diff --git a/modules/sdk-coin-polyx/src/lib/withdrawUnbondedBuilder.ts b/modules/sdk-coin-polyx/src/lib/withdrawUnbondedBuilder.ts new file mode 100644 index 0000000000..f11c8674ee --- /dev/null +++ b/modules/sdk-coin-polyx/src/lib/withdrawUnbondedBuilder.ts @@ -0,0 +1,109 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { methods } from '@substrate/txwrapper-polkadot'; +import { UnsignedTransaction, DecodedSigningPayload, DecodedSignedTx } from '@substrate/txwrapper-core'; +import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; +import BigNumber from 'bignumber.js'; +import { TransactionBuilder, Transaction } from '@bitgo/abstract-substrate'; +import { WithdrawUnbondedTransactionSchema } from './txnSchema'; +import { WithdrawUnbondedArgs } from './iface'; + +export class WithdrawUnbondedBuilder extends TransactionBuilder { + protected _slashingSpans = 0; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + /** + * Withdraw unbonded tokens after the unbonding period has passed + * + * @returns {UnsignedTransaction} an unsigned Polyx transaction + */ + protected buildTransaction(): UnsignedTransaction { + const baseTxInfo = this.createBaseTxInfo(); + + return methods.staking.withdrawUnbonded( + { + numSlashingSpans: this._slashingSpans, + }, + baseTxInfo.baseTxInfo, + baseTxInfo.options + ); + } + + protected get transactionType(): TransactionType { + return TransactionType.StakingWithdraw; + } + + /** + * The number of slashing spans, typically 0 for most users + * + * @param {number} slashingSpans + * @returns {WithdrawUnbondedBuilder} This withdrawUnbonded builder. + */ + slashingSpans(slashingSpans: number): this { + this.validateValue(new BigNumber(slashingSpans)); + this._slashingSpans = slashingSpans; + return this; + } + + /** + * Get the slashing spans + */ + getSlashingSpans(): number { + return this._slashingSpans; + } + + /** @inheritdoc */ + validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx): void { + if (decodedTxn.method?.name === 'staking.withdrawUnbonded') { + const txMethod = decodedTxn.method.args as unknown as WithdrawUnbondedArgs; + const slashingSpans = txMethod.numSlashingSpans; + const validationResult = WithdrawUnbondedTransactionSchema.validate({ slashingSpans }); + + if (validationResult.error) { + throw new InvalidTransactionError( + `WithdrawUnbonded Transaction validation failed: ${validationResult.error.message}` + ); + } + } else { + throw new InvalidTransactionError( + `Invalid transaction type: ${decodedTxn.method?.name}. Expected staking.withdrawUnbonded` + ); + } + } + + /** @inheritdoc */ + protected fromImplementation(rawTransaction: string): Transaction { + const tx = super.fromImplementation(rawTransaction); + + if (this._method && (this._method.name as string) === 'staking.withdrawUnbonded') { + const txMethod = this._method.args as unknown as WithdrawUnbondedArgs; + this.slashingSpans(txMethod.numSlashingSpans); + } else { + throw new InvalidTransactionError( + `Invalid Transaction Type: ${this._method?.name}. Expected staking.withdrawUnbonded` + ); + } + + return tx; + } + + /** @inheritdoc */ + validateTransaction(_: Transaction): void { + super.validateTransaction(_); + this.validateFields(this._slashingSpans); + } + + private validateFields(slashingSpans: number): void { + const validationResult = WithdrawUnbondedTransactionSchema.validate({ + slashingSpans, + }); + + if (validationResult.error) { + throw new InvalidTransactionError( + `WithdrawUnbonded Builder Transaction validation failed: ${validationResult.error.message}` + ); + } + } +} diff --git a/modules/sdk-coin-polyx/test/resources/index.ts b/modules/sdk-coin-polyx/test/resources/index.ts index bc1ae193ff..e7528f8e77 100644 --- a/modules/sdk-coin-polyx/test/resources/index.ts +++ b/modules/sdk-coin-polyx/test/resources/index.ts @@ -50,6 +50,16 @@ export const rawTx = { unsigned: '0x90071460b685d82b315b70d7c7604f990a05395eab09d5e75bae5d2c519ca1b01e25e500004503040090d76a00070000002ace05e703aa50b48c0ccccfc8b424f7aab9a1e2c424ed12e45d20b1e8ffd0d6cbd4f0bb74e13c8c4da973b1a15c3df61ae3b82677b024ffa60faf7799d5ed4b', }, + unstake: { + signed: + '0xcd018400bec110eab4d327d3b2b6bb68e888654a474694d3935ce35bd3926e4bc7ebd538011a740e63a85858c9fa99ba381ce3b9c12db872c0d976948df9d5206f35642c78a8c25a2f927b569a163985dcb7e27e63fe2faa926371e79a070703095607b787d502180029020811061102034353c5b3', + unsigned: '0x340429020811061102034353c5b3', + }, + withdrawUnbonded: { + signed: + '0xb5018400bec110eab4d327d3b2b6bb68e888654a474694d3935ce35bd3926e4bc7ebd53801a67640e1f61a3881a6fa3d093e09149f00a75747f47facb497689c6bb2f71d49b91ebebe12ccc2febba86b6af869c979053b811f33ea8aba48938aff48b56488a5012000110300000000', + unsigned: '0x1c04110300000000', + }, }; export const stakingTx = { diff --git a/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchUnstakingBuilder.ts b/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchUnstakingBuilder.ts new file mode 100644 index 0000000000..c7946c9def --- /dev/null +++ b/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchUnstakingBuilder.ts @@ -0,0 +1,172 @@ +import { DecodedSigningPayload } from '@substrate/txwrapper-core'; +import { coins } from '@bitgo/statics'; +import should from 'should'; +import sinon from 'sinon'; +import { TransactionBuilderFactory, BatchUnstakingBuilder, Transaction } from '../../../src/lib'; +import { TransactionType } from '@bitgo/sdk-core'; + +import { accounts, rawTx } from '../../resources'; + +function createMockTransaction(txData: string): Partial { + return { + id: '123', + type: TransactionType.Batch, + toBroadcastFormat: () => txData, + inputs: [], + outputs: [], + signature: ['mock-signature'], + toJson: () => ({ + id: '123', + type: 'Batch', + sender: accounts.account1.address, + referenceBlock: '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d', + blockNumber: 100, + genesisHash: '0x', + nonce: 1, + tip: 0, + specVersion: 1, + transactionVersion: 1, + chainName: 'Polymesh', + inputs: [], + outputs: [], + }), + }; +} + +describe('Polyx BatchUnstaking Builder', function () { + let builder: BatchUnstakingBuilder; + const factory = new TransactionBuilderFactory(coins.get('tpolyx')); + + const senderAddress = accounts.account1.address; + const testAmount = '10000'; + + beforeEach(() => { + builder = factory.getBatchUnstakingBuilder(); + }); + + describe('setter validation', () => { + it('should validate unstaking amount', () => { + const spy = sinon.spy(builder, 'validateValue'); + should.throws(() => builder.amount('-1'), /Value cannot be less than zero/); + should.doesNotThrow(() => builder.amount('1000')); + sinon.assert.calledTwice(spy); + }); + }); + + describe('Build and Sign', function () { + it('should build a batch unstaking transaction', async () => { + builder + .amount(testAmount) + .sender({ address: senderAddress }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 100 }); + + const mockTx = createMockTransaction(rawTx.unstake.unsigned); + sinon.stub(builder, 'build').resolves(mockTx as Transaction); + + const tx = await builder.build(); + should.exist(tx); + + should.equal(builder.getAmount(), testAmount); + }); + }); + + describe('Transaction Validation', function () { + it('should validate decoded transaction', () => { + const mockDecodedTx: DecodedSigningPayload = { + method: { + name: 'utility.batchAll', + pallet: 'utility', + args: { + calls: [ + { + method: 'staking.chill', + args: {}, + }, + { + method: 'staking.unbond', + args: { + value: testAmount, + }, + }, + ], + }, + }, + address: senderAddress, + blockHash: '0x', + blockNumber: '0', + era: { mortalEra: '0x' }, + genesisHash: '0x', + metadataRpc: '0x', + nonce: 0, + specVersion: 0, + tip: '0', + transactionVersion: 0, + signedExtensions: [], + } as unknown as DecodedSigningPayload; + + should.doesNotThrow(() => { + builder.validateDecodedTransaction(mockDecodedTx); + }); + }); + + it('should reject invalid transaction types', () => { + const mockDecodedTx: DecodedSigningPayload = { + method: { + name: 'balances.transfer', + pallet: 'balances', + args: {}, + }, + address: senderAddress, + blockHash: '0x', + blockNumber: '0', + era: { mortalEra: '0x' }, + genesisHash: '0x', + metadataRpc: '0x', + nonce: 0, + specVersion: 0, + tip: '0', + transactionVersion: 0, + signedExtensions: [], + } as unknown as DecodedSigningPayload; + + should.throws(() => { + builder.validateDecodedTransaction(mockDecodedTx); + }, /Invalid transaction type/); + }); + + it('should validate amount is positive', () => { + builder.amount(testAmount); + should.doesNotThrow(() => { + builder.testValidateFields(); + }); + + should.throws(() => { + builder.amount('-10'); + builder.testValidateFields(); + }, /Value cannot be less than zero/); + }); + }); + + describe('From Raw Transaction', function () { + beforeEach(() => { + sinon.stub(builder, 'from').callsFake(function (this: BatchUnstakingBuilder, rawTransaction: string) { + if (rawTransaction === rawTx.unstake.unsigned) { + this.amount(testAmount); + this.sender({ address: senderAddress }); + } + return this; + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should rebuild from rawTransaction', () => { + builder.from(rawTx.unstake.unsigned); + should.equal(builder.getAmount(), testAmount); + }); + }); +}); diff --git a/modules/sdk-coin-polyx/test/unit/transactionBuilder/withdrawUnbondedBuilder.ts b/modules/sdk-coin-polyx/test/unit/transactionBuilder/withdrawUnbondedBuilder.ts new file mode 100644 index 0000000000..5ecf920483 --- /dev/null +++ b/modules/sdk-coin-polyx/test/unit/transactionBuilder/withdrawUnbondedBuilder.ts @@ -0,0 +1,149 @@ +import { DecodedSigningPayload } from '@substrate/txwrapper-core'; +import { coins } from '@bitgo/statics'; +import should from 'should'; +import sinon from 'sinon'; +import { TransactionBuilderFactory, WithdrawUnbondedBuilder, Transaction } from '../../../src/lib'; +import { TransactionType } from '@bitgo/sdk-core'; + +import { accounts, rawTx } from '../../resources'; + +function createMockTransaction(txData: string): Partial { + return { + id: '123', + type: TransactionType.StakingWithdraw, + toBroadcastFormat: () => txData, + inputs: [], + outputs: [], + signature: ['mock-signature'], + toJson: () => ({ + id: '123', + type: 'StakingWithdraw', + sender: accounts.account1.address, + referenceBlock: '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d', + blockNumber: 100, + genesisHash: '0x', + nonce: 1, + tip: 0, + specVersion: 1, + transactionVersion: 1, + chainName: 'Polymesh', + inputs: [], + outputs: [], + }), + }; +} + +describe('Polyx WithdrawUnbonded Builder', function () { + let builder: WithdrawUnbondedBuilder; + const factory = new TransactionBuilderFactory(coins.get('tpolyx')); + + const senderAddress = accounts.account1.address; + const testSlashingSpans = 0; + + beforeEach(() => { + builder = factory.getWithdrawUnbondedBuilder(); + }); + + describe('setter validation', () => { + it('should validate slashing spans', () => { + const spy = sinon.spy(builder, 'validateValue'); + should.throws(() => builder.slashingSpans(-1), /Value cannot be less than zero/); + should.doesNotThrow(() => builder.slashingSpans(0)); + sinon.assert.calledTwice(spy); + }); + }); + + describe('Build and Sign', function () { + it('should build a withdraw unbonded transaction', async () => { + builder + .slashingSpans(testSlashingSpans) + .sender({ address: senderAddress }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 100 }); + + const mockTx = createMockTransaction(rawTx.withdrawUnbonded.unsigned); + sinon.stub(builder, 'build').resolves(mockTx as Transaction); + + const tx = await builder.build(); + should.exist(tx); + + should.equal(builder.getSlashingSpans(), testSlashingSpans); + }); + }); + + describe('Transaction Validation', function () { + it('should validate decoded transaction', () => { + const mockDecodedTx: DecodedSigningPayload = { + method: { + name: 'staking.withdrawUnbonded', + pallet: 'staking', + args: { + numSlashingSpans: testSlashingSpans, + }, + }, + address: senderAddress, + blockHash: '0x', + blockNumber: '0', + era: { mortalEra: '0x' }, + genesisHash: '0x', + metadataRpc: '0x', + nonce: 0, + specVersion: 0, + tip: '0', + transactionVersion: 0, + signedExtensions: [], + } as unknown as DecodedSigningPayload; + + should.doesNotThrow(() => { + builder.validateDecodedTransaction(mockDecodedTx); + }); + }); + + it('should reject invalid transaction types', () => { + const mockDecodedTx: DecodedSigningPayload = { + method: { + name: 'balances.transfer', + pallet: 'balances', + args: {}, + }, + address: senderAddress, + blockHash: '0x', + blockNumber: '0', + era: { mortalEra: '0x' }, + genesisHash: '0x', + metadataRpc: '0x', + nonce: 0, + specVersion: 0, + tip: '0', + transactionVersion: 0, + signedExtensions: [], + } as unknown as DecodedSigningPayload; + + should.throws(() => { + builder.validateDecodedTransaction(mockDecodedTx); + }, /Invalid transaction type/); + }); + }); + + describe('From Raw Transaction', function () { + beforeEach(() => { + sinon.stub(builder, 'from').callsFake(function (this: WithdrawUnbondedBuilder, rawTransaction: string) { + if (rawTransaction === rawTx.withdrawUnbonded.unsigned) { + this.slashingSpans(testSlashingSpans); + this.sender({ address: senderAddress }); + } + return this; + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should rebuild from rawTransaction', () => { + builder.from(rawTx.withdrawUnbonded.unsigned); + should.equal(builder.getSlashingSpans(), testSlashingSpans); + }); + }); +});