// Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 import type { Transaction, TransactionObjectArgument } from '@mysten/sui/transactions'; import * as royaltyRule from '../contracts/kiosk/royalty_rule.js'; import * as kioskLockRule from '../contracts/kiosk/kiosk_lock_rule.js'; import * as personalKioskRule from '../contracts/kiosk/personal_kiosk_rule.js'; import * as floorPriceRule from '../contracts/kiosk/floor_price_rule.js'; import * as transferPolicyContract from '../contracts/0x2/transfer_policy.js'; import { createTransferPolicy, withdrawFromPolicy } from '../tx/transfer-policy.js'; import type { ObjectArgument, TransferPolicyCap } from '../types/index.js'; import type { KioskClient } from './kiosk-client.js'; export type TransferPolicyBaseParams = { type: string; publisher: ObjectArgument; skipCheck?: boolean; }; export type TransferPolicyTransactionParams = { kioskClient: KioskClient; transaction: Transaction; cap?: TransferPolicyCap; }; export class TransferPolicyTransaction { transaction: Transaction; kioskClient: KioskClient; policy?: ObjectArgument; policyCap?: ObjectArgument; type?: string; constructor({ kioskClient, transaction, cap }: TransferPolicyTransactionParams) { this.kioskClient = kioskClient; this.transaction = transaction; if (cap) this.setCap(cap); } /** * A function to create a new transfer policy. * Checks if there's already an existing transfer policy to prevent * double transfer polciy mistakes. * There's an optional `skipCheck` flag that will just create the policy * without checking * * @param type The Type (`T`) for which we're creating the transfer policy. * @param publisher The Publisher Object Id. * @param address Address to save the `TransferPolicyCap` object to. * @param skipCheck (Optional) skip checking if a transfer policy already exists */ async createAndShare({ type, publisher, address, skipCheck, }: TransferPolicyBaseParams & { address: string; }) { if (!skipCheck) { const policies = await this.kioskClient.getTransferPolicies({ type }); if (policies.length > 0) throw new Error("There's already transfer policy for this Type."); } const cap = createTransferPolicy(this.transaction, type, publisher); this.transaction.transferObjects([cap], address); } /** * A convenient function to create a Transfer Policy and attach some rules * before sharing it (so you can prepare it in a single PTB) * @param type The Type (`T`) for which we're creating the transfer policy. * @param publisher The Publisher Object Id. * @param address Address to save the `TransferPolicyCap` object to. * @param skipCheck (Optional) skip checking if a transfer policy already exists */ async create({ type, publisher, skipCheck, }: TransferPolicyBaseParams): Promise { if (!skipCheck) { const policies = await this.kioskClient.getTransferPolicies({ type }); if (policies.length > 0) throw new Error("There's already transfer policy for this Type."); } const publisherArg = typeof publisher === 'string' ? this.transaction.object(publisher) : publisher; const [policy, policyCap] = this.transaction.add( transferPolicyContract._new({ arguments: [publisherArg], typeArguments: [type], }), ); this.#setup(policy, policyCap, type); // sets the client's TP to the newly created one. return this; } /** * This can be called after calling the `create` function to share the `TransferPolicy`, * and transfer the `TransferPolicyCap` to the specified address * * @param address The address to transfer the `TransferPolicyCap` */ shareAndTransferCap(address: string) { if (!this.type || !this.policyCap || !this.policy) throw new Error('This function can only be called after `transferPolicyManager.create`'); this.transaction.moveCall({ target: '0x2::transfer::public_share_object', arguments: [ typeof this.policy === 'string' ? this.transaction.object(this.policy) : this.policy, ], typeArguments: [`0x2::transfer_policy::TransferPolicy<${this.type}>`], }); this.transaction.transferObjects( [ typeof this.policyCap === 'string' ? this.transaction.object(this.policyCap) : this.policyCap, ], address, ); } /** * Setup the TransferPolicy by passing a `cap` returned from `kioskClient.getOwnedTransferPolicies` or * `kioskClient.getOwnedTransferPoliciesByType`. * @param policyCapId The `TransferPolicyCap` */ setCap({ policyId, policyCapId, type }: TransferPolicyCap) { return this.#setup(policyId, policyCapId, type); } /** * Withdraw from the transfer policy's profits. * @param address Address to transfer the profits to. * @param amount (Optional) amount parameter. Will withdraw all profits if the amount is not specified. */ withdraw(address: string, amount?: string | bigint) { this.#validateInputs(); // Withdraw coin for specified amount (or none) const coin = withdrawFromPolicy( this.transaction, this.type!, this.policy!, this.policyCap!, amount, ); this.transaction.transferObjects([coin], address); return this; } /** * Adds the Kiosk Royalty rule to the Transfer Policy. * You can pass the percentage, as well as a minimum amount. * The royalty that will be paid is the MAX(percentage, minAmount). * You can pass 0 in either value if you want only percentage royalty, or a fixed amount fee. * (but you should define at least one of them for the rule to make sense). * * @param percentageBps The royalty percentage in basis points. Use `percentageToBasisPoints` helper to convert from percentage [0,100]. * @param minAmount The minimum royalty amount per request in MIST. */ addRoyaltyRule( percentageBps: number | string, // this is in basis points. minAmount: number | string, ) { this.#validateInputs(); const policyArg = typeof this.policy === 'string' ? this.transaction.object(this.policy) : (this.policy as TransactionObjectArgument); const policyCapArg = typeof this.policyCap === 'string' ? this.transaction.object(this.policyCap) : (this.policyCap as TransactionObjectArgument); const packageId = this.kioskClient.getRulePackageId('royaltyRulePackageId'); this.transaction.add( royaltyRule.add({ package: packageId, arguments: { policy: policyArg, cap: policyCapArg, amountBp: Number(percentageBps), minAmount: typeof minAmount === 'string' ? BigInt(minAmount) : BigInt(minAmount), }, typeArguments: [this.type!], }), ); return this; } /** * Adds the Kiosk Lock Rule to the Transfer Policy. * This Rule forces buyer to lock the item in the kiosk, preserving strong royalties. */ addLockRule() { this.#validateInputs(); const policyArg = typeof this.policy === 'string' ? this.transaction.object(this.policy) : (this.policy as TransactionObjectArgument); const policyCapArg = typeof this.policyCap === 'string' ? this.transaction.object(this.policyCap) : (this.policyCap as TransactionObjectArgument); const packageId = this.kioskClient.getRulePackageId('kioskLockRulePackageId'); this.transaction.add( kioskLockRule.add({ package: packageId, arguments: { policy: policyArg, cap: policyCapArg, }, typeArguments: [this.type!], }), ); return this; } /** * Attaches the Personal Kiosk Rule, making a purchase valid only for `SoulBound` kiosks. */ addPersonalKioskRule() { this.#validateInputs(); const policyArg = typeof this.policy === 'string' ? this.transaction.object(this.policy) : (this.policy as TransactionObjectArgument); const policyCapArg = typeof this.policyCap === 'string' ? this.transaction.object(this.policyCap) : (this.policyCap as TransactionObjectArgument); const packageId = this.kioskClient.getRulePackageId('personalKioskRulePackageId'); this.transaction.add( personalKioskRule.add({ package: packageId, arguments: { policy: policyArg, cap: policyCapArg, }, typeArguments: [this.type!], }), ); return this; } /** * A function to add the floor price rule to a transfer policy. * @param minPrice The minimum price in MIST. */ addFloorPriceRule(minPrice: string | bigint) { this.#validateInputs(); const policyArg = typeof this.policy === 'string' ? this.transaction.object(this.policy) : (this.policy as TransactionObjectArgument); const policyCapArg = typeof this.policyCap === 'string' ? this.transaction.object(this.policyCap) : (this.policyCap as TransactionObjectArgument); const packageId = this.kioskClient.getRulePackageId('floorPriceRulePackageId'); this.transaction.add( floorPriceRule.add({ package: packageId, arguments: { policy: policyArg, cap: policyCapArg, floorPrice: typeof minPrice === 'string' ? BigInt(minPrice) : minPrice, }, typeArguments: [this.type!], }), ); return this; } /** * Generic helper to remove a rule, not from the SDK's base ruleset. * @param ruleType The Rule Type * @param configType The Config Type */ removeRule({ ruleType, configType }: { ruleType: string; configType: string }) { this.#validateInputs(); this.transaction.add( transferPolicyContract.removeRule({ arguments: [this.policy!, this.policyCap!], typeArguments: [this.type!, ruleType, configType], }), ); } /** * Removes the lock rule. */ removeLockRule() { this.#validateInputs(); const packageId = this.kioskClient.getRulePackageId('kioskLockRulePackageId'); this.transaction.add( transferPolicyContract.removeRule({ arguments: [this.policy!, this.policyCap!], typeArguments: [ this.type!, `${packageId}::kiosk_lock_rule::Rule`, `${packageId}::kiosk_lock_rule::Config`, ], }), ); return this; } /** * Removes the Royalty rule */ removeRoyaltyRule() { this.#validateInputs(); const packageId = this.kioskClient.getRulePackageId('royaltyRulePackageId'); this.transaction.add( transferPolicyContract.removeRule({ arguments: [this.policy!, this.policyCap!], typeArguments: [ this.type!, `${packageId}::royalty_rule::Rule`, `${packageId}::royalty_rule::Config`, ], }), ); return this; } removePersonalKioskRule() { this.#validateInputs(); const packageId = this.kioskClient.getRulePackageId('personalKioskRulePackageId'); this.transaction.add( transferPolicyContract.removeRule({ arguments: [this.policy!, this.policyCap!], typeArguments: [this.type!, `${packageId}::personal_kiosk_rule::Rule`, `bool`], }), ); return this; } removeFloorPriceRule() { this.#validateInputs(); const packageId = this.kioskClient.getRulePackageId('floorPriceRulePackageId'); this.transaction.add( transferPolicyContract.removeRule({ arguments: [this.policy!, this.policyCap!], typeArguments: [ this.type!, `${packageId}::floor_price_rule::Rule`, `${packageId}::floor_price_rule::Config`, ], }), ); return this; } getPolicy() { if (!this.policy) throw new Error('Policy not set.'); return this.policy; } getPolicyCap() { if (!this.policyCap) throw new Error('Transfer Policy Cap not set.'); return this.policyCap; } // Internal function that that the policy's Id + Cap + type have been set. #validateInputs() { const genericErrorMessage = `Please use 'setCap()' to setup the TransferPolicy.`; if (!this.policy) throw new Error(`${genericErrorMessage} Missing: Transfer Policy Object.`); if (!this.policyCap) throw new Error(`${genericErrorMessage} Missing: TransferPolicyCap Object ID`); if (!this.type) throw new Error( `${genericErrorMessage} Missing: Transfer Policy object type (e.g. {packageId}::item::Item)`, ); } /** * Setup the state of the TransferPolicyTransaction. */ #setup(policyId: ObjectArgument, policyCap: ObjectArgument, type: string) { this.policy = policyId; this.policyCap = policyCap; this.type = type; return this; } }