@mysten/suins
Version:
550 lines (507 loc) • 17.6 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { bcs } from '@mysten/sui/bcs';
import type {
Transaction,
TransactionObjectArgument,
TransactionObjectInput,
} from '@mysten/sui/transactions';
import { isValidSuiNSName, normalizeSuiNSName, SUI_CLOCK_OBJECT_ID } from '@mysten/sui/utils';
import { ALLOWED_METADATA, MAX_U64 } from './constants.js';
import { isNestedSubName, isSubName, zeroCoin } from './helpers.js';
import type { SuinsClient } from './suins-client.js';
import type { DiscountInfo, ReceiptParams, RegistrationParams, RenewalParams } from './types.js';
export class SuinsTransaction {
suinsClient: SuinsClient;
transaction: Transaction;
constructor(client: SuinsClient, transaction: Transaction) {
this.suinsClient = client;
this.transaction = transaction;
}
/**
* Registers a domain for a number of years.
*/
register(params: RegistrationParams): TransactionObjectArgument {
if (params.couponCode && params.discountInfo) {
throw new Error('Cannot apply both coupon and discount NFT');
}
const paymentIntent = this.initRegistration(params.domain);
if (params.couponCode) {
this.applyCoupon(paymentIntent, params.couponCode);
}
if (params.discountInfo) {
this.applyDiscount(paymentIntent, params.discountInfo);
}
const priceAfterDiscount = this.calculatePriceAfterDiscount(
paymentIntent,
params.coinConfig.type,
);
const receipt = this.generateReceipt({
paymentIntent,
priceAfterDiscount,
coinConfig: params.coinConfig,
coin: params.coin,
maxAmount: params.maxAmount,
priceInfoObjectId: params.priceInfoObjectId,
});
const nft = this.finalizeRegister(receipt);
if (params.years > 1) {
this.renew({
nft,
years: params.years - 1,
coinConfig: params.coinConfig,
coin: params.coin,
couponCode: params.couponCode,
discountInfo: params.discountInfo,
maxAmount: params.maxAmount,
priceInfoObjectId: params.priceInfoObjectId,
});
}
return nft as TransactionObjectArgument;
}
/**
* Renews an NFT for a number of years.
*/
renew(params: RenewalParams): void {
if (params.couponCode && params.discountInfo) {
throw new Error('Cannot apply both coupon and discount NFT');
}
const paymentIntent = this.initRenewal(params.nft, params.years);
if (params.couponCode) {
this.applyCoupon(paymentIntent, params.couponCode);
}
if (params.discountInfo) {
this.applyDiscount(paymentIntent, params.discountInfo);
}
const priceAfterDiscount = this.calculatePriceAfterDiscount(
paymentIntent,
params.coinConfig.type,
);
const receipt = this.generateReceipt({
paymentIntent,
priceAfterDiscount,
coinConfig: params.coinConfig,
coin: params.coin,
maxAmount: params.maxAmount,
priceInfoObjectId: params.priceInfoObjectId,
});
this.finalizeRenew(receipt, params.nft);
}
initRegistration(domain: string): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.packageId}::payment::init_registration`,
arguments: [this.transaction.object(config.suins), this.transaction.pure.string(domain)],
});
}
initRenewal(nft: TransactionObjectInput, years: number): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.packageId}::payment::init_renewal`,
arguments: [
this.transaction.object(config.suins),
this.transaction.object(nft),
this.transaction.pure.u8(years),
],
});
}
calculatePrice(
baseAmount: TransactionObjectArgument,
paymentType: string,
priceInfoObjectId: string,
): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.payments.packageId}::payments::calculate_price`,
arguments: [
this.transaction.object(config.suins),
baseAmount,
this.transaction.object.clock(),
this.transaction.object(priceInfoObjectId),
],
typeArguments: [paymentType],
});
}
handleBasePayment(
paymentIntent: TransactionObjectArgument,
payment: TransactionObjectArgument,
paymentType: string,
): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.payments.packageId}::payments::handle_base_payment`,
arguments: [this.transaction.object(config.suins), paymentIntent, payment],
typeArguments: [paymentType],
});
}
handlePayment(
paymentIntent: TransactionObjectArgument,
payment: TransactionObjectArgument,
paymentType: string,
priceInfoObjectId: string,
maxAmount: bigint = MAX_U64,
): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.payments.packageId}::payments::handle_payment`,
arguments: [
this.transaction.object(config.suins),
paymentIntent,
payment,
this.transaction.object.clock(),
this.transaction.object(priceInfoObjectId),
this.transaction.pure.u64(maxAmount),
],
typeArguments: [paymentType],
});
}
finalizeRegister(receipt: TransactionObjectArgument): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.packageId}::payment::register`,
arguments: [receipt, this.transaction.object(config.suins), this.transaction.object.clock()],
});
}
finalizeRenew(
receipt: TransactionObjectArgument,
nft: TransactionObjectInput,
): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.packageId}::payment::renew`,
arguments: [
receipt,
this.transaction.object(config.suins),
this.transaction.object(nft),
this.transaction.object.clock(),
],
});
}
calculatePriceAfterDiscount(
paymentIntent: TransactionObjectArgument,
paymentType: string,
): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.payments.packageId}::payments::calculate_price_after_discount`,
arguments: [this.transaction.object(config.suins), paymentIntent],
typeArguments: [paymentType],
});
}
generateReceipt(params: ReceiptParams): TransactionObjectArgument {
const baseAssetPurchase = params.coinConfig.feed === '';
if (baseAssetPurchase) {
const payment = params.coin
? this.transaction.splitCoins(this.transaction.object(params.coin), [
params.priceAfterDiscount,
])
: zeroCoin(this.transaction, params.coinConfig.type);
const receipt = this.handleBasePayment(params.paymentIntent, payment, params.coinConfig.type);
return receipt;
} else {
const priceInfoObjectId = params.priceInfoObjectId;
if (!priceInfoObjectId)
throw new Error('Price info object ID is required for non-base asset purchases');
const price = this.calculatePrice(
params.priceAfterDiscount,
params.coinConfig.type,
priceInfoObjectId,
);
if (!params.coin) throw new Error('coin input is required');
const payment = this.transaction.splitCoins(this.transaction.object(params.coin!), [price]);
const receipt = this.handlePayment(
params.paymentIntent,
payment,
params.coinConfig.type,
priceInfoObjectId,
params.maxAmount,
);
return receipt;
}
}
/**
* Applies a coupon to the payment intent.
*/
applyCoupon(intent: TransactionObjectArgument, couponCode: string): TransactionObjectArgument {
const config = this.suinsClient.config;
return this.transaction.moveCall({
target: `${config.coupons.packageId}::coupon_house::apply_coupon`,
arguments: [
this.transaction.object(config.suins),
intent,
this.transaction.pure.string(couponCode),
this.transaction.object.clock(),
],
});
}
/**
* Applies a discount to the payment intent.
*/
applyDiscount(intent: TransactionObjectArgument, discountInfo: DiscountInfo): void {
const config = this.suinsClient.config;
if (discountInfo.isFreeClaim) {
this.transaction.moveCall({
target: `${config.discountsPackage.packageId}::free_claims::free_claim`,
arguments: [
this.transaction.object(config.discountsPackage.discountHouseId),
this.transaction.object(config.suins),
intent,
this.transaction.object(discountInfo.discountNft),
],
typeArguments: [discountInfo.type],
});
} else {
this.transaction.moveCall({
target: `${config.discountsPackage.packageId}::discounts::apply_percentage_discount`,
arguments: [
this.transaction.object(config.discountsPackage.discountHouseId),
intent,
this.transaction.object(config.suins),
this.transaction.object(discountInfo.discountNft),
],
typeArguments: [discountInfo.type],
});
}
}
/**
* Creates a subdomain.
*/
createSubName({
parentNft,
name,
expirationTimestampMs,
allowChildCreation,
allowTimeExtension,
}: {
parentNft: TransactionObjectInput;
name: string;
expirationTimestampMs: number;
allowChildCreation: boolean;
allowTimeExtension: boolean;
}) {
if (!isValidSuiNSName(name)) throw new Error('Invalid SuiNS name');
const isParentSubdomain = isNestedSubName(name);
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
if (!this.suinsClient.config.subNamesPackageId)
throw new Error('Subnames package ID not found');
if (isParentSubdomain && !this.suinsClient.config.tempSubdomainsProxyPackageId)
throw new Error('Subnames proxy package ID not found');
const subNft = this.transaction.moveCall({
target: isParentSubdomain
? `${this.suinsClient.config.tempSubdomainsProxyPackageId}::subdomain_proxy::new`
: `${this.suinsClient.config.subNamesPackageId}::subdomains::new`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(parentNft),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
this.transaction.pure.string(normalizeSuiNSName(name, 'dot')),
this.transaction.pure.u64(expirationTimestampMs),
this.transaction.pure.bool(!!allowChildCreation),
this.transaction.pure.bool(!!allowTimeExtension),
],
});
return subNft;
}
/**
* Builds the PTB to create a leaf subdomain.
* Parent can be a `SuinsRegistration` or a `SubDomainRegistration` object.
* Can be passed in as an ID or a TransactionArgument.
*/
createLeafSubName({
parentNft,
name,
targetAddress,
}: {
parentNft: TransactionObjectInput;
name: string;
targetAddress: string;
}) {
if (!isValidSuiNSName(name)) throw new Error('Invalid SuiNS name');
const isParentSubdomain = isNestedSubName(name);
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
if (!this.suinsClient.config.subNamesPackageId)
throw new Error('Subnames package ID not found');
if (isParentSubdomain && !this.suinsClient.config.tempSubdomainsProxyPackageId)
throw new Error('Subnames proxy package ID not found');
this.transaction.moveCall({
target: isParentSubdomain
? `${this.suinsClient.config.tempSubdomainsProxyPackageId}::subdomain_proxy::new_leaf`
: `${this.suinsClient.config.subNamesPackageId}::subdomains::new_leaf`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(parentNft),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
this.transaction.pure.string(normalizeSuiNSName(name, 'dot')),
this.transaction.pure.address(targetAddress),
],
});
}
/**
* Removes a leaf subname.
*/
removeLeafSubName({ parentNft, name }: { parentNft: TransactionObjectInput; name: string }) {
if (!isValidSuiNSName(name)) throw new Error('Invalid SuiNS name');
const isParentSubdomain = isNestedSubName(name);
if (!isSubName(name)) throw new Error('This can only be invoked for subnames');
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
if (!this.suinsClient.config.subNamesPackageId)
throw new Error('Subnames package ID not found');
if (isParentSubdomain && !this.suinsClient.config.tempSubdomainsProxyPackageId)
throw new Error('Subnames proxy package ID not found');
this.transaction.moveCall({
target: isParentSubdomain
? `${this.suinsClient.config.tempSubdomainsProxyPackageId}::subdomain_proxy::remove_leaf`
: `${this.suinsClient.config.subNamesPackageId}::subdomains::remove_leaf`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(parentNft),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
this.transaction.pure.string(normalizeSuiNSName(name, 'dot')),
],
});
}
/**
* Sets the target address of an NFT.
*/
setTargetAddress({
nft, // Can be string or argument
address,
isSubname,
}: {
nft: TransactionObjectInput;
address?: string;
isSubname?: boolean;
}) {
if (isSubname && !this.suinsClient.config.tempSubdomainsProxyPackageId)
throw new Error('Subnames proxy package ID not found');
this.transaction.moveCall({
target: isSubname
? `${this.suinsClient.config.tempSubdomainsProxyPackageId}::subdomain_proxy::set_target_address`
: `${this.suinsClient.config.packageId}::controller::set_target_address`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(nft),
this.transaction.pure(bcs.option(bcs.Address).serialize(address).toBytes()),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
],
});
}
/**
* Sets a default name for the user.
*/
setDefault(name: string) {
if (!isValidSuiNSName(name)) throw new Error('Invalid SuiNS name');
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
this.transaction.moveCall({
target: `${this.suinsClient.config.packageId}::controller::set_reverse_lookup`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.pure.string(normalizeSuiNSName(name, 'dot')),
],
});
}
/**
* Edits the setup of a subname.
*/
editSetup({
parentNft,
name,
allowChildCreation,
allowTimeExtension,
}: {
parentNft: TransactionObjectInput;
name: string;
allowChildCreation: boolean;
allowTimeExtension: boolean;
}) {
if (!isValidSuiNSName(name)) throw new Error('Invalid SuiNS name');
const isParentSubdomain = isNestedSubName(name);
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
if (!isParentSubdomain && !this.suinsClient.config.subNamesPackageId)
throw new Error('Subnames package ID not found');
if (isParentSubdomain && !this.suinsClient.config.tempSubdomainsProxyPackageId)
throw new Error('Subnames proxy package ID not found');
this.transaction.moveCall({
target: isParentSubdomain
? `${this.suinsClient.config.tempSubdomainsProxyPackageId}::subdomain_proxy::edit_setup`
: `${this.suinsClient.config.subNamesPackageId}::subdomains::edit_setup`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(parentNft),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
this.transaction.pure.string(normalizeSuiNSName(name, 'dot')),
this.transaction.pure.bool(!!allowChildCreation),
this.transaction.pure.bool(!!allowTimeExtension),
],
});
}
/**
* Extends the expiration of a subname.
*/
extendExpiration({
nft,
expirationTimestampMs,
}: {
nft: TransactionObjectInput;
expirationTimestampMs: number;
}) {
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
if (!this.suinsClient.config.subNamesPackageId)
throw new Error('Subnames package ID not found');
this.transaction.moveCall({
target: `${this.suinsClient.config.subNamesPackageId}::subdomains::extend_expiration`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(nft),
this.transaction.pure.u64(expirationTimestampMs),
],
});
}
/**
* Sets the user data of an NFT.
*/
setUserData({
nft,
value,
key,
isSubname,
}: {
nft: TransactionObjectInput;
value: string;
key: string;
isSubname?: boolean;
}) {
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
if (isSubname && !this.suinsClient.config.tempSubdomainsProxyPackageId)
throw new Error('Subnames proxy package ID not found');
if (!Object.values(ALLOWED_METADATA).some((x) => x === key)) throw new Error('Invalid key');
this.transaction.moveCall({
target: isSubname
? `${this.suinsClient.config.tempSubdomainsProxyPackageId}::subdomain_proxy::set_user_data`
: `${this.suinsClient.config.packageId}::controller::set_user_data`,
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(nft),
this.transaction.pure.string(key),
this.transaction.pure.string(value),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
],
});
}
/**
* Burns an expired NFT to collect storage rebates.
*/
burnExpired({ nft, isSubname }: { nft: TransactionObjectInput; isSubname?: boolean }) {
if (!this.suinsClient.config.suins) throw new Error('SuiNS Object ID not found');
this.transaction.moveCall({
target: `${this.suinsClient.config.packageId}::controller::${
isSubname ? 'burn_expired_subname' : 'burn_expired'
}`, // Update this
arguments: [
this.transaction.object(this.suinsClient.config.suins),
this.transaction.object(nft),
this.transaction.object(SUI_CLOCK_OBJECT_ID),
],
});
}
}