Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Kiosk SDK] Support kiosk_lock_rule and environments #12287

Merged
merged 12 commits into from
Jun 2, 2023
Merged
5 changes: 5 additions & 0 deletions .changeset/nervous-eels-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/kiosk": minor
---

Support kiosk_lock_rule and environment support for rules package. Breaks `purchaseAndResolvePolicies` as it changes signature and return format.
4 changes: 2 additions & 2 deletions sdk/kiosk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

- c322a230da: Fix fetchKiosk consistency/naming, include locked state in items

## 1.0.0
## 0.1.0

### Major Changes
### Minor Changes

- 4ea96d909a: Kiosk SDK for managing, querying and interacting with Kiosk and TransferPolicy objects

Expand Down
36 changes: 27 additions & 9 deletions sdk/kiosk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ const getKiosk = async () => {
</details>

<details>
<summary>Purchasing an item (currently supports royalty rule deployed on testnet or no rules)</summary>
<summary>Purchasing an item (currently supports royalty rule, kiosk_lock_rule, no rules, (combination works too))</summary>

```typescript
import { fetchKiosk } from '@mysten/kiosk';
import { queryTransferPolicy, purchaseAndResolvePolicies, place, testnetEnvironment } from '@mysten/kiosk';
import { Connection, JsonRpcProvider } from '@mysten/sui.js';

const provider = new JsonRpcProvider(
Expand All @@ -76,6 +76,9 @@ const item = {
price: "20000000000" // in MIST
}
}
const ownedKiosk = `0xMyKioskAddress`;
const ownedKioskCap = `0xMyKioskOwnerCap`;

const purchaseItem = async (item, kioskId) => {

// fetch the policy of the item (could be an array, if there's more than one transfer policy)
Expand All @@ -85,14 +88,25 @@ const purchaseItem = async (item, kioskId) => {
// initialize tx block.
const tx = new TransactionBlock();

// Purchases the item. Right now it also resolves a royalty rule, if one exists.
// There will be some additional work to support further rules & custom ones soon.
const purchasedItem = purchaseAndResolvePolicies(tx, item.type, item.listing, kioskId, item.objectId, policy[0]);
// Both are required if you there is a `kiosk_lock_rule`.
// Optional otherwise. Function will throw an error if there's a kiosk_lock_rule and these are missing.
const extraParams = {
ownedKiosk,
ownedKioskCap
}
// Define the environment.
// To use a custom package address for rules, you could call:
// const environment = customEnvironment('<PackageAddress>');
const environment = testnetEnvironment;

// now we need to decide what to do with the item
// ... e.g. place() // places the item to the user's kiosk.
// (NOT YET SUPPORTED BUT WORTH MENTIONING if the item has the `kiosk_lock` rule, the resolver will place it in the kiosk automatically.
// For now, to support this rule, we need to manually resolve the `kiosk_lock` rule and place it in our owned kiosk.)
// Extra params. Optional, but required if the user tries to resolve a `kiosk_lock_rule`.
// Purchases the item. Supports `kiosk_lock_rule`, `royalty_rule` (accepts combination too).
const result = purchaseAndResolvePolicies(tx, item.type, item.listing.price, kioskId, item.objectId, policy[0], environment, extraParams);

// result = {item: <the_purchased_item>, canTransfer: true/false // depending on whether there was a kiosk lock rule }
// if the item didn't have a kiosk_lock_rule, we need to do something with it.
// for e..g place it in our own kiosk. (demonstrated below)
if(result.canTransfer) place(tx, item.type, ownedKiosk, ownedKioskCap , result.item);

// ...finally, sign PTB & execute it.

Expand Down Expand Up @@ -158,12 +172,16 @@ import { TransactionBlock } from '@mysten/sui.js';
const withdraw = async () => {
const kiosk = 'SomeKioskId';
const kioskCap = 'KioskCapObjectId';
const address = '0xSomeAddressThatReceivesTheFunds';
const amount = '100000';

const tx = new TransactionBlock();

withdrawFromKiosk(tx, kiosk, kioskCap, amount);

// transfer the Coin to self or any other address.
tx.transferObjects([coin], tx.pure(address, 'address'));

// ... continue to sign and execute the transaction
// ...
};
Expand Down
51 changes: 11 additions & 40 deletions sdk/kiosk/src/bcs.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,37 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { ObjectOwner, bcs } from '@mysten/sui.js';

/**
* The Kiosk object fields (for BCS queries).
*/
export type Kiosk = {
id: string;
profits: string;
owner: string;
itemCount: number;
allowExtensions: boolean;
};
import { bcs } from '@mysten/sui.js';
import {
KIOSK_PURCHASE_CAP,
KIOSK_TYPE,
TRANSFER_POLICY_CREATED_EVENT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we had a function to create generic events of kind: transferPolicyCreated(type): 0x2:....::Created<${type}>. Wonder what you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@damirka Could you elaborate a bit on that?

TRANSFER_POLICY_TYPE,
} from './types';

// Register the `Kiosk` struct for faster queries.
bcs.registerStructType('0x2::kiosk::Kiosk', {
bcs.registerStructType(KIOSK_TYPE, {
id: 'address',
profits: 'u64',
owner: 'address',
itemCount: 'u32',
allowExtensions: 'bool',
});

/**
* PurchaseCap object fields (for BCS queries).
*/
export type PurchaseCap = {
id: string;
kioskId: string;
itemId: string;
minPrice: string;
};

// Register the `PurchaseCap` for faster queries.
bcs.registerStructType('0x2::kiosk::PurchaseCap', {
bcs.registerStructType(KIOSK_PURCHASE_CAP, {
id: 'address',
kioskId: 'address',
itemId: 'address',
minPrice: 'u64',
});

/** Event emitted when a TransferPolicy is created. */
export type TransferPolicyCreated = {
id: string;
};

// Register the `TransferPolicyCreated` event data.
bcs.registerStructType('0x2::transfer_policy::TransferPolicyCreated', {
bcs.registerStructType(TRANSFER_POLICY_CREATED_EVENT, {
id: 'address',
});

/** The `TransferPolicy` object */
export type TransferPolicy = {
id: string;
type: string;
balance: string;
rules: string[];
owner: ObjectOwner;
};

bcs.registerStructType('0x2::transfer_policy::TransferPolicy', {
bcs.registerStructType(TRANSFER_POLICY_TYPE, {
id: 'address',
balance: 'u64',
rules: ['vector', 'string'],
Expand Down
10 changes: 10 additions & 0 deletions sdk/kiosk/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/** The Transer Policy Rules package address on testnet */
export const TESTNET_RULES_PACKAGE_ADDRESS =
'bd8fc1947cf119350184107a3087e2dc27efefa0dd82e25a1f699069fe81a585';

/** The transfer policy rules package address on mainnet */
export const MAINNET_RULES_PACKAGE_ADDRESS =
'0x434b5bd8f6a7b05fede0ff46c6e511d71ea326ed38056e3bcd681d2d7c2a7879';
2 changes: 2 additions & 0 deletions sdk/kiosk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from './query/kiosk';
export * from './bcs';
export * from './utils';
export * from './query/transfer-policy';
export * from './types';
export * from './constants';
2 changes: 1 addition & 1 deletion sdk/kiosk/src/query/kiosk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
extractKioskData,
getKioskObject,
} from '../utils';
import { Kiosk } from '../bcs';
import { Kiosk } from '../types';

/**
* A dynamic field `Listing { ID, isExclusive }` attached to the Kiosk.
Expand Down
18 changes: 8 additions & 10 deletions sdk/kiosk/src/query/transfer-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { JsonRpcProvider } from '@mysten/sui.js';
import { TransferPolicy, bcs } from '../bcs';

/** Name of the event emitted when a TransferPolicy for T is created. */
export const TRANSFER_POLICY_CREATED_EVENT = `0x2::transfer_policy::TransferPolicyCreated`;
import { bcs } from '../bcs';
import {
TRANSFER_POLICY_CREATED_EVENT,
TRANSFER_POLICY_TYPE,
TransferPolicy,
} from '../types';

/**
* Searches the `TransferPolicy`-s for the given type. The seach is performed via
Expand Down Expand Up @@ -44,15 +46,11 @@ export async function queryTransferPolicy(
);
}

let parsed = bcs.de(
'0x2::transfer_policy::TransferPolicy',
policy.bcs.bcsBytes,
'base64',
);
let parsed = bcs.de(TRANSFER_POLICY_TYPE, policy.bcs.bcsBytes, 'base64');

return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we could help provide initialSharedVersion for shared transfer policy. That could help build arguments for the purchase flow.

id: policy?.objectId,
type: `0x2::transfer_policy::TransferPolicy<${type}>`,
type: `${TRANSFER_POLICY_TYPE}<${type}>`,
owner: policy?.owner!,
rules: parsed.rules,
balance: parsed.balance,
Expand Down
93 changes: 65 additions & 28 deletions sdk/kiosk/src/tx/kiosk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ import {
TransactionBlock,
} from '@mysten/sui.js';

import { ObjectArgument, getTypeWithoutPackageAddress, objArg } from '../utils';
import { KioskListing } from '../query/kiosk';
import { TransferPolicy } from '../bcs';
import { confirmRequest, resolveRoyaltyRule } from './transfer-policy';

/** The Kiosk module. */
export const KIOSK_MODULE = '0x2::kiosk';

/** The Kiosk type. */
export const KIOSK_TYPE = `${KIOSK_MODULE}::Kiosk`;

/** The Kiosk Owner Cap Type */
export const KIOSK_OWNER_CAP = `${KIOSK_MODULE}::KioskOwnerCap`;
import { getTypeWithoutPackageAddress, objArg } from '../utils';
import {
confirmRequest,
resolveKioskLockRule,
resolveRoyaltyRule,
} from './transfer-policy';
import {
KIOSK_LOCK_RULE,
KIOSK_MODULE,
KIOSK_TYPE,
ObjectArgument,
PurchaseAndResolvePoliciesResponse,
PurchaseOptionalParams,
ROYALTY_RULE,
RulesEnvironmentParam,
TransferPolicy,
} from '../types';

/**
* Create a new shared Kiosk and returns the [kiosk, kioskOwnerCap] tuple.
Expand Down Expand Up @@ -228,7 +232,12 @@ export function withdrawFromKiosk(
): TransactionArgument {
let amountArg =
amount !== null
? tx.pure(amount, 'Option<u64>')
? tx.pure(
{
Some: amount,
},
'Option<u64>',
)
: tx.pure({ None: true }, 'Option<u64>');

let [coin] = tx.moveCall({
Expand Down Expand Up @@ -335,46 +344,71 @@ export function returnValue(
* Completes the full purchase flow that includes:
* 1. Purchasing the item.
* 2. Resolving all the transfer policies (if any).
* 3. Returns the PurchasedItem OR places the item in the user's kiosk (if there's a kiosk lock policy).
* 3. Returns the item and whether the user can transfer it or not.
*
* If the item can be transferred, there's an extra transaction required by the user
* to transfer it to an address or place it in their kiosk.
*/
export function purchaseAndResolvePolicies(
tx: TransactionBlock,
itemType: string,
listing: KioskListing,
kioskId: string,
itemId: string,
price: string,
kiosk: ObjectArgument,
itemId: SuiAddress,
policy: TransferPolicy,
): TransactionArgument | null {
environment: RulesEnvironmentParam,
extraParams?: PurchaseOptionalParams,
): PurchaseAndResolvePoliciesResponse {
// if we don't pass the listing or the listing doens't have a price, return.
if (!listing || listing?.price === undefined) return null;
if (price === undefined || typeof price !== 'string')
throw new Error(`Price of the listing is not supplied.`);

// Split the coin for the amount of the listing.
const coin = tx.splitCoins(tx.gas, [tx.pure(listing.price)]);
const coin = tx.splitCoins(tx.gas, [tx.pure(price, 'u64')]);

// initialize the purchase `kiosk::purchase`
const [purchasedItem, transferRequest] = purchase(
tx,
itemType,
kioskId,
kiosk,
itemId,
coin,
);

// Start resolving rules.
// For now, we only support royalty rule.
// Will need some tweaking to make it function properly with the other
// ruleset.
// Right now we support `kiosk_lock_rule` and `royalty_rule`.
// They can also be supplied in parallel (supports combination).
let hasKioskLockRule = false;

for (let rule of policy.rules) {
const ruleWithoutAddr = getTypeWithoutPackageAddress(rule);

switch (ruleWithoutAddr) {
case 'royalty_rule::Rule':
case ROYALTY_RULE:
resolveRoyaltyRule(
tx,
itemType,
listing.price,
price,
policy.id,
transferRequest,
environment,
);
break;
case KIOSK_LOCK_RULE:
if (!extraParams?.ownedKiosk || !extraParams?.ownedKioskCap)
throw new Error(
`This item type ${itemType} has a 'kiosk_lock_rule', but function call is missing user's kiosk and kioskCap params`,
);
hasKioskLockRule = true;
resolveKioskLockRule(
tx,
itemType,
purchasedItem,
extraParams.ownedKiosk,
extraParams.ownedKioskCap,
policy.id,
transferRequest,
environment,
);
break;
default:
Expand All @@ -385,5 +419,8 @@ export function purchaseAndResolvePolicies(
// confirm the Transfer Policy request.
confirmRequest(tx, itemType, policy.id, transferRequest);

return purchasedItem;
return {
item: purchasedItem,
canTransfer: !hasKioskLockRule,
};
}