From c25b90fbd42b6953608ced34762d25a57962227e Mon Sep 17 00:00:00 2001 From: huianyang Date: Wed, 24 Apr 2024 13:01:15 -0700 Subject: [PATCH] feat: walletAPI supports stake, unstake, finalizeUnstake with integration-tests --- .../wallet/staking-pseudo-operations.spec.ts | 62 ++++++++++++ .../src/taquito-beacon-wallet.ts | 56 ++++++++++- packages/taquito/src/wallet/interface.ts | 26 +++++ packages/taquito/src/wallet/legacy.ts | 15 +++ packages/taquito/src/wallet/wallet.ts | 98 ++++++++++++++++++- 5 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 integration-tests/__tests__/wallet/staking-pseudo-operations.spec.ts diff --git a/integration-tests/__tests__/wallet/staking-pseudo-operations.spec.ts b/integration-tests/__tests__/wallet/staking-pseudo-operations.spec.ts new file mode 100644 index 0000000000..4aa9137437 --- /dev/null +++ b/integration-tests/__tests__/wallet/staking-pseudo-operations.spec.ts @@ -0,0 +1,62 @@ +import { CONFIGS } from '../../config'; +import { Protocols } from '@taquito/taquito'; +import { ProtoGreaterOrEqual } from '@taquito/michel-codec'; +import { InvalidStakingAddressError, InvalidFinalizeUnstakeAmountError } from '@taquito/core'; + +CONFIGS().forEach(({ lib, rpc, setup, protocol }) => { + const Tezos = lib; + const parisAndAlpha = ProtoGreaterOrEqual(protocol, Protocols.PtParisBQ) ? test : test.skip; + describe(`Test staking pseudo operations using: ${rpc}`, () => { + beforeAll(async () => { + await setup(true); + try { + const delegateOp = await Tezos.contract.setDelegate({ + delegate: 'tz1PZY3tEWmXGasYeehXYqwXuw2Z3iZ6QDnA', // can use knownBaker in future + source: await Tezos.signer.publicKeyHash() + }); + await delegateOp.confirmation(); + } catch (e) { + console.log(JSON.stringify(e)); + } + }); + + parisAndAlpha(`should be able to stake successfully: ${rpc}`, async () => { + const op = await Tezos.wallet.stake({ amount: 3000000, mutez: true }).send() + await op.confirmation(); + expect(op.status).toBeTruthy(); + + const stakedBalance = await Tezos.rpc.getStakedBalance(await Tezos.signer.publicKeyHash()); + expect(stakedBalance.toNumber()).toEqual(3000000); + }); + + parisAndAlpha(`should be able to unstake successfully: ${rpc}`, async () => { + const op = await Tezos.wallet.unstake({ amount: 1 }).send() + await op.confirmation(); + expect(op.status).toBeTruthy(); + + const UnstakedBalance = await Tezos.rpc.getUnstakedFrozenBalance(await Tezos.signer.publicKeyHash()); + expect(UnstakedBalance.toNumber()).toEqual(1000000); // 1000000 mutez = 1 tez + }); + + parisAndAlpha(`should be able to finalizeUnstake successfully: ${rpc}`, async () => { + const op = await Tezos.wallet.finalizeUnstake({ }).send() + await op.confirmation(); + expect(op.status).toBeTruthy(); + }); + + parisAndAlpha('should throw error when param is against pseudo operation', async () => { + expect(async () => { + const op = await Tezos.wallet.stake({ amount: 1, to: 'tz1PZY3tEWmXGasYeehXYqwXuw2Z3iZ6QDnA' }).send(); + }).rejects.toThrow(InvalidStakingAddressError); + expect(async () => { + const op = await Tezos.wallet.unstake({ amount: 1, to: 'tz1PZY3tEWmXGasYeehXYqwXuw2Z3iZ6QDnA' }).send(); + }).rejects.toThrow(InvalidStakingAddressError); + expect(async () => { + const op = await Tezos.wallet.finalizeUnstake({ to: 'tz1PZY3tEWmXGasYeehXYqwXuw2Z3iZ6QDnA' }).send(); + }).rejects.toThrow(InvalidStakingAddressError); + expect(async () => { + const op = await Tezos.wallet.finalizeUnstake({ amount: 1 }).send(); + }).rejects.toThrow(InvalidFinalizeUnstakeAmountError); + }); + }); +}); diff --git a/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts b/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts index a683acae70..c725124970 100644 --- a/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts +++ b/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts @@ -23,6 +23,9 @@ import { WalletOriginateParams, WalletProvider, WalletTransferParams, + WalletStakeParams, + WalletUnstakeParams, + WalletFinalizeUnstakeParams, } from '@taquito/taquito'; import { buf2hex, hex2buf, mergebuf } from '@taquito/utils'; import { UnsupportedActionError } from '@taquito/core'; @@ -89,6 +92,51 @@ export class BeaconWallet implements WalletProvider { ); } + async mapStakeParamsToWalletParams(params: () => Promise) { + let walletParams: WalletStakeParams; + await this.client.showPrepare(); + try { + walletParams = await params(); + } catch (err) { + await this.client.hideUI(['alert']); + throw err; + } + return this.removeDefaultParams( + walletParams, + await createTransferOperation(this.formatParameters(walletParams)) + ); + } + + async mapUnstakeParamsToWalletParams(params: () => Promise) { + let walletParams: WalletUnstakeParams; + await this.client.showPrepare(); + try { + walletParams = await params(); + } catch (err) { + await this.client.hideUI(['alert']); + throw err; + } + return this.removeDefaultParams( + walletParams, + await createTransferOperation(this.formatParameters(walletParams)) + ); + } + + async mapFinalizeUnstakeParamsToWalletParams(params: () => Promise) { + let walletParams: WalletFinalizeUnstakeParams; + await this.client.showPrepare(); + try { + walletParams = await params(); + } catch (err) { + await this.client.hideUI(['alert']); + throw err; + } + return this.removeDefaultParams( + walletParams, + await createTransferOperation(this.formatParameters(walletParams)) + ); + } + async mapIncreasePaidStorageWalletParams(params: () => Promise) { let walletParams: WalletIncreasePaidStorageParams; await this.client.showPrepare(); @@ -148,7 +196,13 @@ export class BeaconWallet implements WalletProvider { } removeDefaultParams( - params: WalletTransferParams | WalletOriginateParams | WalletDelegateParams, + params: + | WalletTransferParams + | WalletStakeParams + | WalletUnstakeParams + | WalletFinalizeUnstakeParams + | WalletOriginateParams + | WalletDelegateParams, operatedParams: any ) { // If fee, storageLimit or gasLimit is undefined by user diff --git a/packages/taquito/src/wallet/interface.ts b/packages/taquito/src/wallet/interface.ts index a117882fe4..20e10fa560 100644 --- a/packages/taquito/src/wallet/interface.ts +++ b/packages/taquito/src/wallet/interface.ts @@ -4,12 +4,21 @@ import { IncreasePaidStorageParams, OriginateParams, TransferParams, + StakeParams, + UnstakeParams, + FinalizeUnstakeParams, } from '../operations/types'; export type WalletDefinedFields = 'source'; export type WalletTransferParams = Omit; +export type WalletStakeParams = Omit; + +export type WalletUnstakeParams = Omit; + +export type WalletFinalizeUnstakeParams = Omit; + export type WalletOriginateParams = Omit< OriginateParams, WalletDefinedFields @@ -37,6 +46,23 @@ export interface WalletProvider { */ mapTransferParamsToWalletParams: (params: () => Promise) => Promise; + /** + * @description Transform WalletStakeParams into a format compliant with the underlying wallet + */ + mapStakeParamsToWalletParams: (params: () => Promise) => Promise; + + /** + * @description Transform WalletUnstakeParams into a format compliant with the underlying wallet + */ + mapUnstakeParamsToWalletParams: (params: () => Promise) => Promise; + + /** + * @description Transform WalletFinalizeUnstakeParams into a format compliant with the underlying wallet + */ + mapFinalizeUnstakeParamsToWalletParams: ( + params: () => Promise + ) => Promise; + /** * @description Transform WalletOriginateParams into a format compliant with the underlying wallet */ diff --git a/packages/taquito/src/wallet/legacy.ts b/packages/taquito/src/wallet/legacy.ts index 010d66e707..7e84c26f43 100644 --- a/packages/taquito/src/wallet/legacy.ts +++ b/packages/taquito/src/wallet/legacy.ts @@ -6,6 +6,9 @@ import { WalletOriginateParams, WalletProvider, WalletTransferParams, + WalletStakeParams, + WalletUnstakeParams, + WalletFinalizeUnstakeParams, } from './interface'; import { WalletParamsWithKind } from './wallet'; @@ -24,6 +27,18 @@ export class LegacyWalletProvider implements WalletProvider { return attachKind(await params(), OpKind.TRANSACTION); } + async mapStakeParamsToWalletParams(params: () => Promise) { + return attachKind(await params(), OpKind.TRANSACTION); + } + + async mapUnstakeParamsToWalletParams(params: () => Promise) { + return attachKind(await params(), OpKind.TRANSACTION); + } + + async mapFinalizeUnstakeParamsToWalletParams(params: () => Promise) { + return attachKind(await params(), OpKind.TRANSACTION); + } + async mapOriginateParamsToWalletParams(params: () => Promise) { return attachKind(await params(), OpKind.ORIGINATION); } diff --git a/packages/taquito/src/wallet/wallet.ts b/packages/taquito/src/wallet/wallet.ts index f35fa37da6..11ed4826e7 100644 --- a/packages/taquito/src/wallet/wallet.ts +++ b/packages/taquito/src/wallet/wallet.ts @@ -16,11 +16,16 @@ import { WalletOriginateParams, WalletProvider, WalletTransferParams, + WalletStakeParams, + WalletUnstakeParams, + WalletFinalizeUnstakeParams, } from './interface'; import { InvalidAddressError, InvalidContractAddressError, InvalidOperationKindError, + InvalidStakingAddressError, + InvalidFinalizeUnstakeAmountError, } from '@taquito/core'; import { validateAddress, @@ -351,11 +356,98 @@ export class Wallet { /** * - * @description + * @description Stake a given amount for the source address * - * @returns + * @returns An operation handle with the result from the rpc node + * + * @param Stake pseudo-operation parameter + */ + stake(params: WalletStakeParams) { + return this.walletCommand(async () => { + const mappedParams = await this.walletProvider.mapStakeParamsToWalletParams(async () => { + const source = await this.pkh(); + if (!params.to) { + params.to = source; + } + if (params.to !== source) { + throw new InvalidStakingAddressError(params.to); + } + params.parameter = { entrypoint: 'stake', value: { prim: 'Unit' } }; + return params; + }); + const opHash = await this.walletProvider.sendOperations([mappedParams]); + return this.context.operationFactory.createTransactionOperation(opHash); + }); + } + + /** + * + * @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 + */ + unstake(params: WalletUnstakeParams) { + return this.walletCommand(async () => { + const mappedParams = await this.walletProvider.mapUnstakeParamsToWalletParams(async () => { + const source = await this.pkh(); + if (!params.to) { + params.to = source; + } + if (params.to !== source) { + throw new InvalidStakingAddressError(params.to); + } + params.parameter = { entrypoint: 'unstake', value: { prim: 'Unit' } }; + return params; + }); + const opHash = await this.walletProvider.sendOperations([mappedParams]); + return await this.context.operationFactory.createTransactionOperation(opHash); + }); + } + + /** + * + * @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 params + * @param Finalize_unstake pseudo-operation parameter + */ + finalizeUnstake(params: WalletFinalizeUnstakeParams) { + return this.walletCommand(async () => { + const mappedParams = await this.walletProvider.mapFinalizeUnstakeParamsToWalletParams( + async () => { + const source = await this.pkh(); + if (!params.to) { + params.to = source; + } + if (params.to !== source) { + throw new InvalidStakingAddressError(params.to); + } + if (!params.amount) { + params.amount = 0; + } + if (params.amount !== 0) { + throw new InvalidFinalizeUnstakeAmountError('Amount must be 0 to finalize unstake.'); + } + params.parameter = { entrypoint: 'finalize_unstake', value: { prim: 'Unit' } }; + return params; + } + ); + const opHash = await this.walletProvider.sendOperations([mappedParams]); + return await this.context.operationFactory.createTransactionOperation(opHash); + }); + } + + /** + * + * @description Increase the paid storage of a smart contract. + * + * @returns A wallet command from which we can send the operation to the wallet + * + * @param params operation parameter */ increasePaidStorage(params: WalletIncreasePaidStorageParams) { const destinationValidation = validateAddress(params.destination);