Skip to content

Commit

Permalink
added staking support in contract API, with unit and integration tests (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
dsawali committed Apr 24, 2024
1 parent a89b872 commit d95d865
Show file tree
Hide file tree
Showing 13 changed files with 1,455 additions and 2 deletions.
56 changes: 56 additions & 0 deletions integration-tests/__tests__/contract/operations/staking.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
22 changes: 22 additions & 0 deletions packages/taquito-core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/taquito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
33 changes: 33 additions & 0 deletions packages/taquito/src/contract/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -158,6 +161,36 @@ export interface ContractProvider extends StorageProvider {
*/
transfer(params: TransferParams): Promise<TransactionOperation>;

/**
*
* @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<TransactionOperation>;

/**
*
* @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<TransactionOperation>;

/**
*
* @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<TransactionOperation>;

/**
*
* @description Transfer tickets from an implicit account to a contract or another implicit account.
Expand Down
128 changes: 128 additions & 0 deletions packages/taquito/src/contract/rpc-contract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
InvalidAddressError,
InvalidContractAddressError,
InvalidAmountError,
InvalidFinalizeUnstakeAmountError,
InvalidStakingAddressError,
} from '@taquito/core';
import { OperationBatch } from '../batch/rpc-batch-provider';
import { Context } from '../context';
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions packages/taquito/src/estimate/estimate-provider-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
SmartRollupAddMessagesParams,
SmartRollupOriginateParams,
SmartRollupExecuteOutboxMessageParams,
StakeParams,
UnstakeParams,
FinalizeUnstakeParams,
} from '../operations/types';
import { Estimate } from './estimate';
import { ContractMethod, ContractMethodObject, ContractProvider } from '../contract';
Expand All @@ -39,6 +42,41 @@ export interface EstimationProvider {
*/
transfer({ fee, storageLimit, gasLimit, ...rest }: TransferParams): Promise<Estimate>;

/**
*
* @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<Estimate>;

/**
*
* @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<Estimate>;

/**
*
* @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<Estimate>;

/**
*
* @description Estimate gasLimit, storageLimit and fees for an transferTicket operation
Expand Down
Loading

0 comments on commit d95d865

Please sign in to comment.