diff --git a/integration-tests/__tests__/wallet/transfer-ticket-operation.spec.ts b/integration-tests/__tests__/wallet/transfer-ticket-operation.spec.ts new file mode 100644 index 0000000000..f1f0fbd7a5 --- /dev/null +++ b/integration-tests/__tests__/wallet/transfer-ticket-operation.spec.ts @@ -0,0 +1,76 @@ +import { CONFIGS } from '../../config'; +import { DefaultContractType, TezosToolkit } from '@taquito/taquito'; +import { ticketsSendTz } from '../../data/code_with_ticket_transfer'; +import { RpcClient, TicketTokenParams } from '@taquito/rpc'; + +CONFIGS().forEach(({ lib, rpc, setup, createAddress }) => { + const Tezos = lib; + const client = new RpcClient(rpc); + + let ticketSendContract: DefaultContractType; + let recipient: TezosToolkit; + let sender: TezosToolkit; + let recipientPkh: string; + let senderPkh: string + let ticketToken: TicketTokenParams; + + describe(`Transfer tickets between implicit accounts using: ${rpc}`, () => { + + beforeAll(async () => { + await setup(true); + try { + recipient = await createAddress(); + sender = await createAddress(); + + recipientPkh = await recipient.signer.publicKeyHash(); + senderPkh = await sender.wallet.pkh(); + + const fundSender = await Tezos.contract.transfer({ to: senderPkh, amount: 5 }); + await fundSender.confirmation(); + + const ticketSendOrigination = await Tezos.contract.originate({ code: ticketsSendTz, storage: null }); + await ticketSendOrigination.confirmation(); + + ticketSendContract = await ticketSendOrigination.contract(); + ticketToken = { ticketer: ticketSendContract.address, content_type: { prim: 'string' }, content: { string: 'Ticket' } }; + + // Send 3 tickets from the originated contract to sender + const sendTickets = await ticketSendContract.methodsObject.default([senderPkh, '3']).send() + await sendTickets.confirmation(); + + } catch (error) { + console.log(error); + } + }); + + it('should transfer 1 ticket from an implicit account to another implicit account using a Wallet', async () => { + // Check balances before transferring tickets + const balanceBefore = await client.getTicketBalance(recipientPkh, ticketToken); + expect(balanceBefore).toEqual('0'); + + const senderBalanceBefore = await client.getTicketBalance(senderPkh, ticketToken); + expect(senderBalanceBefore).toEqual('3'); + + // Transfer 1 ticket from sender to recipient + const transferTicketOp = await sender.wallet.transferTicket({ + ticketContents: { string: "Ticket" }, + ticketTy: { prim: "string" }, + ticketTicketer: ticketSendContract.address, + ticketAmount: 1, + destination: recipientPkh, + entrypoint: 'default', + }).send(); + + await transferTicketOp.confirmation(); + + expect(await transferTicketOp.status()).toEqual('applied'); + + // Check balances after transferring tickets + const balanceAfter = await client.getTicketBalance(recipientPkh, ticketToken); + expect(balanceAfter).toEqual('1'); + + const senderBalanceAfter = await client.getTicketBalance(senderPkh, ticketToken); + expect(senderBalanceAfter).toEqual('2'); + }); + }); +}); \ No newline at end of file diff --git a/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts b/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts index 1bc77474a4..fcf165e4f0 100644 --- a/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts +++ b/packages/taquito-beacon-wallet/src/taquito-beacon-wallet.ts @@ -28,6 +28,8 @@ import { WalletStakeParams, WalletUnstakeParams, WalletFinalizeUnstakeParams, + WalletTransferTicketParams, + createTransferTicketOperation, } from '@taquito/taquito'; import { buf2hex, hex2buf, mergebuf } from '@taquito/utils'; import { UnsupportedActionError } from '@taquito/core'; @@ -97,6 +99,21 @@ export class BeaconWallet implements WalletProvider { ); } + async mapTransferTicketParamsToWalletParams(params: () => Promise) { + let walletParams: WalletTransferTicketParams; + await this.client.showPrepare(); + try { + walletParams = await params(); + } catch (err) { + await this.client.hideUI(['alert']); + throw err; + } + return this.removeDefaultParams( + walletParams, + await createTransferTicketOperation(this.formatParameters(walletParams)) + ); + } + async mapStakeParamsToWalletParams(params: () => Promise) { let walletParams: WalletStakeParams; await this.client.showPrepare(); diff --git a/packages/taquito/src/wallet/index.ts b/packages/taquito/src/wallet/index.ts index fd4fcb197b..19faefc439 100644 --- a/packages/taquito/src/wallet/index.ts +++ b/packages/taquito/src/wallet/index.ts @@ -3,5 +3,6 @@ export * from './operation'; export * from './transaction-operation'; export * from './origination-operation'; export * from './delegation-operation'; +export * from './transfer-ticket-operation'; export * from './interface'; export * from './legacy'; diff --git a/packages/taquito/src/wallet/interface.ts b/packages/taquito/src/wallet/interface.ts index 20e10fa560..033445d18d 100644 --- a/packages/taquito/src/wallet/interface.ts +++ b/packages/taquito/src/wallet/interface.ts @@ -7,6 +7,7 @@ import { StakeParams, UnstakeParams, FinalizeUnstakeParams, + TransferTicketParams, } from '../operations/types'; export type WalletDefinedFields = 'source'; @@ -30,6 +31,8 @@ export type WalletFailingNoopParams = Omit; +export type WalletTransferTicketParams = Omit; + export interface WalletProvider { /** * @description Request the public key hash from the wallet @@ -46,6 +49,13 @@ export interface WalletProvider { */ mapTransferParamsToWalletParams: (params: () => Promise) => Promise; + /** + * @description Transform WalletTransferTicketParams into a format compliant with the underlying wallet + */ + mapTransferTicketParamsToWalletParams: ( + params: () => Promise + ) => Promise; + /** * @description Transform WalletStakeParams 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 7e84c26f43..bd985fa1bb 100644 --- a/packages/taquito/src/wallet/legacy.ts +++ b/packages/taquito/src/wallet/legacy.ts @@ -9,6 +9,7 @@ import { WalletStakeParams, WalletUnstakeParams, WalletFinalizeUnstakeParams, + WalletTransferTicketParams, } from './interface'; import { WalletParamsWithKind } from './wallet'; @@ -51,6 +52,10 @@ export class LegacyWalletProvider implements WalletProvider { return attachKind(await params(), OpKind.INCREASE_PAID_STORAGE); } + async mapTransferTicketParamsToWalletParams(params: () => Promise) { + return attachKind(await params(), OpKind.TRANSFER_TICKET); + } + async sendOperations(params: WalletParamsWithKind[]) { const op = await this.context.batch.batch(params as any).send(); return op.hash; diff --git a/packages/taquito/src/wallet/operation-factory.ts b/packages/taquito/src/wallet/operation-factory.ts index 16c38a6018..5f7677a110 100644 --- a/packages/taquito/src/wallet/operation-factory.ts +++ b/packages/taquito/src/wallet/operation-factory.ts @@ -21,6 +21,7 @@ import { IncreasePaidStorageWalletOperation } from './increase-paid-storage-oper import { WalletOperation } from './operation'; import { OriginationWalletOperation } from './origination-operation'; import { TransactionWalletOperation } from './transaction-operation'; +import { TransferTicketWalletOperation } from './transfer-ticket-operation'; import { ConfirmationTimeoutError } from '../errors'; export function timeoutAfter(timeoutMillisec: number): (source: Observable) => Observable { @@ -132,6 +133,17 @@ export class OperationFactory { ); } + async createTransferTicketOperation( + hash: string, + config: OperationFactoryConfig = {} + ): Promise { + return new TransferTicketWalletOperation( + hash, + this.context.clone(), + await this.createHeadObservableFromConfig(config) + ); + } + async createDelegationOperation( hash: string, config: OperationFactoryConfig = {} diff --git a/packages/taquito/src/wallet/transfer-ticket-operation.ts b/packages/taquito/src/wallet/transfer-ticket-operation.ts new file mode 100644 index 0000000000..ddbea838e3 --- /dev/null +++ b/packages/taquito/src/wallet/transfer-ticket-operation.ts @@ -0,0 +1,54 @@ +import { WalletOperation, OperationStatus } from './operation'; +import { Context } from '../context'; +import { Observable } from 'rxjs'; +import { + BlockResponse, + OpKind, + OperationContentsAndResultReveal, + OperationContentsAndResultTransferTicket, +} from '@taquito/rpc'; +import { ObservableError } from './errors'; + +export class TransferTicketWalletOperation extends WalletOperation { + constructor( + public readonly opHash: string, + protected readonly context: Context, + newHead$: Observable + ) { + super(opHash, context, newHead$); + } + + public async revealOperation() { + const operationResult = await this.operationResults(); + if (!operationResult) { + throw new ObservableError('operationResult returned undefined'); + } + + return operationResult.find((x) => x.kind === OpKind.REVEAL) as + | OperationContentsAndResultReveal + | undefined; + } + + public async transferTicketOperation() { + const operationResult = await this.operationResults(); + if (!operationResult) { + throw new ObservableError('operationResult returned undefined'); + } + return operationResult.find((x) => x.kind === OpKind.TRANSFER_TICKET) as + | OperationContentsAndResultTransferTicket + | undefined; + } + + public async status(): Promise { + if (!this._included) { + return 'pending'; + } + + const op = await this.transferTicketOperation(); + if (!op) { + return 'unknown'; + } + + return op.metadata.operation_result.status; + } +} diff --git a/packages/taquito/src/wallet/wallet.ts b/packages/taquito/src/wallet/wallet.ts index 5e92cf645b..2808ac59a0 100644 --- a/packages/taquito/src/wallet/wallet.ts +++ b/packages/taquito/src/wallet/wallet.ts @@ -19,6 +19,7 @@ import { WalletStakeParams, WalletUnstakeParams, WalletFinalizeUnstakeParams, + WalletTransferTicketParams, } from './interface'; import { InvalidAddressError, @@ -43,7 +44,8 @@ export type WalletParamsWithKind = | withKind | withKind | withKind - | withKind; + | withKind + | withKind; export class WalletOperationBatch { private operations: WalletParamsWithKind[] = []; @@ -115,6 +117,19 @@ export class WalletOperationBatch { return this; } + /** + * @description Add an TransferTicket operation to the batch + * @param param TransferTicket operation parameter + */ + withTransferTicket(params: WalletTransferTicketParams) { + const destinationValidation = validateAddress(params.destination); + if (destinationValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(params.destination, invalidDetail(destinationValidation)); + } + this.operations.push({ kind: OpKind.TRANSFER_TICKET, ...params }); + return this; + } + private async mapOperation(param: WalletParamsWithKind) { switch (param.kind) { case OpKind.TRANSACTION: @@ -323,6 +338,26 @@ export class Wallet { }); } + /** + * @description Transfer tezos tickets from current address to a specific address or a smart contract + * @returns a TransferTicketWalletOperation promise object when followed by .send() + * @param params operation parameter + */ + transferTicket(params: WalletTransferTicketParams) { + const toValidation = validateAddress(params.destination); + if (toValidation !== ValidationResult.VALID) { + throw new InvalidAddressError(params.destination, invalidDetail(toValidation)); + } + return this.walletCommand(async () => { + const mappedParams = await this.walletProvider.mapTransferTicketParamsToWalletParams( + async () => params + ); + + const opHash = await this.walletProvider.sendOperations([mappedParams]); + return this.context.operationFactory.createTransferTicketOperation(opHash); + }); + } + /** * @description Stake a given amount for the source address * @returns a TransactionWalletOperation promise object when followed by .send()