diff --git a/cspell.json b/cspell.json index 1ed410f95f..f56a09c4ac 100644 --- a/cspell.json +++ b/cspell.json @@ -76,7 +76,9 @@ "tzip", "Umami", "unopt", - "UNPAIR" + "UNPAIR", + "unstake", + "zarith" ], "files": [ "**/*.ts", diff --git a/integration-tests/contract-staking.spec.ts b/integration-tests/contract-staking.spec.ts new file mode 100644 index 0000000000..321365d0d7 --- /dev/null +++ b/integration-tests/contract-staking.spec.ts @@ -0,0 +1,27 @@ +import { CONFIGS } from "./config"; + +CONFIGS().forEach(({ lib, rpc, setup, knownBaker }) => { + const Tezos = lib; + describe(`Test staking through contract API using: ${rpc}`, () => { + beforeEach(async (done) => { + await setup(true) + done() + }); + it('Should be able to stake', async (done) => { + const delegate = knownBaker + const pkh = await Tezos.signer.publicKeyHash(); + const op = await Tezos.contract.stake({ + baker: delegate, + amount: 0.1, + parameter: { + entrypoint: "stake", + value: { "prim": "Unit" } + } + }); + await op.confirmation() + expect(op.hash).toBeDefined(); + console.log(op); + done(); + }); + }); +}); \ No newline at end of file diff --git a/packages/taquito/src/contract/interface.ts b/packages/taquito/src/contract/interface.ts index c6fc72e036..8c35fc8dd2 100644 --- a/packages/taquito/src/contract/interface.ts +++ b/packages/taquito/src/contract/interface.ts @@ -24,6 +24,9 @@ import { SmartRollupAddMessagesParams, SmartRollupOriginateParams, FailingNoopParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { ContractAbstraction, ContractStorageType, DefaultContractType } from './contract'; import { IncreasePaidStorageOperation } from '../operations/increase-paid-storage-operation'; @@ -283,4 +286,34 @@ export interface ContractProvider extends StorageProvider { * @param params FailingNoop operation parameter */ failingNoop(params: FailingNoopParams): Promise; + + /** + * + * @description Stake founds. Will sign and inject an operation using the current context + * + * @returns An operation handle with the result from the rpc node + * + * @param stake operation parameter + */ + stake(params: StakeParams): Promise; + + /** + * + * @description Unstake founds. Will sign and inject an operation using the current context + * + * @returns An operation handle with the result from the rpc node + * + * @param unstake operation parameter + */ + unstake(params: UnstakeParams): Promise; + + /** + * + * @description Finalize unstake founds. Will sign and inject an operation using the current context + * + * @returns An operation handle with the result from the rpc node + * + * @param finalizeUnstake operation parameter + */ + unstake(params: FinalizeUnstakeParams): Promise; } diff --git a/packages/taquito/src/contract/prepare.ts b/packages/taquito/src/contract/prepare.ts index 96955e3bb7..a0c6cf3c3a 100644 --- a/packages/taquito/src/contract/prepare.ts +++ b/packages/taquito/src/contract/prepare.ts @@ -31,6 +31,12 @@ import { SmartRollupOriginateParamsWithProof, ActivationParams, RPCActivateOperation, + RPCStakeOperation, + RPCUnstakeOperation, + RPCFinalizeUnstakeOperation, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { DEFAULT_FEE, @@ -339,3 +345,81 @@ export const createSmartRollupOriginateOperation = async ({ parameters_ty: parametersType, } as RPCSmartRollupOriginateOperation; }; + +export const createStakeOperation = async ({ + baker, + amount, + fee = DEFAULT_FEE.TRANSFER, + gasLimit = DEFAULT_GAS_LIMIT.TRANSFER, + storageLimit = DEFAULT_STORAGE_LIMIT.TRANSFER, + mutez = false, +}: StakeParams) => { + const operation: RPCStakeOperation = { + kind: OpKind.TRANSACTION, + fee, + gas_limit: gasLimit, + storage_limit: storageLimit, + amount: mutez ? amount.toString() : format('tz', 'mutez', amount).toString(), + source: baker, + destination: baker, + parameters: { + entrypoint: 'stake', + value: { + prim: 'Unit', + }, + }, + }; + return operation; +}; + +export const createUnstakeOperation = async ({ + baker, + amount, + fee = DEFAULT_FEE.TRANSFER, + gasLimit = DEFAULT_GAS_LIMIT.TRANSFER, + storageLimit = DEFAULT_STORAGE_LIMIT.TRANSFER, + mutez = false, +}: UnstakeParams) => { + const operation: RPCUnstakeOperation = { + kind: OpKind.TRANSACTION, + fee, + gas_limit: gasLimit, + storage_limit: storageLimit, + amount: mutez ? amount.toString() : format('tz', 'mutez', amount).toString(), + source: baker, + destination: baker, + parameters: { + entrypoint: 'unstake', + value: { + prim: 'Unit', + }, + }, + }; + return operation; +}; + +export const createFinalizeUnstakeOperation = async ({ + baker, + amount, + fee = DEFAULT_FEE.TRANSFER, + gasLimit = DEFAULT_GAS_LIMIT.TRANSFER, + storageLimit = DEFAULT_STORAGE_LIMIT.TRANSFER, + mutez = false, +}: FinalizeUnstakeParams) => { + const operation: RPCFinalizeUnstakeOperation = { + kind: OpKind.TRANSACTION, + fee, + gas_limit: gasLimit, + storage_limit: storageLimit, + amount: mutez ? amount.toString() : format('tz', 'mutez', amount).toString(), + source: baker, + destination: baker, + parameters: { + entrypoint: 'finalize_unstake', + value: { + prim: 'Unit', + }, + }, + }; + return operation; +}; diff --git a/packages/taquito/src/contract/rpc-contract-provider.ts b/packages/taquito/src/contract/rpc-contract-provider.ts index 35dfed1195..cfb57a4915 100644 --- a/packages/taquito/src/contract/rpc-contract-provider.ts +++ b/packages/taquito/src/contract/rpc-contract-provider.ts @@ -54,6 +54,8 @@ import { SmartRollupAddMessagesParams, SmartRollupOriginateParams, FailingNoopParams, + StakeParams, + UnstakeParams, } from '../operations/types'; import { DefaultContractType, ContractStorageType, ContractAbstraction } from './contract'; import { InvalidDelegationSource, RevealOperationError } from './errors'; @@ -764,6 +766,75 @@ export class RpcContractProvider extends Provider implements ContractProvider, S return batch; } + + /** + * + * @description Stake founds. Will sign and inject an operation using the current context + * + * @returns An operation handle with the result from the rpc node + * + * @param stake operation parameter + */ + async stake(params: StakeParams) { + const estimate = await this.estimate(params, this.estimator.stake.bind(this.estimator)); + const source = await this.signer.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 founds. Will sign and inject an operation using the current context + * + * @returns An operation handle with the result from the rpc node + * + * @param unstake operation parameter + */ + async unstake(params: UnstakeParams) { + const estimate = await this.estimate(params, this.estimator.stake.bind(this.estimator)); + const source = await this.signer.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 Finalize unstake founds. Will sign and inject an operation using the current context + * + * @returns An operation handle with the result from the rpc node + * + * @param unstake operation parameter + */ + async finalizeUnstake(params: UnstakeParams) { + const estimate = await this.estimate( + params, + this.estimator.finalizeUnstake.bind(this.estimator) + ); + const source = await this.signer.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); + } } type ContractAbstractionComposer = ( diff --git a/packages/taquito/src/estimate/estimate-provider-interface.ts b/packages/taquito/src/estimate/estimate-provider-interface.ts index 6708340060..1cb068ea1b 100644 --- a/packages/taquito/src/estimate/estimate-provider-interface.ts +++ b/packages/taquito/src/estimate/estimate-provider-interface.ts @@ -13,6 +13,9 @@ import { UpdateConsensusKeyParams, SmartRollupAddMessagesParams, SmartRollupOriginateParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { Estimate } from './estimate'; import { ContractMethod, ContractMethodObject, ContractProvider } from '../contract'; @@ -140,4 +143,28 @@ export interface EstimationProvider { * @param SmartRollupOrigianteParams */ smartRollupOriginate(params: SmartRollupOriginateParams): Promise; + + /** + * + * @description Estimate gasLimit, storageLimit and fees for stake operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the stake operation + */ + stake(params: StakeParams): Promise; + + /** + * + * @description Estimate gasLimit, storageLimit and fees for unstake operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the unstake operation + */ + unstake(params: UnstakeParams): Promise; + + /** + * + * @description Estimate gasLimit, storageLimit and fees for finalize_unstake operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the finalize_unstake operation + */ + finalizeUnstake(params: FinalizeUnstakeParams): Promise; } diff --git a/packages/taquito/src/estimate/rpc-estimate-provider.ts b/packages/taquito/src/estimate/rpc-estimate-provider.ts index 0f474e3dc6..6b42ae528e 100644 --- a/packages/taquito/src/estimate/rpc-estimate-provider.ts +++ b/packages/taquito/src/estimate/rpc-estimate-provider.ts @@ -15,6 +15,9 @@ import { UpdateConsensusKeyParams, SmartRollupAddMessagesParams, SmartRollupOriginateParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { Estimate, EstimateProperties } from './estimate'; import { EstimationProvider } from '../estimate/estimate-provider-interface'; @@ -457,4 +460,64 @@ export class RPCEstimateProvider extends Provider implements EstimationProvider } return Estimate.createEstimateInstanceFromProperties(estimateProperties); } + + /** + * + * @description Estimate gasLimit, storageLimit and fees for stake operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the stake operation + * + * @param Estimate + */ + async stake(params: StakeParams) { + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const preparedOperation = await this.prepare.stake(params); + + const estimateProperties = await this.calculateEstimates(preparedOperation, protocolConstants); + + if (preparedOperation.opOb.contents[0].kind === 'reveal') { + estimateProperties.shift(); + } + return Estimate.createEstimateInstanceFromProperties(estimateProperties); + } + + /** + * + * @description Estimate gasLimit, storageLimit and fees for unstake operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the unstake operation + * + * @param Estimate + */ + async unstake(params: UnstakeParams) { + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const preparedOperation = await this.prepare.unstake(params); + + const estimateProperties = await this.calculateEstimates(preparedOperation, protocolConstants); + + if (preparedOperation.opOb.contents[0].kind === 'reveal') { + estimateProperties.shift(); + } + return Estimate.createEstimateInstanceFromProperties(estimateProperties); + } + + /** + * + * @description Estimate gasLimit, storageLimit and fees for finalize_unstake operation + * + * @returns An estimation of gasLimit, storageLimit and fees for the finalize_unstake operation + * + * @param Estimate + */ + async finalizeUnstake(params: FinalizeUnstakeParams) { + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const preparedOperation = await this.prepare.finalizeUnstake(params); + + const estimateProperties = await this.calculateEstimates(preparedOperation, protocolConstants); + + if (preparedOperation.opOb.contents[0].kind === 'reveal') { + estimateProperties.shift(); + } + return Estimate.createEstimateInstanceFromProperties(estimateProperties); + } } diff --git a/packages/taquito/src/operations/types.ts b/packages/taquito/src/operations/types.ts index 619bdb9bed..1bf339144b 100644 --- a/packages/taquito/src/operations/types.ts +++ b/packages/taquito/src/operations/types.ts @@ -568,6 +568,84 @@ export interface FailingNoopParams { basedOnBlock: BlockIdentifier; } +export interface StakeParams { + baker: string; + amount: number; + fee?: number; + parameter?: TransactionOperationParameter; + gasLimit?: number; + storageLimit?: number; + mutez?: boolean; +} + +export interface RPCStakeOperation { + kind: OpKind.TRANSACTION; + fee: number; + gas_limit: number; + storage_limit: number; + amount: string; + source: string; + destination: string; + parameters: { + entrypoint: 'stake'; + value: { + prim: 'Unit'; + }; + }; +} + +export interface UnstakeParams { + baker: string; + amount: number; + fee?: number; + parameter?: TransactionOperationParameter; + gasLimit?: number; + storageLimit?: number; + mutez?: boolean; +} + +export interface RPCUnstakeOperation { + kind: OpKind.TRANSACTION; + fee: number; + gas_limit: number; + storage_limit: number; + amount: string; + source: string; + destination: string; + parameters: { + entrypoint: 'unstake'; + value: { + prim: 'Unit'; + }; + }; +} + +export interface FinalizeUnstakeParams { + baker: string; + amount: number; + fee?: number; + parameter?: TransactionOperationParameter; + gasLimit?: number; + storageLimit?: number; + mutez?: boolean; +} + +export interface RPCFinalizeUnstakeOperation { + kind: OpKind.TRANSACTION; + fee: number; + gas_limit: number; + storage_limit: number; + amount: string; + source: string; + destination: string; + parameters: { + entrypoint: 'finalize_unstake'; + value: { + prim: 'Unit'; + }; + }; +} + export type RPCOperation = | RPCOriginationOperation | RPCTransferOperation @@ -583,7 +661,10 @@ export type RPCOperation = | RPCUpdateConsensusKeyOperation | RPCSmartRollupAddMessagesOperation | RPCFailingNoopOperation - | RPCSmartRollupOriginateOperation; + | RPCSmartRollupOriginateOperation + | RPCStakeOperation + | RPCUnstakeOperation + | RPCFinalizeUnstakeOperation; export type PrepareOperationParams = { operation: RPCOperation | RPCOperation[]; diff --git a/packages/taquito/src/prepare/prepare-provider.ts b/packages/taquito/src/prepare/prepare-provider.ts index b32bc70037..b460ed384f 100644 --- a/packages/taquito/src/prepare/prepare-provider.ts +++ b/packages/taquito/src/prepare/prepare-provider.ts @@ -27,6 +27,9 @@ import { isOpWithFee, RegisterDelegateParams, ActivationParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; import { PreparationProvider, PreparedOperation } from './interface'; import { DEFAULT_FEE, DEFAULT_STORAGE_LIMIT, Protocols, getRevealGasLimit } from '../constants'; @@ -54,6 +57,9 @@ import { createSmartRollupOriginateOperation, createRegisterDelegateOperation, createActivationOperation, + createStakeOperation, + createUnstakeOperation, + createFinalizeUnstakeOperation, } from '../contract'; import { Estimate } from '../estimate'; import { ForgeParams } from '@taquito/local-forging'; @@ -1098,4 +1104,133 @@ export class PrepareProvider extends Provider implements PreparationProvider { contents, }; } + + /** + * + * @description Method to prepare a stake operation + * @param operation RPCOperation object + * @param source string or undefined source pkh + * @returns a PreparedOperation object + */ + async stake( + { fee, storageLimit, gasLimit, ...rest }: StakeParams, + source?: string + ): Promise { + const { pkh } = await this.getKeys(); + + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const DEFAULT_PARAMS = await this.getAccountLimits(pkh, protocolConstants); + const mergedEstimates = mergeLimits({ fee, storageLimit, gasLimit }, DEFAULT_PARAMS); + + const op = await createStakeOperation({ + ...rest, + ...mergedEstimates, + }); + + 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, source); + + return { + opOb: { + branch: hash, + contents, + protocol, + }, + counter: headCounter, + }; + } + + /** + * + * @description Method to prepare an unstake operation + * @param operation RPCOperation object + * @param source string or undefined source pkh + * @returns a PreparedOperation object + */ + async unstake( + { fee, storageLimit, gasLimit, ...rest }: UnstakeParams, + source?: string + ): Promise { + const { pkh } = await this.getKeys(); + + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const DEFAULT_PARAMS = await this.getAccountLimits(pkh, protocolConstants); + const mergedEstimates = mergeLimits({ fee, storageLimit, gasLimit }, DEFAULT_PARAMS); + + const op = await createUnstakeOperation({ + ...rest, + ...mergedEstimates, + }); + + 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, source); + + return { + opOb: { + branch: hash, + contents, + protocol, + }, + counter: headCounter, + }; + } + + /** + * + * @description Method to prepare an unstake operation + * @param operation RPCOperation object + * @param source string or undefined source pkh + * @returns a PreparedOperation object + */ + async finalizeUnstake( + { fee, storageLimit, gasLimit, ...rest }: FinalizeUnstakeParams, + source?: string + ): Promise { + const { pkh } = await this.getKeys(); + + const protocolConstants = await this.context.readProvider.getProtocolConstants('head'); + const DEFAULT_PARAMS = await this.getAccountLimits(pkh, protocolConstants); + const mergedEstimates = mergeLimits({ fee, storageLimit, gasLimit }, DEFAULT_PARAMS); + + const op = await createFinalizeUnstakeOperation({ + ...rest, + ...mergedEstimates, + }); + + 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, source); + + return { + opOb: { + branch: hash, + contents, + protocol, + }, + counter: headCounter, + }; + } }