From d95d865d97ba27e962802f0ebc0526a6595d77e9 Mon Sep 17 00:00:00 2001 From: Davis Sawali Date: Tue, 23 Apr 2024 18:11:36 -0700 Subject: [PATCH] added staking support in contract API, with unit and integration tests (#2930) * added staking support in contract API, with unit and integration tests * updated ux to not require destination (for stake/unstake) and not require a param for finalize unstake * updated error message * addressed PR comments * remove wrong check * removed undefined check for a more general falsy check --- .../contract/operations/staking.spec.ts | 56 +++ packages/taquito-core/src/errors.ts | 22 + packages/taquito/package.json | 2 +- packages/taquito/src/contract/interface.ts | 33 ++ .../src/contract/rpc-contract-provider.ts | 128 ++++++ .../estimate/estimate-provider-interface.ts | 38 ++ .../src/estimate/rpc-estimate-provider.ts | 129 +++++- packages/taquito/src/operations/types.ts | 42 ++ .../taquito/src/prepare/prepare-provider.ts | 142 ++++++ packages/taquito/test/contract/helper.ts | 183 ++++++++ .../contract/rpc-contract-provider.spec.ts | 416 ++++++++++++++++++ .../estimate/rpc-estimate-provider.spec.ts | 33 ++ .../test/prepare/prepare-provider.spec.ts | 233 ++++++++++ 13 files changed, 1455 insertions(+), 2 deletions(-) create mode 100644 integration-tests/__tests__/contract/operations/staking.spec.ts diff --git a/integration-tests/__tests__/contract/operations/staking.spec.ts b/integration-tests/__tests__/contract/operations/staking.spec.ts new file mode 100644 index 0000000000..f09db91058 --- /dev/null +++ b/integration-tests/__tests__/contract/operations/staking.spec.ts @@ -0,0 +1,56 @@ +import { CONFIGS } from "../../../config"; + +CONFIGS().forEach(({ lib, rpc, setup }) => { + const Tezos = lib; + + describe(`Staking pseudo operations: ${rpc}`, () => { + + beforeAll(async () => { + await setup(true); + + const delegateOp = await Tezos.contract.setDelegate({ + delegate: 'tz1PZY3tEWmXGasYeehXYqwXuw2Z3iZ6QDnA', + source: await Tezos.signer.publicKeyHash() + }); + + await delegateOp.confirmation(); + }); + + it('should throw an error when the destination specified is not the same as source', async () => { + expect(async () => { + const op = await Tezos.contract.stake({ + amount: 0.1, + to: 'tz1PZY3tEWmXGasYeehXYqwXuw2Z3iZ6QDnA' + }); + }).rejects.toThrow(); + }); + + it('should be able to stake funds to a designated delegate', async () => { + const op = await Tezos.contract.stake({ + amount: 0.1 + }); + await op.confirmation(); + + expect(op.hash).toBeDefined(); + expect(op.status).toEqual('applied'); + }); + + it('should be able to unstake funds from a designated delegate', async () => { + const op = await Tezos.contract.unstake({ + amount: 0.1 + }); + await op.confirmation(); + + expect(op.hash).toBeDefined(); + expect(op.status).toEqual('applied'); + }); + + it('should be able to finalize_unstake funds from a designated delegate', async () => { + const op = await Tezos.contract.finalizeUnstake({}); + await op.confirmation(); + + expect(op.hash).toBeDefined(); + expect(op.status).toEqual('applied'); + }); + }); +}); diff --git a/packages/taquito-core/src/errors.ts b/packages/taquito-core/src/errors.ts index 104e821a16..192147c35a 100644 --- a/packages/taquito-core/src/errors.ts +++ b/packages/taquito-core/src/errors.ts @@ -62,6 +62,28 @@ export class InvalidAddressError extends ParameterValidationError { } } +export class InvalidStakingAddressError extends ParameterValidationError { + constructor( + public readonly address: string, + public readonly errorDetail?: string + ) { + super(); + this.name = 'InvalidStakingAddressError'; + this.message = `Invalid staking address "${address}", you can only set destination as your own address`; + } +} + +export class InvalidFinalizeUnstakeAmountError extends ParameterValidationError { + constructor( + public readonly address: string, + public readonly errorDetail?: string + ) { + super(); + this.name = 'InvalidFinalizeUnstakeAmountError'; + this.message = `The amount can only be 0 when finalizing an unstake`; + } +} + /** * @category Error * @description Error that indicates an invalid block hash being passed or used diff --git a/packages/taquito/package.json b/packages/taquito/package.json index 268d2ebeec..38801ea3dd 100644 --- a/packages/taquito/package.json +++ b/packages/taquito/package.json @@ -31,7 +31,7 @@ "node": ">=18" }, "scripts": { - "test": "jest --coverage --testPathIgnorePatterns=operation-factory.spec.ts", + "test": "jest --coverage", "test:watch": "jest --coverage --watch", "test:prod": "npm run lint && npm run test -- --no-cache", "lint": "eslint --ext .js,.ts .", diff --git a/packages/taquito/src/contract/interface.ts b/packages/taquito/src/contract/interface.ts index ef3b5ea24c..cbcf003bef 100644 --- a/packages/taquito/src/contract/interface.ts +++ b/packages/taquito/src/contract/interface.ts @@ -25,6 +25,9 @@ import { SmartRollupOriginateParams, SmartRollupExecuteOutboxMessageParams, FailingNoopParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { ContractAbstraction, ContractStorageType, DefaultContractType } from './contract'; import { IncreasePaidStorageOperation } from '../operations/increase-paid-storage-operation'; @@ -158,6 +161,36 @@ export interface ContractProvider extends StorageProvider { */ transfer(params: TransferParams): Promise; + /** + * + * @description Stake tz from current address to a specific address. Built on top of the existing transaction operation + * + * @returns An operation handle with the result from the rpc node + * + * @param Stake pseudo-operation parameter + */ + stake(params: StakeParams): Promise; + + /** + * + * @description Unstake tz from current address to a specific address. Built on top of the existing transaction operation + * + * @returns An operation handle with the result from the rpc node + * + * @param Unstake pseudo-operation parameter + */ + unstake(params: UnstakeParams): Promise; + + /** + * + * @description Finalize unstake tz from current address to a specific address. Built on top of the existing transaction operation + * + * @returns An operation handle with the result from the rpc node + * + * @param finalize_unstake pseudo-operation parameter + */ + finalizeUnstake(params: FinalizeUnstakeParams): Promise; + /** * * @description Transfer tickets from an implicit account to a contract or another implicit account. diff --git a/packages/taquito/src/contract/rpc-contract-provider.ts b/packages/taquito/src/contract/rpc-contract-provider.ts index 34695f0d4c..d4d490981d 100644 --- a/packages/taquito/src/contract/rpc-contract-provider.ts +++ b/packages/taquito/src/contract/rpc-contract-provider.ts @@ -30,6 +30,8 @@ import { InvalidAddressError, InvalidContractAddressError, InvalidAmountError, + InvalidFinalizeUnstakeAmountError, + InvalidStakingAddressError, } from '@taquito/core'; import { OperationBatch } from '../batch/rpc-batch-provider'; import { Context } from '../context'; @@ -56,6 +58,9 @@ import { SmartRollupOriginateParams, SmartRollupExecuteOutboxMessageParams, FailingNoopParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { DefaultContractType, ContractStorageType, ContractAbstraction } from './contract'; import { InvalidDelegationSource, RevealOperationError } from './errors'; @@ -395,6 +400,129 @@ export class RpcContractProvider extends Provider implements ContractProvider, S return new TransactionOperation(hash, content, source, forgedBytes, opResponse, context); } + /** + * + * @description Stake a given amount for the source address + * + * @returns An operation handle with the result from the rpc node + * + * @param Stake pseudo-operation parameter + */ + async stake(params: StakeParams) { + const sourceValidation = validateAddress(params.source ?? ''); + if (params.source && sourceValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(params.source, invalidDetail(sourceValidation)); + } + + if (!params.to) { + params.to = params.source; + } + if (params.to && params.to !== params.source) { + throw new InvalidStakingAddressError(params.to); + } + + if (params.amount < 0) { + throw new InvalidAmountError(params.amount.toString()); + } + const publicKeyHash = await this.signer.publicKeyHash(); + const estimate = await this.estimate(params, this.estimator.stake.bind(this.estimator)); + + const source = params.source || publicKeyHash; + const prepared = await this.prepare.stake({ ...params, ...estimate }); + const content = prepared.opOb.contents.find( + (op) => op.kind === OpKind.TRANSACTION + ) as OperationContentsTransaction; + + const opBytes = await this.forge(prepared); + const { hash, context, forgedBytes, opResponse } = await this.signAndInject(opBytes); + return new TransactionOperation(hash, content, source, forgedBytes, opResponse, context); + } + + /** + * + * @description Unstake the given amount. If "everything" is given as amount, unstakes everything from the staking balance. + * Unstaked tez remains frozen for a set amount of cycles (the slashing period) after the operation. Once this period is over, + * the operation "finalize unstake" must be called for the funds to appear in the liquid balance. + * + * @returns An operation handle with the result from the rpc node + * + * @param Unstake pseudo-operation parameter + */ + async unstake(params: UnstakeParams) { + const sourceValidation = validateAddress(params.source ?? ''); + if (params.source && sourceValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(params.source, invalidDetail(sourceValidation)); + } + + if (!params.to) { + params.to = params.source; + } + + if (params.to && params.to !== params.source) { + throw new InvalidStakingAddressError(params.to); + } + + if (params.amount < 0) { + throw new InvalidAmountError(params.amount.toString()); + } + const publicKeyHash = await this.signer.publicKeyHash(); + const estimate = await this.estimate(params, this.estimator.unstake.bind(this.estimator)); + + const source = params.source || publicKeyHash; + const prepared = await this.prepare.unstake({ ...params, ...estimate }); + const content = prepared.opOb.contents.find( + (op) => op.kind === OpKind.TRANSACTION + ) as OperationContentsTransaction; + + const opBytes = await this.forge(prepared); + const { hash, context, forgedBytes, opResponse } = await this.signAndInject(opBytes); + return new TransactionOperation(hash, content, source, forgedBytes, opResponse, context); + } + + /** + * + * @description Transfer all the finalizable unstaked funds of the source to their liquid balance + * @returns An operation handle with the result from the rpc node + * + * @param Finalize_unstake pseudo-operation parameter + */ + async finalizeUnstake(params: FinalizeUnstakeParams) { + const sourceValidation = validateAddress(params.source ?? ''); + if (params.source && sourceValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(params.source, invalidDetail(sourceValidation)); + } + + if (!params.to) { + params.to = params.source; + } + if (params.to && params.to !== params.source) { + throw new InvalidStakingAddressError(params.to); + } + + if (!params.amount) { + params.amount = 0; + } + if (params.amount !== undefined && params.amount > 0) { + throw new InvalidFinalizeUnstakeAmountError('Amount must be 0 to finalize unstake.'); + } + + const publicKeyHash = await this.signer.publicKeyHash(); + const estimate = await this.estimate( + params, + this.estimator.finalizeUnstake.bind(this.estimator) + ); + + const source = params.source || publicKeyHash; + const prepared = await this.prepare.finalizeUnstake({ ...params, ...estimate }); + const content = prepared.opOb.contents.find( + (op) => op.kind === OpKind.TRANSACTION + ) as OperationContentsTransaction; + + const opBytes = await this.forge(prepared); + const { hash, context, forgedBytes, opResponse } = await this.signAndInject(opBytes); + return new TransactionOperation(hash, content, source, forgedBytes, opResponse, context); + } + /** * * @description Transfer Tickets to a smart contract address diff --git a/packages/taquito/src/estimate/estimate-provider-interface.ts b/packages/taquito/src/estimate/estimate-provider-interface.ts index 4a8ed02138..dc5644cec7 100644 --- a/packages/taquito/src/estimate/estimate-provider-interface.ts +++ b/packages/taquito/src/estimate/estimate-provider-interface.ts @@ -14,6 +14,9 @@ import { SmartRollupAddMessagesParams, SmartRollupOriginateParams, SmartRollupExecuteOutboxMessageParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { Estimate } from './estimate'; import { ContractMethod, ContractMethodObject, ContractProvider } from '../contract'; @@ -39,6 +42,41 @@ export interface EstimationProvider { */ transfer({ fee, storageLimit, gasLimit, ...rest }: TransferParams): Promise; + /** + * + * @description Estimate gasLimit, storageLimit and fees for an stake pseudo-operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the operation + * + * @param Estimate + */ + stake({ fee, storageLimit, gasLimit, ...rest }: StakeParams): Promise; + + /** + * + * @description Estimate gasLimit, storageLimit and fees for an unstake pseudo-operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the operation + * + * @param Estimate + */ + unstake({ fee, storageLimit, gasLimit, ...rest }: UnstakeParams): Promise; + + /** + * + * @description Estimate gasLimit, storageLimit and fees for an finalize_unstake pseudo-operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the operation + * + * @param Estimate + */ + finalizeUnstake({ + fee, + storageLimit, + gasLimit, + ...rest + }: FinalizeUnstakeParams): Promise; + /** * * @description Estimate gasLimit, storageLimit and fees for an transferTicket operation diff --git a/packages/taquito/src/estimate/rpc-estimate-provider.ts b/packages/taquito/src/estimate/rpc-estimate-provider.ts index bbe13d9b62..76875ec62b 100644 --- a/packages/taquito/src/estimate/rpc-estimate-provider.ts +++ b/packages/taquito/src/estimate/rpc-estimate-provider.ts @@ -17,6 +17,9 @@ import { SmartRollupAddMessagesParams, SmartRollupOriginateParams, SmartRollupExecuteOutboxMessageParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { Estimate, EstimateProperties } from './estimate'; import { EstimationProvider } from '../estimate/estimate-provider-interface'; @@ -26,7 +29,7 @@ import { ContractMethod, ContractMethodObject, ContractProvider } from '../contr import { Provider } from '../provider'; import { PrepareProvider } from '../prepare/prepare-provider'; import { PreparedOperation } from '../prepare'; -import { InvalidAddressError, InvalidAmountError } from '@taquito/core'; +import { InvalidAddressError, InvalidAmountError, InvalidStakingAddressError } from '@taquito/core'; // stub signature that won't be verified by tezos rpc simulate_operation const STUB_SIGNATURE = @@ -203,6 +206,130 @@ export class RPCEstimateProvider extends Provider implements EstimationProvider return Estimate.createEstimateInstanceFromProperties(estimateProperties); } + /** + * + * @description Estimate gasLimit, storageLimit and fees for an stake pseudo-operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the operation + * + * @param Stake pseudo-operation parameter + */ + async stake({ fee, storageLimit, gasLimit, ...rest }: StakeParams) { + const sourceValidation = validateAddress(rest.source ?? ''); + if (rest.source && sourceValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(rest.source, invalidDetail(sourceValidation)); + } + + if (!rest.to) { + rest.to = rest.source; + } + if (rest.to && rest.to !== rest.source) { + throw new InvalidStakingAddressError(rest.to); + } + + if (rest.amount < 0) { + throw new InvalidAmountError(rest.amount.toString()); + } + const preparedOperation = await this.prepare.stake({ + fee, + storageLimit, + gasLimit, + ...rest, + }); + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const estimateProperties = await this.calculateEstimates(preparedOperation, protocolConstants); + + if (preparedOperation.opOb.contents[0].kind === 'reveal') { + estimateProperties.shift(); + estimateProperties[0].opSize -= this.OP_SIZE_REVEAL / 2; + } + return Estimate.createEstimateInstanceFromProperties(estimateProperties); + } + + /** + * + * @description Estimate gasLimit, storageLimit and fees for an Unstake pseudo-operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the operation + * + * @param Unstake pseudo-operation parameter + */ + async unstake({ fee, storageLimit, gasLimit, ...rest }: UnstakeParams) { + const sourceValidation = validateAddress(rest.source ?? ''); + if (rest.source && sourceValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(rest.source, invalidDetail(sourceValidation)); + } + + if (!rest.to) { + rest.to = rest.source; + } + if (rest.to && rest.to !== rest.source) { + throw new InvalidStakingAddressError(rest.to); + } + + if (rest.amount < 0) { + throw new InvalidAmountError(rest.amount.toString()); + } + const preparedOperation = await this.prepare.unstake({ + fee, + storageLimit, + gasLimit, + ...rest, + }); + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const estimateProperties = await this.calculateEstimates(preparedOperation, protocolConstants); + + if (preparedOperation.opOb.contents[0].kind === 'reveal') { + estimateProperties.shift(); + estimateProperties[0].opSize -= this.OP_SIZE_REVEAL / 2; + } + return Estimate.createEstimateInstanceFromProperties(estimateProperties); + } + + /** + * + * @description Estimate gasLimit, storageLimit and fees for an finalize_unstake pseudo-operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the operation + * + * @param finalize_unstake pseudo-operation parameter + */ + async finalizeUnstake({ fee, storageLimit, gasLimit, ...rest }: FinalizeUnstakeParams) { + const sourceValidation = validateAddress(rest.source ?? ''); + if (rest.source && sourceValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(rest.source, invalidDetail(sourceValidation)); + } + + if (!rest.to) { + rest.to = rest.source; + } + if (rest.to && rest.to !== rest.source) { + throw new InvalidStakingAddressError(rest.to); + } + + if (!rest.amount) { + rest.amount = 0; + } + if (rest.amount !== undefined && rest.amount !== 0) { + throw new Error('Amount must be 0 for finalize_unstake operation'); + } + + const preparedOperation = await this.prepare.finalizeUnstake({ + fee, + storageLimit, + gasLimit, + ...rest, + }); + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const estimateProperties = await this.calculateEstimates(preparedOperation, protocolConstants); + + if (preparedOperation.opOb.contents[0].kind === 'reveal') { + estimateProperties.shift(); + estimateProperties[0].opSize -= this.OP_SIZE_REVEAL / 2; + } + return Estimate.createEstimateInstanceFromProperties(estimateProperties); + } + /** * * @description Estimate gasLimit, storageLimit and fees for a transferTicket operation diff --git a/packages/taquito/src/operations/types.ts b/packages/taquito/src/operations/types.ts index 7077bcf2cf..e6f860ad4f 100644 --- a/packages/taquito/src/operations/types.ts +++ b/packages/taquito/src/operations/types.ts @@ -310,6 +310,48 @@ export interface TransferParams { mutez?: boolean; } +/** + * @description RPC Stake pseudo operation params + */ +export interface StakeParams { + to?: string; + source?: string; + amount: number; + fee?: number; + parameter?: TransactionOperationParameter; + gasLimit?: number; + storageLimit?: number; + mutez?: boolean; +} + +/** + * @description RPC unstake pseudo operation params + */ +export interface UnstakeParams { + to?: string; + source?: string; + amount: number; + fee?: number; + parameter?: TransactionOperationParameter; + gasLimit?: number; + storageLimit?: number; + mutez?: boolean; +} + +/** + * @description RPC finalize_unstake pseudo operation params + */ +export interface FinalizeUnstakeParams { + to?: string; + source?: string; + amount?: number; + fee?: number; + parameter?: TransactionOperationParameter; + gasLimit?: number; + storageLimit?: number; + mutez?: boolean; +} + /** * @description RPC register global constant operation */ diff --git a/packages/taquito/src/prepare/prepare-provider.ts b/packages/taquito/src/prepare/prepare-provider.ts index 90f94108de..1989137e74 100644 --- a/packages/taquito/src/prepare/prepare-provider.ts +++ b/packages/taquito/src/prepare/prepare-provider.ts @@ -28,6 +28,9 @@ import { isOpWithFee, RegisterDelegateParams, ActivationParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { PreparationProvider, PreparedOperation } from './interface'; import { REVEAL_STORAGE_LIMIT, Protocols, getRevealFee, getRevealGasLimit } from '../constants'; @@ -453,6 +456,145 @@ export class PrepareProvider extends Provider implements PreparationProvider { }; } + /** + * + * @description Method to prepare a stake pseudo-operation + * @param operation RPCOperation object or RPCOperation array + * @param source string or undefined source pkh + * @returns a PreparedOperation object + */ + async stake({ fee, storageLimit, gasLimit, ...rest }: StakeParams): Promise { + const { pkh } = await this.getKeys(); + + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const DEFAULT_PARAMS = await this.getOperationLimits(protocolConstants); + const op = await createTransferOperation({ + ...rest, + to: pkh, + ...mergeLimits({ fee, storageLimit, gasLimit }, DEFAULT_PARAMS), + parameter: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + }); + + const operation = await this.addRevealOperationIfNeeded(op, pkh); + const ops = this.convertIntoArray(operation); + + const hash = await this.getBlockHash(); + const protocol = await this.getProtocolHash(); + + this.#counters = {}; + const headCounter = parseInt(await this.getHeadCounter(pkh), 10); + const contents = this.constructOpContents(ops, headCounter, pkh, rest.source); + + return { + opOb: { + branch: hash, + contents, + protocol, + }, + counter: headCounter, + }; + } + + /** + * + * @description Method to prepare a unstake pseudo-operation + * @param operation RPCOperation object or RPCOperation array + * @param source string or undefined source pkh + * @returns a PreparedOperation object + */ + async unstake({ + fee, + storageLimit, + gasLimit, + ...rest + }: UnstakeParams): Promise { + const { pkh } = await this.getKeys(); + + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const DEFAULT_PARAMS = await this.getOperationLimits(protocolConstants); + const op = await createTransferOperation({ + ...rest, + to: pkh, + ...mergeLimits({ fee, storageLimit, gasLimit }, DEFAULT_PARAMS), + parameter: { + entrypoint: 'unstake', + value: { prim: 'Unit' }, + }, + }); + + const operation = await this.addRevealOperationIfNeeded(op, pkh); + const ops = this.convertIntoArray(operation); + + const hash = await this.getBlockHash(); + const protocol = await this.getProtocolHash(); + + this.#counters = {}; + const headCounter = parseInt(await this.getHeadCounter(pkh), 10); + const contents = this.constructOpContents(ops, headCounter, pkh, rest.source); + + return { + opOb: { + branch: hash, + contents, + protocol, + }, + counter: headCounter, + }; + } + + /** + * + * @description Method to prepare a finalize_unstake pseudo-operation + * @param operation RPCOperation object or RPCOperation array + * @param source string or undefined source pkh + * @returns a PreparedOperation object + */ + async finalizeUnstake({ + fee, + storageLimit, + gasLimit, + ...rest + }: FinalizeUnstakeParams): Promise { + const { pkh } = await this.getKeys(); + + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const DEFAULT_PARAMS = await this.getOperationLimits(protocolConstants); + const op = await createTransferOperation({ + ...rest, + to: pkh, + amount: 0, + ...mergeLimits({ fee, storageLimit, gasLimit }, DEFAULT_PARAMS), + parameter: { + entrypoint: 'finalize_unstake', + value: { prim: 'Unit' }, + }, + }); + + const operation = await this.addRevealOperationIfNeeded(op, pkh); + const ops = this.convertIntoArray(operation); + + const hash = await this.getBlockHash(); + const protocol = await this.getProtocolHash(); + + this.#counters = {}; + const headCounter = parseInt(await this.getHeadCounter(pkh), 10); + const contents = this.constructOpContents(ops, headCounter, pkh, rest.source); + + return { + opOb: { + branch: hash, + contents, + protocol, + }, + counter: headCounter, + }; + } + /** * * @description Method to prepare a delegation operation diff --git a/packages/taquito/test/contract/helper.ts b/packages/taquito/test/contract/helper.ts index de8c8ee5c1..323b512f61 100644 --- a/packages/taquito/test/contract/helper.ts +++ b/packages/taquito/test/contract/helper.ts @@ -1658,3 +1658,186 @@ export const smartRollupExecuteOutboxMessageNoReveal = { signature: 'sigs8LVwSkqcMLzTVZWa1yS8aNz26A8bzR6QUHws5uVELh6kcmH7dWz5aKPqW3RXoFfynf5kVCvLJcsP3ucB5P6DEbD2YcQR', }; + +export const stakeNoReveal = { + contents: [ + { + kind: 'transaction', + source: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + fee: '623', + counter: '390', + gas_limit: '3630', + storage_limit: '0', + amount: '6000000000', + destination: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + metadata: { + balance_updates: [ + { + kind: 'contract', + contract: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + change: '-623', + origin: 'block', + }, + { + kind: 'accumulator', + category: 'block fees', + change: '623', + origin: 'block', + }, + ], + operation_result: { + status: 'applied', + balance_updates: [ + { + kind: 'contract', + contract: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + change: '-6000000000', + origin: 'block', + }, + { + kind: 'freezer', + category: 'deposits', + staker: { + baker: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + }, + change: '6000000000', + origin: 'block', + }, + ], + consumed_milligas: '3629020', + }, + }, + }, + ], + signature: + 'sigRn6MmipGZEBYYCrN5MYQmc8A6ye8iBJdahUY4gfHwFAP8kFXaoRvEx51bmo2qmfEDobuUJ4Ld9HKyuS9tV45qmKzLhuVE', +}; + +export const unstakeNoReveal = { + contents: [ + { + kind: 'transaction', + source: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + fee: '689', + counter: '408', + gas_limit: '4250', + storage_limit: '0', + amount: '99999999999000000', + destination: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + metadata: { + balance_updates: [ + { + kind: 'contract', + contract: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + change: '-689', + origin: 'block', + }, + { + kind: 'accumulator', + category: 'block fees', + change: '689', + origin: 'block', + }, + ], + operation_result: { + status: 'applied', + balance_updates: [ + { + kind: 'staking', + category: 'delegate_denominator', + delegate: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + change: '-1000000000', + origin: 'block', + }, + { + kind: 'staking', + category: 'delegator_numerator', + delegator: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + change: '-1000000000', + origin: 'block', + }, + { + kind: 'freezer', + category: 'deposits', + staker: { + contract: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + delegate: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + }, + change: '-1000000000', + origin: 'block', + }, + { + kind: 'freezer', + category: 'unstaked_deposits', + staker: { + contract: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + delegate: 'tz1X1TKpLiZuPEo2YVvDiqQ47Zp9a797ejkp', + }, + cycle: 37, + change: '1000000000', + origin: 'block', + }, + ], + consumed_milligas: '4249152', + }, + }, + }, + ], + signature: + 'sighibvGpL1NnZUyBfHPpPqE67tCb3aFK9L5JWhXA1GS9GqcizyZEEUfAGvzLMHxFG9oohhDzCJdMLFxppy3xbS9ZX45tWhM', +}; + +export const finalizeUnstakeNoReveal = { + contents: [ + { + kind: 'transaction', + source: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + fee: '409', + counter: '409', + gas_limit: '1529', + storage_limit: '0', + amount: '0', + destination: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + metadata: { + balance_updates: [ + { + kind: 'contract', + contract: 'tz1eDDguimfor6kkt96Ri4pBeEZXEzvuyjQX', + change: '-409', + origin: 'block', + }, + { + kind: 'accumulator', + category: 'block fees', + change: '409', + origin: 'block', + }, + ], + operation_result: { + status: 'applied', + consumed_milligas: '1528887', + }, + }, + }, + ], + signature: + 'sigoVqiaKpRr5jctRcRXmpJaeRFPxKTRAaUyEbhGXWbKyo1EZ1eBCjVzN7Cg7nzSexQLWit75o2cJPd3bfJn1ciwHRHDLCSf', +}; diff --git a/packages/taquito/test/contract/rpc-contract-provider.spec.ts b/packages/taquito/test/contract/rpc-contract-provider.spec.ts index ee44dea222..d83d04ff1f 100644 --- a/packages/taquito/test/contract/rpc-contract-provider.spec.ts +++ b/packages/taquito/test/contract/rpc-contract-provider.spec.ts @@ -87,6 +87,9 @@ describe('RpcContractProvider test', () => { contractCall: jest.Mock; smartRollupOriginate: jest.Mock; smartRollupExecuteOutboxMessage: jest.Mock; + stake: jest.Mock; + unstake: jest.Mock; + finalizeUnstake: jest.Mock; }; beforeEach(() => { @@ -146,6 +149,9 @@ describe('RpcContractProvider test', () => { contractCall: jest.fn(), smartRollupOriginate: jest.fn(), smartRollupExecuteOutboxMessage: jest.fn(), + stake: jest.fn(), + unstake: jest.fn(), + finalizeUnstake: jest.fn(), }; // Required for operations confirmation polling @@ -628,6 +634,416 @@ describe('RpcContractProvider test', () => { }); }); + describe('transfer - staking pseudo operations', () => { + it('should be able to produce a reveal and stake pseudo operation', async () => { + const result = await rpcContractProvider.stake({ + amount: 2, + fee: 10000, + gasLimit: 10600, + storageLimit: 300, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '10000', + gas_limit: '10600', + storage_limit: '300', + amount: '2000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce an error if destination is passed and is different than the source', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.stake.mockResolvedValue(estimate); + + expect(async () => { + await rpcContractProvider.stake({ + to: 'tz1iedjFYksExq8snZK9MNo4AvXHBdXfTsGX', + amount: 2, + }); + }).rejects.toThrow(); + }); + + it('should be able to produce a stake operation when no fees are specified', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.stake.mockResolvedValue(estimate); + + const result = await rpcContractProvider.stake({ + amount: 2, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '301', + gas_limit: '1', + storage_limit: '1000', + amount: '2000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce a stake operation without reveal when manager is defined', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.stake.mockResolvedValue(estimate); + + mockReadProvider.isAccountRevealed.mockResolvedValue(true); + + const result = await rpcContractProvider.stake({ + amount: 2, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'transaction', + fee: '301', + gas_limit: '1', + storage_limit: '1000', + amount: '2000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '1', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce a reveal and unstake pseudo operation', async () => { + const result = await rpcContractProvider.unstake({ + amount: 2, + fee: 10000, + gasLimit: 10600, + storageLimit: 300, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '10000', + gas_limit: '10600', + storage_limit: '300', + amount: '2000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce a reveal and unstake pseudo operation when no fees are specified', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.unstake.mockResolvedValue(estimate); + + const result = await rpcContractProvider.unstake({ + amount: 2, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '301', + gas_limit: '1', + storage_limit: '1000', + amount: '2000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce a reveal and unstake pseudo operation without reveal when account is revealed', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.unstake.mockResolvedValue(estimate); + + mockReadProvider.isAccountRevealed.mockResolvedValue(true); + + const result = await rpcContractProvider.unstake({ + amount: 2, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'transaction', + fee: '301', + gas_limit: '1', + storage_limit: '1000', + amount: '2000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '1', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce a reveal and finalize_unstake pseudo operation', async () => { + const result = await rpcContractProvider.finalizeUnstake({ + fee: 10000, + gasLimit: 10600, + storageLimit: 300, + }); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '10000', + gas_limit: '10600', + storage_limit: '300', + amount: '0', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should throw an error when an amount other than 0 is specified on a finalize_unstake pseudo-operation', async () => { + await expect( + rpcContractProvider.finalizeUnstake({ + amount: 2, + }) + ).rejects.toThrow(); + }); + + it('should be able to produce a reveal and finalize_unstake pseudo operation when no fees are specified', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.finalizeUnstake.mockResolvedValue(estimate); + + const result = await rpcContractProvider.finalizeUnstake({}); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '301', + gas_limit: '1', + storage_limit: '1000', + amount: '0', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + + it('should be able to produce a reveal and finalize_unstake pseudo operation without reveal when account is revealed', async () => { + const estimate = new Estimate(1000, 1000, 180, 1000); + mockEstimate.finalizeUnstake.mockResolvedValue(estimate); + + mockReadProvider.isAccountRevealed.mockResolvedValue(true); + + const result = await rpcContractProvider.finalizeUnstake({}); + + expect(result.raw).toEqual({ + opbytes: 'test', + opOb: { + branch: 'test', + contents: [ + { + kind: 'transaction', + fee: '301', + gas_limit: '1', + storage_limit: '1000', + amount: '0', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '1', + }, + ], + protocol: 'test_proto', + signature: 'test_sig', + }, + counter: 0, + }); + }); + }); + describe('transferTicket', () => { it('validate that a reveal operation will be added when needed', async () => { mockRpcClient.getManagerKey.mockReturnValue(null); diff --git a/packages/taquito/test/estimate/rpc-estimate-provider.spec.ts b/packages/taquito/test/estimate/rpc-estimate-provider.spec.ts index e06f698daf..c236c54c78 100644 --- a/packages/taquito/test/estimate/rpc-estimate-provider.spec.ts +++ b/packages/taquito/test/estimate/rpc-estimate-provider.spec.ts @@ -22,6 +22,9 @@ import { smartRollupAddMessagesNoReveal, smartRollupOriginateWithReveal, smartRollupExecuteOutboxMessageNoReveal, + stakeNoReveal, + unstakeNoReveal, + finalizeUnstakeNoReveal, } from '../contract/helper'; import { OpKind, PvmKind } from '@taquito/rpc'; import { TransferTicketParams } from '../../src/operations/types'; @@ -178,6 +181,36 @@ describe('RPCEstimateProvider test signer', () => { }); }); + describe('staking', () => { + it('should return estimates for stake pseudo-operation', async () => { + mockRpcClient.simulateOperation.mockResolvedValue(stakeNoReveal); + + const estimate = await estimateProvider.stake({ + amount: 2, + }); + + expect(estimate.gasLimit).toEqual(3730); + }); + + it('should return estimates for unstake pseudo-operation', async () => { + mockRpcClient.simulateOperation.mockResolvedValue(unstakeNoReveal); + + const estimate = await estimateProvider.unstake({ + amount: 2, + }); + + expect(estimate.gasLimit).toEqual(4350); + }); + + it('should return estimates for finalize_unstake pseudo-operation', async () => { + mockRpcClient.simulateOperation.mockResolvedValue(finalizeUnstakeNoReveal); + + const estimate = await estimateProvider.finalizeUnstake({}); + + expect(estimate.gasLimit).toEqual(1629); + }); + }); + describe('transfer', () => { it('return the correct estimate for multiple internal origination', async () => { mockRpcClient.getManagerKey.mockResolvedValue(null); diff --git a/packages/taquito/test/prepare/prepare-provider.spec.ts b/packages/taquito/test/prepare/prepare-provider.spec.ts index cb7c4892b7..89b4d309cf 100644 --- a/packages/taquito/test/prepare/prepare-provider.spec.ts +++ b/packages/taquito/test/prepare/prepare-provider.spec.ts @@ -264,6 +264,239 @@ describe('PrepareProvider test', () => { }); }); + describe('stake', () => { + it('should return a prepared stake pseudo operation with a reveal op', async () => { + mockReadProvider.isAccountRevealed.mockResolvedValue(false); + + const prepared = await prepareProvider.stake({ + amount: 1000000000, + }); + + expect(prepared).toEqual({ + opOb: { + branch: 'test_block_hash', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '0', + gas_limit: '1040000', + storage_limit: '60000', + amount: '1000000000000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_protocol', + }, + counter: 0, + }); + }); + + it('should return a prepared stake pseudo operation without a reveal op', async () => { + mockReadProvider.isAccountRevealed.mockResolvedValue(true); + + const prepared = await prepareProvider.stake({ + amount: 1000000000, + }); + + expect(prepared).toEqual({ + opOb: { + branch: 'test_block_hash', + contents: [ + { + kind: 'transaction', + fee: '0', + gas_limit: '1040000', + storage_limit: '60000', + amount: '1000000000000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '1', + }, + ], + protocol: 'test_protocol', + }, + counter: 0, + }); + }); + }); + + describe('finalize_unstake', () => { + it('should return an unstake pseudo operation with a reveal op', async () => { + mockReadProvider.isAccountRevealed.mockResolvedValue(false); + + const prepared = await prepareProvider.unstake({ + amount: 9999, + }); + + expect(prepared).toEqual({ + opOb: { + branch: 'test_block_hash', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '0', + gas_limit: '1040000', + storage_limit: '60000', + amount: '9999000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_protocol', + }, + counter: 0, + }); + }); + + it('should return an unstake pseudo operation without a reveal op', async () => { + mockReadProvider.isAccountRevealed.mockResolvedValue(true); + + const prepared = await prepareProvider.unstake({ + amount: 9999, + }); + + expect(prepared).toEqual({ + opOb: { + branch: 'test_block_hash', + contents: [ + { + kind: 'transaction', + fee: '0', + gas_limit: '1040000', + storage_limit: '60000', + amount: '9999000000', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '1', + }, + ], + protocol: 'test_protocol', + }, + counter: 0, + }); + }); + }); + + describe('finalize_unstake', () => { + it('should return a prepared finalize_unstake pseudo operation with a reveal op', async () => { + mockReadProvider.isAccountRevealed.mockResolvedValue(false); + + const prepared = await prepareProvider.finalizeUnstake({}); + + expect(prepared).toEqual({ + opOb: { + branch: 'test_block_hash', + contents: [ + { + kind: 'reveal', + fee: '331', + public_key: 'test_pub_key', + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + gas_limit: '625', + storage_limit: '0', + counter: '1', + }, + { + kind: 'transaction', + fee: '0', + gas_limit: '1040000', + storage_limit: '60000', + amount: '0', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '2', + }, + ], + protocol: 'test_protocol', + }, + counter: 0, + }); + }); + + it('should return a prepared finalize_unstake pseudo operation without a reveal op', async () => { + mockReadProvider.isAccountRevealed.mockResolvedValue(true); + + const prepared = await prepareProvider.finalizeUnstake({}); + + expect(prepared).toEqual({ + opOb: { + branch: 'test_block_hash', + contents: [ + { + kind: 'transaction', + fee: '0', + gas_limit: '1040000', + storage_limit: '60000', + amount: '0', + destination: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + source: 'tz1gvF4cD2dDtqitL3ZTraggSR1Mju2BKFEM', + counter: '1', + }, + ], + protocol: 'test_protocol', + }, + counter: 0, + }); + }); + }); + describe('drainDelegate', () => { it('should return a prepared drain_delegate operation', async () => { mockReadProvider.isAccountRevealed.mockResolvedValue(true);