From d1bf67acfd6b5aa96d0b7cce8534ad96dc5ac70d Mon Sep 17 00:00:00 2001 From: Manolis Liolios Date: Mon, 18 Nov 2024 16:57:53 +0200 Subject: [PATCH] Format core pkg (#204) --- packages/suins/sources/admin.move | 92 +- packages/suins/sources/auction.move | 848 ++++++++--------- packages/suins/sources/config.move | 230 ++--- packages/suins/sources/constants.move | 133 +-- packages/suins/sources/controller.move | 277 +++--- packages/suins/sources/domain.move | 553 ++++++------ packages/suins/sources/name_record.move | 195 ++-- packages/suins/sources/registry.move | 852 +++++++++--------- .../suins/sources/subdomain_registration.move | 108 ++- packages/suins/sources/suins.move | 442 ++++----- .../suins/sources/suins_registration.move | 257 +++--- 11 files changed, 2097 insertions(+), 1890 deletions(-) diff --git a/packages/suins/sources/admin.move b/packages/suins/sources/admin.move index 62fbd7e1..9830a69c 100644 --- a/packages/suins/sources/admin.move +++ b/packages/suins/sources/admin.move @@ -3,53 +3,57 @@ /// Admin features of the SuiNS application. Meant to be called directly /// by the suins admin. -module suins::admin { - use std::string::String; - use sui::{clock::Clock, tx_context::{sender}}; +module suins::admin; - use suins::{domain, config, suins::{Self, AdminCap, SuiNS}, suins_registration::SuinsRegistration, registry::Registry}; +use std::string::String; +use sui::clock::Clock; +use sui::tx_context::sender; +use suins::config; +use suins::domain; +use suins::registry::Registry; +use suins::suins::{Self, AdminCap, SuiNS}; +use suins::suins_registration::SuinsRegistration; - /// The authorization witness. - public struct Admin has drop {} +/// The authorization witness. +public struct Admin has drop {} - /// Authorize the admin application in the SuiNS to get access - /// to protected functions. Must be called in order to use the rest - /// of the functions. - public fun authorize(cap: &AdminCap, suins: &mut SuiNS) { - suins::authorize_app(cap, suins) - } +/// Authorize the admin application in the SuiNS to get access +/// to protected functions. Must be called in order to use the rest +/// of the functions. +public fun authorize(cap: &AdminCap, suins: &mut SuiNS) { + suins::authorize_app(cap, suins) +} - /// Reserve a `domain` in the `SuiNS`. - public fun reserve_domain( - _: &AdminCap, - suins: &mut SuiNS, - domain_name: String, - no_years: u8, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - let domain = domain::new(domain_name); - config::assert_valid_user_registerable_domain(&domain); - let registry = suins::app_registry_mut(Admin {}, suins); - registry.add_record(domain, no_years, clock, ctx) - } +/// Reserve a `domain` in the `SuiNS`. +public fun reserve_domain( + _: &AdminCap, + suins: &mut SuiNS, + domain_name: String, + no_years: u8, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + let domain = domain::new(domain_name); + config::assert_valid_user_registerable_domain(&domain); + let registry = suins::app_registry_mut(Admin {}, suins); + registry.add_record(domain, no_years, clock, ctx) +} - /// Reserve a list of domains. - entry fun reserve_domains( - _: &AdminCap, - suins: &mut SuiNS, - mut domains: vector, - no_years: u8, - clock: &Clock, - ctx: &mut TxContext - ) { - let sender = sender(ctx); - let registry = suins::app_registry_mut(Admin {}, suins); - while (!domains.is_empty()) { - let domain = domain::new(domains.pop_back()); - config::assert_valid_user_registerable_domain(&domain); - let nft = registry.add_record(domain, no_years, clock, ctx); - sui::transfer::public_transfer(nft, sender); - }; - } +/// Reserve a list of domains. +entry fun reserve_domains( + _: &AdminCap, + suins: &mut SuiNS, + mut domains: vector, + no_years: u8, + clock: &Clock, + ctx: &mut TxContext, +) { + let sender = sender(ctx); + let registry = suins::app_registry_mut(Admin {}, suins); + while (!domains.is_empty()) { + let domain = domain::new(domains.pop_back()); + config::assert_valid_user_registerable_domain(&domain); + let nft = registry.add_record(domain, no_years, clock, ctx); + sui::transfer::public_transfer(nft, sender); + }; } diff --git a/packages/suins/sources/auction.move b/packages/suins/sources/auction.move index bae81e83..51c01257 100644 --- a/packages/suins/sources/auction.move +++ b/packages/suins/sources/auction.move @@ -3,434 +3,440 @@ /// Implementation of auction module. /// More information in: ../../../docs -module suins::auction { - use std::{option::{none, some, is_some}, string::String}; - - use sui::{ - balance::{Self, Balance}, - coin::{Self, Coin}, - clock::Clock, - event, - sui::SUI, - linked_table::{Self, LinkedTable} +module suins::auction; + +use std::option::{none, some, is_some}; +use std::string::String; +use sui::balance::{Self, Balance}; +use sui::clock::Clock; +use sui::coin::{Self, Coin}; +use sui::event; +use sui::linked_table::{Self, LinkedTable}; +use sui::sui::SUI; +use suins::config::{Self, Config}; +use suins::domain::{Self, Domain}; +use suins::registry::Registry; +use suins::suins::{Self, AdminCap, SuiNS}; +use suins::suins_registration::SuinsRegistration; + +/// One year is the default duration for a domain. +const DEFAULT_DURATION: u8 = 1; +/// The auction bidding period is 2 days. +const AUCTION_BIDDING_PERIOD_MS: u64 = 2 * 24 * 60 * 60 * 1000; +/// The auction quiet period is 10 minutes. +const AUCTION_MIN_QUIET_PERIOD_MS: u64 = 10 * 60 * 1000; + +// === Abort codes === + +/// The bid value is too low (compared to min_bid or previous bid). +const EInvalidBidValue: u64 = 0; +/// Trying to start an action but it's already started. +const EAuctionStarted: u64 = 1; +/// Placing a bid in a not started +const EAuctionNotStarted: u64 = 7; +const EAuctionNotEndedYet: u64 = 8; +const EAuctionEnded: u64 = 9; +const ENotWinner: u64 = 10; +const EWinnerCannotPlaceBid: u64 = 11; +const EBidAmountTooLow: u64 = 12; +const ENoProfits: u64 = 13; + +/// Authorization witness to call protected functions of suins. +public struct App has drop {} + +/// The AuctionHouse application. +public struct AuctionHouse has key, store { + id: UID, + balance: Balance, + auctions: LinkedTable, +} + +/// The Auction application. +public struct Auction has store { + domain: Domain, + start_timestamp_ms: u64, + end_timestamp_ms: u64, + winner: address, + current_bid: Coin, + nft: SuinsRegistration, +} + +fun init(ctx: &mut TxContext) { + sui::transfer::share_object(AuctionHouse { + id: object::new(ctx), + balance: balance::zero(), + auctions: linked_table::new(ctx), + }); +} + +/// Start an auction if it's not started yet; and make the first bid. +public fun start_auction_and_place_bid( + self: &mut AuctionHouse, + suins: &mut SuiNS, + domain_name: String, + bid: Coin, + clock: &Clock, + ctx: &mut TxContext, +) { + suins.assert_app_is_authorized(); + + let domain = domain::new(domain_name); + + // make sure the domain is a .sui domain and not a subdomain + config::assert_valid_user_registerable_domain(&domain); + + assert!(!self.auctions.contains(domain), EAuctionStarted); + + // The minimum price only applies to newly created auctions + let config = suins.get_config(); + let label = domain.sld(); + let min_price = config.calculate_price( + (label.length() as u8), + DEFAULT_DURATION, + ); + assert!(bid.value() >= min_price, EInvalidBidValue); + + let registry = suins::app_registry_mut(App {}, suins); + let nft = registry.add_record(domain, DEFAULT_DURATION, clock, ctx); + let starting_bid = bid.value(); + + let auction = Auction { + domain, + start_timestamp_ms: clock.timestamp_ms(), + end_timestamp_ms: clock.timestamp_ms() + AUCTION_BIDDING_PERIOD_MS, + winner: ctx.sender(), + current_bid: bid, + nft, }; - use suins::{ - config::{Self, Config}, - suins::{Self, AdminCap, SuiNS}, - suins_registration::SuinsRegistration, - registry::Registry, - domain::{Self, Domain} + event::emit(AuctionStartedEvent { + domain, + start_timestamp_ms: auction.start_timestamp_ms, + end_timestamp_ms: auction.end_timestamp_ms, + starting_bid, + bidder: auction.winner, + }); + + self.auctions.push_front(domain, auction) +} + +/// #### Notice +/// Bidders use this function to place a new bid. +/// +/// Panics +/// Panics if `domain` is invalid +/// or there isn't an auction for `domain` +/// or `bid` is too low, +public fun place_bid( + self: &mut AuctionHouse, + domain_name: String, + bid: Coin, + clock: &Clock, + ctx: &mut TxContext, +) { + let domain = domain::new(domain_name); + let bidder = ctx.sender(); + + assert!(self.auctions.contains(domain), EAuctionNotStarted); + + let Auction { + domain, + start_timestamp_ms, + mut end_timestamp_ms, + winner, + current_bid, + nft, + } = self.auctions.remove(domain); + + // Ensure that the auction is not over + assert!(clock.timestamp_ms() <= end_timestamp_ms, EAuctionEnded); + // Ensure the bidder isn't already the winner + assert!(bidder != winner, EWinnerCannotPlaceBid); + + // get the current highest bid and ensure that the new bid is greater than + // the current winning bid + let current_winning_bid = current_bid.value(); + let bid_amount = bid.value(); + assert!(bid_amount > current_winning_bid, EBidAmountTooLow); + + // Return the previous winner their bid + sui::transfer::public_transfer(current_bid, winner); + + event::emit(BidEvent { + domain, + bid: bid_amount, + bidder, + }); + + // If there is less than `AUCTION_MIN_QUIET_PERIOD_MS` time left on the + // auction + // then extend the auction so that there is `AUCTION_MIN_QUIET_PERIOD_MS` + // left. + // Auctions can't be finished until there is at least + // `AUCTION_MIN_QUIET_PERIOD_MS` + // time where there are no bids. + if (end_timestamp_ms - clock.timestamp_ms() < AUCTION_MIN_QUIET_PERIOD_MS) { + let new_end_timestamp_ms = + clock.timestamp_ms() + AUCTION_MIN_QUIET_PERIOD_MS; + + // Only extend the auction if the new auction end time is before + // the NFT's expiration timestamp + if (new_end_timestamp_ms < nft.expiration_timestamp_ms()) { + end_timestamp_ms = new_end_timestamp_ms; + + event::emit(AuctionExtendedEvent { + domain, + end_timestamp_ms: end_timestamp_ms, + }); + } }; - /// One year is the default duration for a domain. - const DEFAULT_DURATION: u8 = 1; - /// The auction bidding period is 2 days. - const AUCTION_BIDDING_PERIOD_MS: u64 = 2 * 24 * 60 * 60 * 1000; - /// The auction quiet period is 10 minutes. - const AUCTION_MIN_QUIET_PERIOD_MS: u64 = 10 * 60 * 1000; - - // === Abort codes === - - /// The bid value is too low (compared to min_bid or previous bid). - const EInvalidBidValue: u64 = 0; - /// Trying to start an action but it's already started. - const EAuctionStarted: u64 = 1; - /// Placing a bid in a not started - const EAuctionNotStarted: u64 = 7; - const EAuctionNotEndedYet: u64 = 8; - const EAuctionEnded: u64 = 9; - const ENotWinner: u64 = 10; - const EWinnerCannotPlaceBid: u64 = 11; - const EBidAmountTooLow: u64 = 12; - const ENoProfits: u64 = 13; - - /// Authorization witness to call protected functions of suins. - public struct App has drop {} - - /// The AuctionHouse application. - public struct AuctionHouse has key, store { - id: UID, - balance: Balance, - auctions: LinkedTable, - } - - /// The Auction application. - public struct Auction has store { - domain: Domain, - start_timestamp_ms: u64, - end_timestamp_ms: u64, - winner: address, - current_bid: Coin, - nft: SuinsRegistration, - } - - fun init(ctx: &mut TxContext) { - sui::transfer::share_object(AuctionHouse { - id: object::new(ctx), - balance: balance::zero(), - auctions: linked_table::new(ctx), - }); - } - - /// Start an auction if it's not started yet; and make the first bid. - public fun start_auction_and_place_bid( - self: &mut AuctionHouse, - suins: &mut SuiNS, - domain_name: String, - bid: Coin, - clock: &Clock, - ctx: &mut TxContext, - ) { - suins.assert_app_is_authorized(); - - let domain = domain::new(domain_name); - - // make sure the domain is a .sui domain and not a subdomain - config::assert_valid_user_registerable_domain(&domain); - - assert!(!self.auctions.contains(domain), EAuctionStarted); - - // The minimum price only applies to newly created auctions - let config = suins.get_config(); - let label = domain.sld(); - let min_price = config.calculate_price((label.length() as u8), DEFAULT_DURATION); - assert!(bid.value() >= min_price, EInvalidBidValue); - - let registry = suins::app_registry_mut(App {}, suins); - let nft = registry.add_record(domain, DEFAULT_DURATION, clock, ctx); - let starting_bid = bid.value(); - - let auction = Auction { - domain, - start_timestamp_ms: clock.timestamp_ms(), - end_timestamp_ms: clock.timestamp_ms() + AUCTION_BIDDING_PERIOD_MS, - winner: ctx.sender(), - current_bid: bid, - nft, - }; + let auction = Auction { + domain, + start_timestamp_ms, + end_timestamp_ms, + winner: tx_context::sender(ctx), + current_bid: bid, + nft, + }; - event::emit(AuctionStartedEvent { - domain, - start_timestamp_ms: auction.start_timestamp_ms, - end_timestamp_ms: auction.end_timestamp_ms, - starting_bid, - bidder: auction.winner, - }); - - self.auctions.push_front(domain, auction) - } - - /// #### Notice - /// Bidders use this function to place a new bid. - /// - /// Panics - /// Panics if `domain` is invalid - /// or there isn't an auction for `domain` - /// or `bid` is too low, - public fun place_bid( - self: &mut AuctionHouse, - domain_name: String, - bid: Coin, - clock: &Clock, - ctx: &mut TxContext - ) { - let domain = domain::new(domain_name); - let bidder = ctx.sender(); - - assert!(self.auctions.contains(domain), EAuctionNotStarted); - - let Auction { - domain, - start_timestamp_ms, - mut end_timestamp_ms, - winner, - current_bid, - nft, - } = self.auctions.remove(domain); - - // Ensure that the auction is not over - assert!(clock.timestamp_ms() <= end_timestamp_ms, EAuctionEnded); - // Ensure the bidder isn't already the winner - assert!(bidder != winner, EWinnerCannotPlaceBid); - - // get the current highest bid and ensure that the new bid is greater than the current winning bid - let current_winning_bid = current_bid.value(); - let bid_amount = bid.value(); - assert!(bid_amount > current_winning_bid, EBidAmountTooLow); - - // Return the previous winner their bid - sui::transfer::public_transfer(current_bid, winner); - - event::emit(BidEvent { - domain, - bid: bid_amount, - bidder, - }); - - // If there is less than `AUCTION_MIN_QUIET_PERIOD_MS` time left on the auction - // then extend the auction so that there is `AUCTION_MIN_QUIET_PERIOD_MS` left. - // Auctions can't be finished until there is at least `AUCTION_MIN_QUIET_PERIOD_MS` - // time where there are no bids. - if (end_timestamp_ms - clock.timestamp_ms() < AUCTION_MIN_QUIET_PERIOD_MS) { - let new_end_timestamp_ms = clock.timestamp_ms() + AUCTION_MIN_QUIET_PERIOD_MS; - - // Only extend the auction if the new auction end time is before - // the NFT's expiration timestamp - if (new_end_timestamp_ms < nft.expiration_timestamp_ms()) { - end_timestamp_ms = new_end_timestamp_ms; - - event::emit(AuctionExtendedEvent { - domain, - end_timestamp_ms: end_timestamp_ms, - }); - } - }; + self.auctions.push_front(domain, auction); +} - let auction = Auction { - domain, - start_timestamp_ms, - end_timestamp_ms, - winner: tx_context::sender(ctx), - current_bid: bid, - nft, - }; +/// #### Notice +/// Auction winner can come and claim the NFT +/// +/// Panics +/// sender is not the winner +public fun claim( + self: &mut AuctionHouse, + domain_name: String, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + let domain = domain::new(domain_name); + + let Auction { + domain: _, + start_timestamp_ms, + end_timestamp_ms, + winner, + current_bid, + nft, + } = self.auctions.remove(domain); + + // Ensure that the auction is over + assert!(clock.timestamp_ms() > end_timestamp_ms, EAuctionNotEndedYet); + + // Ensure the sender is the winner + assert!(ctx.sender() == winner, ENotWinner); + + event::emit(AuctionFinalizedEvent { + domain, + start_timestamp_ms, + end_timestamp_ms, + winning_bid: coin::value(¤t_bid), + winner, + }); + + // Extract the NFT and their bid, returning the NFT to the user + // and sending the proceeds of the auction to suins + self.balance.join(current_bid.into_balance()); + nft +} + +// === Public Functions === + +/// #### Notice +/// Get metadata of an auction +/// +/// #### Params +/// The domain name being auctioned. +/// +/// #### Return +/// (`start_timestamp_ms`, `end_timestamp_ms`, `winner`, `highest_amount`) +public fun get_auction_metadata( + self: &AuctionHouse, + domain_name: String, +): (Option, Option, Option
, Option) { + let domain = domain::new(domain_name); + + if (self.auctions.contains(domain)) { + let auction = &self.auctions[domain]; + let highest_amount = auction.current_bid.value(); + return ( + some(auction.start_timestamp_ms), + some(auction.end_timestamp_ms), + some(auction.winner), + some(highest_amount), + ) + }; + (none(), none(), none(), none()) +} + +public fun collect_winning_auction_fund( + self: &mut AuctionHouse, + domain_name: String, + clock: &Clock, + ctx: &mut TxContext, +) { + let domain = domain::new(domain_name); + let auction = &mut self.auctions[domain]; + // Ensure that the auction is over + assert!( + clock.timestamp_ms() > auction.end_timestamp_ms, + EAuctionNotEndedYet, + ); + + let amount = auction.current_bid.value(); + self.balance.join(auction.current_bid.split(amount, ctx).into_balance()); +} + +// === Admin Functions === + +public fun admin_withdraw_funds( + _: &AdminCap, + self: &mut AuctionHouse, + ctx: &mut TxContext, +): Coin { + let amount = self.balance.value(); + assert!(amount > 0, ENoProfits); + coin::take(&mut self.balance, amount, ctx) +} - self.auctions.push_front(domain, auction); - } - - /// #### Notice - /// Auction winner can come and claim the NFT - /// - /// Panics - /// sender is not the winner - public fun claim( - self: &mut AuctionHouse, - domain_name: String, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - let domain = domain::new(domain_name); - - let Auction { - domain: _, - start_timestamp_ms, - end_timestamp_ms, - winner, - current_bid, - nft, - } = self.auctions.remove(domain); - - // Ensure that the auction is over - assert!(clock.timestamp_ms() > end_timestamp_ms, EAuctionNotEndedYet); - - // Ensure the sender is the winner - assert!(ctx.sender() == winner, ENotWinner); - - event::emit(AuctionFinalizedEvent { - domain, - start_timestamp_ms, - end_timestamp_ms, - winning_bid: coin::value(¤t_bid), - winner, - }); - - // Extract the NFT and their bid, returning the NFT to the user - // and sending the proceeds of the auction to suins - self.balance.join(current_bid.into_balance()); - nft - } - - // === Public Functions === - - /// #### Notice - /// Get metadata of an auction - /// - /// #### Params - /// The domain name being auctioned. - /// - /// #### Return - /// (`start_timestamp_ms`, `end_timestamp_ms`, `winner`, `highest_amount`) - public fun get_auction_metadata( - self: &AuctionHouse, - domain_name: String, - ): (Option, Option, Option
, Option) { - let domain = domain::new(domain_name); - - if (self.auctions.contains(domain)) { - let auction = &self.auctions[domain]; - let highest_amount = auction.current_bid.value(); - return ( - some(auction.start_timestamp_ms), - some(auction.end_timestamp_ms), - some(auction.winner), - some(highest_amount) - ) +/// Admin functionality used to finalize a single auction. +/// +/// An `operation_limit` limit must be provided which controls how many +/// individual operations to perform. This allows the admin to be able to +/// make forward progress in finalizing auctions even in the presence of +/// thousands of bids. +/// +/// This will attempt to do as much as possible of the following +/// based on the provided `operation_limit`: +/// - claim the winning bid and place in `AuctionHouse.balance` +/// - push the `SuinsRegistration` to the winner +/// - push loosing bids back to their respective account owners +/// +/// Once all of the above has been done the auction is destroyed, +/// freeing on-chain storage. +public fun admin_finalize_auction( + admin: &AdminCap, + self: &mut AuctionHouse, + domain: String, + clock: &Clock, +) { + let domain = domain::new(domain); + admin_finalize_auction_internal(admin, self, domain, clock); +} + +fun admin_finalize_auction_internal( + _: &AdminCap, + self: &mut AuctionHouse, + domain: Domain, + clock: &Clock, +) { + let Auction { + domain: _, + start_timestamp_ms, + end_timestamp_ms, + winner, + current_bid, + nft, + } = self.auctions.remove(domain); + + // Ensure that the auction is over + assert!(clock.timestamp_ms() > end_timestamp_ms, EAuctionNotEndedYet); + + event::emit(AuctionFinalizedEvent { + domain, + start_timestamp_ms, + end_timestamp_ms, + winning_bid: coin::value(¤t_bid), + winner, + }); + + self.balance.join(current_bid.into_balance()); + transfer::public_transfer(nft, winner); +} + +/// Admin functionality used to finalize an arbitrary number of auctions. +/// +/// An `operation_limit` limit must be provided which controls how many +/// individual operations to perform. This allows the admin to be able to +/// make forward progress in finalizing auctions even in the presence of +/// thousands of auctions/bids. +public fun admin_try_finalize_auctions( + admin: &AdminCap, + self: &mut AuctionHouse, + mut operation_limit: u64, + clock: &Clock, +) { + let mut next_domain = *self.auctions.back(); + + while (is_some(&next_domain)) { + if (operation_limit == 0) { + return }; - (none(), none(), none(), none()) - } - - public fun collect_winning_auction_fund( - self: &mut AuctionHouse, - domain_name: String, - clock: &Clock, - ctx: &mut TxContext, - ) { - let domain = domain::new(domain_name); - let auction = &mut self.auctions[domain]; - // Ensure that the auction is over - assert!(clock.timestamp_ms() > auction.end_timestamp_ms, EAuctionNotEndedYet); - - let amount = auction.current_bid.value(); - self.balance.join(auction.current_bid.split(amount, ctx).into_balance()); - } - - // === Admin Functions === - - public fun admin_withdraw_funds( - _: &AdminCap, - self: &mut AuctionHouse, - ctx: &mut TxContext, - ): Coin { - let amount = self.balance.value(); - assert!(amount > 0, ENoProfits); - coin::take(&mut self.balance, amount, ctx) - } - - /// Admin functionality used to finalize a single auction. - /// - /// An `operation_limit` limit must be provided which controls how many - /// individual operations to perform. This allows the admin to be able to - /// make forward progress in finalizing auctions even in the presence of - /// thousands of bids. - /// - /// This will attempt to do as much as possible of the following - /// based on the provided `operation_limit`: - /// - claim the winning bid and place in `AuctionHouse.balance` - /// - push the `SuinsRegistration` to the winner - /// - push loosing bids back to their respective account owners - /// - /// Once all of the above has been done the auction is destroyed, - /// freeing on-chain storage. - public fun admin_finalize_auction( - admin: &AdminCap, - self: &mut AuctionHouse, - domain: String, - clock: &Clock, - ) { - let domain = domain::new(domain); - admin_finalize_auction_internal(admin, self, domain, clock); - } - - fun admin_finalize_auction_internal( - _: &AdminCap, - self: &mut AuctionHouse, - domain: Domain, - clock: &Clock, - ) { - let Auction { - domain: _, - start_timestamp_ms, - end_timestamp_ms, - winner, - current_bid, - nft, - } = self.auctions.remove(domain); - - // Ensure that the auction is over - assert!(clock.timestamp_ms() > end_timestamp_ms, EAuctionNotEndedYet); - - event::emit(AuctionFinalizedEvent { - domain, - start_timestamp_ms, - end_timestamp_ms, - winning_bid: coin::value(¤t_bid), - winner, - }); - - self.balance.join(current_bid.into_balance()); - transfer::public_transfer(nft, winner); - } - - /// Admin functionality used to finalize an arbitrary number of auctions. - /// - /// An `operation_limit` limit must be provided which controls how many - /// individual operations to perform. This allows the admin to be able to - /// make forward progress in finalizing auctions even in the presence of - /// thousands of auctions/bids. - public fun admin_try_finalize_auctions( - admin: &AdminCap, - self: &mut AuctionHouse, - mut operation_limit: u64, - clock: &Clock, - ) { - let mut next_domain = *self.auctions.back(); - - while (is_some(&next_domain)) { - if (operation_limit == 0) { - return - }; - operation_limit = operation_limit - 1; - - let domain = option::extract(&mut next_domain); - next_domain = *self.auctions.prev(domain); - - let auction = &self.auctions[domain]; - - // If the auction has ended, then try to finalize it - if (clock.timestamp_ms() > auction.end_timestamp_ms) { - admin_finalize_auction_internal( - admin, - self, - domain, - clock - ); - }; + operation_limit = operation_limit - 1; + + let domain = option::extract(&mut next_domain); + next_domain = *self.auctions.prev(domain); + + let auction = &self.auctions[domain]; + + // If the auction has ended, then try to finalize it + if (clock.timestamp_ms() > auction.end_timestamp_ms) { + admin_finalize_auction_internal( + admin, + self, + domain, + clock, + ); }; - } - - // === Events === - - public struct AuctionStartedEvent has copy, drop { - domain: Domain, - start_timestamp_ms: u64, - end_timestamp_ms: u64, - starting_bid: u64, - bidder: address, - } - - public struct AuctionFinalizedEvent has copy, drop { - domain: Domain, - start_timestamp_ms: u64, - end_timestamp_ms: u64, - winning_bid: u64, - winner: address, - } - - public struct BidEvent has copy, drop { - domain: Domain, - bid: u64, - bidder: address, - } - - public struct AuctionExtendedEvent has copy, drop { - domain: Domain, - end_timestamp_ms: u64, - } - - // === Testing === - - #[test_only] - public fun init_for_testing(ctx: &mut TxContext) { - sui::transfer::share_object(AuctionHouse { - id: object::new(ctx), - balance: balance::zero(), - auctions: linked_table::new(ctx), - }); - } - - #[test_only] - public fun total_balance(self: &AuctionHouse): u64 { - self.balance.value() - } + }; +} + +// === Events === + +public struct AuctionStartedEvent has copy, drop { + domain: Domain, + start_timestamp_ms: u64, + end_timestamp_ms: u64, + starting_bid: u64, + bidder: address, +} + +public struct AuctionFinalizedEvent has copy, drop { + domain: Domain, + start_timestamp_ms: u64, + end_timestamp_ms: u64, + winning_bid: u64, + winner: address, +} + +public struct BidEvent has copy, drop { + domain: Domain, + bid: u64, + bidder: address, +} + +public struct AuctionExtendedEvent has copy, drop { + domain: Domain, + end_timestamp_ms: u64, +} + +// === Testing === + +#[test_only] +public fun init_for_testing(ctx: &mut TxContext) { + sui::transfer::share_object(AuctionHouse { + id: object::new(ctx), + balance: balance::zero(), + auctions: linked_table::new(ctx), + }); +} + +#[test_only] +public fun total_balance(self: &AuctionHouse): u64 { + self.balance.value() } diff --git a/packages/suins/sources/config.move b/packages/suins/sources/config.move index 350f499b..3ccd3d3e 100644 --- a/packages/suins/sources/config.move +++ b/packages/suins/sources/config.move @@ -9,142 +9,146 @@ /// Contains no access-control checks and all methods are public for the /// following reasons: /// - configuration can only be attached by the application Admin; -/// - attached to the SuiNS object directly and can only be *read* by other parts of the system; +/// - attached to the SuiNS object directly and can only be *read* by other +/// parts of the system; /// /// Notes: /// - set_* methods are currently not used; /// - a simpler way to update the configuration would be to remove it completely /// and set again within the same Programmable Transaction Block (can only be /// performed by Admin) -module suins::config { - use suins::{constants, domain::Domain}; - - /// A label is too short to be registered. - const ELabelTooShort: u64 = 0; - /// A label is too long to be registered. - const ELabelTooLong: u64 = 1; - /// The price value is invalid. - const EInvalidPrice: u64 = 2; - /// The public key is not a Secp256k1 public key which is of length 33 bytes - const EInvalidPublicKey: u64 = 3; - /// Incorrect number of years passed to the function. - const ENoYears: u64 = 4; - /// Trying to register a subdomain (only *.sui is currently allowed). - const EInvalidDomain: u64 = 5; - /// Trying to register a domain name in a different TLD (not .sui). - const EInvalidTld: u64 = 6; - - /// The configuration object, holds current settings of the SuiNS - /// application. Does not carry any business logic and can easily - /// be replaced with any other module providing similar interface - /// and fitting the needs of the application. - public struct Config has store, drop { - public_key: vector, - three_char_price: u64, - four_char_price: u64, - five_plus_char_price: u64, - } - - /// Create a new instance of the configuration object. - /// Define all properties from the start. - public fun new( - public_key: vector, - three_char_price: u64, - four_char_price: u64, - five_plus_char_price: u64, - ): Config { - assert!(public_key.length() == 33, EInvalidPublicKey); - - Config { - public_key, - three_char_price, - four_char_price, - five_plus_char_price, - } - } - - // === Modification: one per property === +module suins::config; + +use suins::constants; +use suins::domain::Domain; + +/// A label is too short to be registered. +const ELabelTooShort: u64 = 0; +/// A label is too long to be registered. +const ELabelTooLong: u64 = 1; +/// The price value is invalid. +const EInvalidPrice: u64 = 2; +/// The public key is not a Secp256k1 public key which is of length 33 bytes +const EInvalidPublicKey: u64 = 3; +/// Incorrect number of years passed to the function. +const ENoYears: u64 = 4; +/// Trying to register a subdomain (only *.sui is currently allowed). +const EInvalidDomain: u64 = 5; +/// Trying to register a domain name in a different TLD (not .sui). +const EInvalidTld: u64 = 6; + +/// The configuration object, holds current settings of the SuiNS +/// application. Does not carry any business logic and can easily +/// be replaced with any other module providing similar interface +/// and fitting the needs of the application. +public struct Config has store, drop { + public_key: vector, + three_char_price: u64, + four_char_price: u64, + five_plus_char_price: u64, +} - /// Change the value of the `public_key` field. - public fun set_public_key(self: &mut Config, value: vector) { - assert!(value.length() == 33, EInvalidPublicKey); - self.public_key = value; +/// Create a new instance of the configuration object. +/// Define all properties from the start. +public fun new( + public_key: vector, + three_char_price: u64, + four_char_price: u64, + five_plus_char_price: u64, +): Config { + assert!(public_key.length() == 33, EInvalidPublicKey); + + Config { + public_key, + three_char_price, + four_char_price, + five_plus_char_price, } +} - /// Change the value of the `three_char_price` field. - public fun set_three_char_price(self: &mut Config, value: u64) { - check_price(value); - self.three_char_price = value; - } +// === Modification: one per property === - /// Change the value of the `four_char_price` field. - public fun set_four_char_price(self: &mut Config, value: u64) { - check_price(value); - self.four_char_price = value; - } +/// Change the value of the `public_key` field. +public fun set_public_key(self: &mut Config, value: vector) { + assert!(value.length() == 33, EInvalidPublicKey); + self.public_key = value; +} - /// Change the value of the `five_plus_char_price` field. - public fun set_five_plus_char_price(self: &mut Config, value: u64) { - check_price(value); - self.five_plus_char_price = value; - } +/// Change the value of the `three_char_price` field. +public fun set_three_char_price(self: &mut Config, value: u64) { + check_price(value); + self.three_char_price = value; +} - // === Price calculations === +/// Change the value of the `four_char_price` field. +public fun set_four_char_price(self: &mut Config, value: u64) { + check_price(value); + self.four_char_price = value; +} - /// Calculate the price of a label. - public fun calculate_price(self: &Config, length: u8, years: u8): u64 { - assert!(years > 0, ENoYears); - assert!(length >= constants::min_domain_length(), ELabelTooShort); - assert!(length <= constants::max_domain_length(), ELabelTooLong); +/// Change the value of the `five_plus_char_price` field. +public fun set_five_plus_char_price(self: &mut Config, value: u64) { + check_price(value); + self.five_plus_char_price = value; +} - let price = if (length == 3) { - self.three_char_price - } else if (length == 4) { - self.four_char_price - } else { - self.five_plus_char_price - }; +// === Price calculations === - ((price as u64) * (years as u64)) - } +/// Calculate the price of a label. +public fun calculate_price(self: &Config, length: u8, years: u8): u64 { + assert!(years > 0, ENoYears); + assert!(length >= constants::min_domain_length(), ELabelTooShort); + assert!(length <= constants::max_domain_length(), ELabelTooLong); + let price = if (length == 3) { + self.three_char_price + } else if (length == 4) { + self.four_char_price + } else { + self.five_plus_char_price + }; - // === Reads: one per property === + ((price as u64) * (years as u64)) +} - /// Get the value of the `public_key` field. - public fun public_key(self: &Config): &vector { &self.public_key } +// === Reads: one per property === - /// Get the value of the `three_char_price` field. - public fun three_char_price(self: &Config): u64 { self.three_char_price } +/// Get the value of the `public_key` field. +public fun public_key(self: &Config): &vector { &self.public_key } - /// Get the value of the `four_char_price` field. - public fun four_char_price(self: &Config): u64 { self.four_char_price } +/// Get the value of the `three_char_price` field. +public fun three_char_price(self: &Config): u64 { self.three_char_price } - /// Get the value of the `five_plus_char_price` field. - public fun five_plus_char_price(self: &Config): u64 { self.five_plus_char_price } +/// Get the value of the `four_char_price` field. +public fun four_char_price(self: &Config): u64 { self.four_char_price } - // === Helpers === +/// Get the value of the `five_plus_char_price` field. +public fun five_plus_char_price(self: &Config): u64 { + self.five_plus_char_price +} - /// Asserts that a domain is registerable by a user: - /// - TLD is "sui" - /// - only has 1 label, "name", other than the TLD - /// - "name" is >= 3 characters long - public fun assert_valid_user_registerable_domain(domain: &Domain) { - assert!(domain.number_of_levels() == 2, EInvalidDomain); - assert!(domain.tld() == &constants::sui_tld(), EInvalidTld); - let length = domain.sld().length(); - assert!(length >= (constants::min_domain_length() as u64), ELabelTooShort); - assert!(length <= (constants::max_domain_length() as u64), ELabelTooLong); - } +// === Helpers === + +/// Asserts that a domain is registerable by a user: +/// - TLD is "sui" +/// - only has 1 label, "name", other than the TLD +/// - "name" is >= 3 characters long +public fun assert_valid_user_registerable_domain(domain: &Domain) { + assert!(domain.number_of_levels() == 2, EInvalidDomain); + assert!(domain.tld() == &constants::sui_tld(), EInvalidTld); + let length = domain.sld().length(); + assert!(length >= (constants::min_domain_length() as u64), ELabelTooShort); + assert!(length <= (constants::max_domain_length() as u64), ELabelTooLong); +} - // === Internal === +// === Internal === - /// Assert that the price is within the allowed range (1-1M). - /// TODO: revisit, are we sure we can't use less than 1 SUI? - fun check_price(price: u64) { - assert!( - constants::mist_per_sui() <= price - && price <= constants::mist_per_sui() * 1_000_000 - , EInvalidPrice); - } +/// Assert that the price is within the allowed range (1-1M). +/// TODO: revisit, are we sure we can't use less than 1 SUI? +fun check_price(price: u64) { + assert!( + constants::mist_per_sui() <= price + && price <= constants::mist_per_sui() * 1_000_000, + EInvalidPrice, + ); } diff --git a/packages/suins/sources/constants.move b/packages/suins/sources/constants.move index ea4697b2..8087e838 100644 --- a/packages/suins/sources/constants.move +++ b/packages/suins/sources/constants.move @@ -6,64 +6,77 @@ /// /// This module is free from any non-framework dependencies and serves as a /// single place of storing constants and proving convenient APIs for reading. -module suins::constants { - use std::string::{utf8, String}; - - /// Max value for basis points. - const MAX_BPS: u16 = 10000; - /// The amount of MIST in 1 SUI. - const MIST_PER_SUI: u64 = 1_000_000_000; - /// The minimum length of a domain name. - const MIN_DOMAIN_LENGTH: u8 = 3; - /// The maximum length of a domain name. - const MAX_DOMAIN_LENGTH: u8 = 63; - /// Top level domain for SUI. - const SUI_TLD: vector = b"sui"; - /// The amount of milliseconds in a year. - const YEAR_MS: u64 = 365 * 24 * 60 * 60 * 1000; - /// Default value for the image_url; IPFS hash. - const DEFAULT_IMAGE: vector = b"QmaLFg4tQYansFpyRqmDfABdkUVy66dHtpnkH15v1LPzcY"; - /// 30 day Grace period in milliseconds. - const GRACE_PERIOD_MS: u64 = 30 * 24 * 60 * 60 * 1000; - - /// A leaf record doesn't expire. Expiration is retrieved by the parent's expiration. - const LEAF_EXPIRATION_TIMESTAMP: u64 = 0; - - /// Subdomain constants - /// - /// These constants are the core of the subdomain functionality. - /// Even if we decide to change the subdomain module, these can - /// be re-used. They're added as metadata on NameRecord. - /// - /// Whether a parent name can create child names. (name -> subdomain) - const ALLOW_CREATION: vector = b"S_AC"; - /// Whether a child-name can auto-renew (if the parent hasn't changed). - const ALLOW_TIME_EXTENSION: vector = b"S_ATE"; - - // === Public functions === - - /// Top level domain for SUI as a String. - public fun sui_tld(): String { utf8(SUI_TLD) } - /// Default value for the image_url. - public fun default_image(): String { utf8(DEFAULT_IMAGE) } - /// The amount of MIST in 1 SUI. - public fun mist_per_sui(): u64 { MIST_PER_SUI } - /// The minimum length of a domain name. - public fun min_domain_length(): u8 { MIN_DOMAIN_LENGTH } - /// The maximum length of a domain name. - public fun max_domain_length(): u8 { MAX_DOMAIN_LENGTH } - /// Maximum value for basis points. - public fun max_bps(): u16 { MAX_BPS } - /// The amount of milliseconds in a year. - public fun year_ms(): u64 { YEAR_MS } - /// Grace period in milliseconds after which the domain expires. - public fun grace_period_ms(): u64 { GRACE_PERIOD_MS } - - /// Subdomain constants - /// The NameRecord key that a subdomain can create child names. - public fun subdomain_allow_creation_key(): String{ utf8(ALLOW_CREATION) } - /// The NameRecord key that a subdomain can self-renew. - public fun subdomain_allow_extension_key(): String{ utf8(ALLOW_TIME_EXTENSION) } - /// A getter for a leaf name record's expiration timestamp. - public fun leaf_expiration_timestamp(): u64 { LEAF_EXPIRATION_TIMESTAMP } +module suins::constants; + +use std::string::{utf8, String}; + +/// Max value for basis points. +const MAX_BPS: u16 = 10000; +/// The amount of MIST in 1 SUI. +const MIST_PER_SUI: u64 = 1_000_000_000; +/// The minimum length of a domain name. +const MIN_DOMAIN_LENGTH: u8 = 3; +/// The maximum length of a domain name. +const MAX_DOMAIN_LENGTH: u8 = 63; +/// Top level domain for SUI. +const SUI_TLD: vector = b"sui"; +/// The amount of milliseconds in a year. +const YEAR_MS: u64 = 365 * 24 * 60 * 60 * 1000; +/// Default value for the image_url; IPFS hash. +const DEFAULT_IMAGE: vector = + b"QmaLFg4tQYansFpyRqmDfABdkUVy66dHtpnkH15v1LPzcY"; +/// 30 day Grace period in milliseconds. +const GRACE_PERIOD_MS: u64 = 30 * 24 * 60 * 60 * 1000; + +/// A leaf record doesn't expire. Expiration is retrieved by the parent's +/// expiration. +const LEAF_EXPIRATION_TIMESTAMP: u64 = 0; + +/// Subdomain constants +/// +/// These constants are the core of the subdomain functionality. +/// Even if we decide to change the subdomain module, these can +/// be re-used. They're added as metadata on NameRecord. +/// +/// Whether a parent name can create child names. (name -> subdomain) +const ALLOW_CREATION: vector = b"S_AC"; +/// Whether a child-name can auto-renew (if the parent hasn't changed). +const ALLOW_TIME_EXTENSION: vector = b"S_ATE"; + +// === Public functions === + +/// Top level domain for SUI as a String. +public fun sui_tld(): String { utf8(SUI_TLD) } + +/// Default value for the image_url. +public fun default_image(): String { utf8(DEFAULT_IMAGE) } + +/// The amount of MIST in 1 SUI. +public fun mist_per_sui(): u64 { MIST_PER_SUI } + +/// The minimum length of a domain name. +public fun min_domain_length(): u8 { MIN_DOMAIN_LENGTH } + +/// The maximum length of a domain name. +public fun max_domain_length(): u8 { MAX_DOMAIN_LENGTH } + +/// Maximum value for basis points. +public fun max_bps(): u16 { MAX_BPS } + +/// The amount of milliseconds in a year. +public fun year_ms(): u64 { YEAR_MS } + +/// Grace period in milliseconds after which the domain expires. +public fun grace_period_ms(): u64 { GRACE_PERIOD_MS } + +/// Subdomain constants +/// The NameRecord key that a subdomain can create child names. +public fun subdomain_allow_creation_key(): String { utf8(ALLOW_CREATION) } + +/// The NameRecord key that a subdomain can self-renew. +public fun subdomain_allow_extension_key(): String { + utf8(ALLOW_TIME_EXTENSION) } + +/// A getter for a leaf name record's expiration timestamp. +public fun leaf_expiration_timestamp(): u64 { LEAF_EXPIRATION_TIMESTAMP } diff --git a/packages/suins/sources/controller.move b/packages/suins/sources/controller.move index c41181e4..a7c0da9b 100644 --- a/packages/suins/sources/controller.move +++ b/packages/suins/sources/controller.move @@ -1,119 +1,166 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -module suins::controller { - use std::string::String; - use sui::{tx_context::{sender}, clock::Clock}; - - use suins::{domain, registry::Registry, suins::{Self, SuiNS}, suins_registration::SuinsRegistration}; - - const AVATAR: vector = b"avatar"; - const CONTENT_HASH: vector = b"content_hash"; - - const EUnsupportedKey: u64 = 0; - - /// Authorization token for the controller. - public struct Controller has drop {} - - // === Update Records Functionality === - - /// User-facing function (upgradable) - set the target address of a domain. - entry fun set_target_address( - suins: &mut SuiNS, - nft: &SuinsRegistration, - new_target: Option
, - clock: &Clock, - ) { - let registry = suins::app_registry_mut(Controller {}, suins); - registry.assert_nft_is_authorized(nft, clock); - - let domain = nft.domain(); - registry.set_target_address(domain, new_target); - } - - /// User-facing function (upgradable) - set the reverse lookup address for the domain. - entry fun set_reverse_lookup(suins: &mut SuiNS, domain_name: String, ctx: &TxContext) { - let domain = domain::new(domain_name); - let registry = suins::app_registry_mut(Controller {}, suins); - registry.set_reverse_lookup(sender(ctx), domain); - } - - /// User-facing function (upgradable) - unset the reverse lookup address for the domain. - entry fun unset_reverse_lookup(suins: &mut SuiNS, ctx: &TxContext) { - let registry = suins::app_registry_mut(Controller {}, suins); - registry.unset_reverse_lookup(sender(ctx)); - } - - /// User-facing function (upgradable) - add a new key-value pair to the name record's data. - entry fun set_user_data( - suins: &mut SuiNS, nft: &SuinsRegistration, key: String, value: String, clock: &Clock - ) { - - let registry = suins::app_registry_mut(Controller {}, suins); - let mut data = *registry.get_data(nft.domain()); - let domain = nft.domain(); - - registry.assert_nft_is_authorized(nft, clock); - let key_bytes = *key.bytes(); - assert!(key_bytes == AVATAR || key_bytes == CONTENT_HASH, EUnsupportedKey); - - if (data.contains(&key)) { - data.remove(&key); - }; - - data.insert(key, value); - registry.set_data(domain, data); - } - - /// User-facing function (upgradable) - remove a key from the name record's data. - entry fun unset_user_data( - suins: &mut SuiNS, nft: &SuinsRegistration, key: String, clock: &Clock - ) { - let registry = suins::app_registry_mut(Controller {}, suins); - let mut data = *registry.get_data(nft.domain()); - let domain = nft.domain(); - - registry.assert_nft_is_authorized(nft, clock); - - if (data.contains(&key)) { - data.remove(&key); - }; - - registry.set_data(domain, data); - } - - // === Testing === - - #[test_only] - public fun set_target_address_for_testing( - suins: &mut SuiNS, nft: &SuinsRegistration, new_target: Option
, clock: &Clock - ) { - set_target_address(suins, nft, new_target, clock) - } - - #[test_only] - public fun set_reverse_lookup_for_testing( - suins: &mut SuiNS, domain_name: String, ctx: &TxContext - ) { - set_reverse_lookup(suins, domain_name, ctx) - } - - #[test_only] - public fun unset_reverse_lookup_for_testing(suins: &mut SuiNS, ctx: &TxContext) { - unset_reverse_lookup(suins, ctx) - } - - #[test_only] - public fun set_user_data_for_testing( - suins: &mut SuiNS, nft: &SuinsRegistration, key: String, value: String, clock: &Clock - ) { - set_user_data(suins, nft, key, value, clock); - } - - #[test_only] - public fun unset_user_data_for_testing( - suins: &mut SuiNS, nft: &SuinsRegistration, key: String, clock: &Clock - ) { - unset_user_data(suins, nft, key, clock); - } +module suins::controller; + +use std::string::String; +use sui::clock::Clock; +use sui::tx_context::sender; +use suins::domain; +use suins::registry::Registry; +use suins::suins::{Self, SuiNS}; +use suins::suins_registration::SuinsRegistration; + +const AVATAR: vector = b"avatar"; +const CONTENT_HASH: vector = b"content_hash"; + +const EUnsupportedKey: u64 = 0; + +/// Authorization token for the controller. +public struct Controller has drop {} + +// === Update Records Functionality === + +/// User-facing function (upgradable) - set the target address of a domain. +entry fun set_target_address( + suins: &mut SuiNS, + nft: &SuinsRegistration, + new_target: Option
, + clock: &Clock, +) { + let registry = suins::app_registry_mut( + Controller {}, + suins, + ); + registry.assert_nft_is_authorized(nft, clock); + + let domain = nft.domain(); + registry.set_target_address(domain, new_target); +} + +/// User-facing function (upgradable) - set the reverse lookup address for the +/// domain. +entry fun set_reverse_lookup( + suins: &mut SuiNS, + domain_name: String, + ctx: &TxContext, +) { + let domain = domain::new(domain_name); + let registry = suins::app_registry_mut( + Controller {}, + suins, + ); + registry.set_reverse_lookup(sender(ctx), domain); +} + +/// User-facing function (upgradable) - unset the reverse lookup address for the +/// domain. +entry fun unset_reverse_lookup(suins: &mut SuiNS, ctx: &TxContext) { + let registry = suins::app_registry_mut( + Controller {}, + suins, + ); + registry.unset_reverse_lookup(sender(ctx)); +} + +/// User-facing function (upgradable) - add a new key-value pair to the name +/// record's data. +entry fun set_user_data( + suins: &mut SuiNS, + nft: &SuinsRegistration, + key: String, + value: String, + clock: &Clock, +) { + let registry = suins::app_registry_mut( + Controller {}, + suins, + ); + let mut data = *registry.get_data(nft.domain()); + let domain = nft.domain(); + + registry.assert_nft_is_authorized(nft, clock); + let key_bytes = *key.bytes(); + assert!(key_bytes == AVATAR || key_bytes == CONTENT_HASH, EUnsupportedKey); + + if (data.contains(&key)) { + data.remove(&key); + }; + + data.insert(key, value); + registry.set_data(domain, data); +} + +/// User-facing function (upgradable) - remove a key from the name record's +/// data. +entry fun unset_user_data( + suins: &mut SuiNS, + nft: &SuinsRegistration, + key: String, + clock: &Clock, +) { + let registry = suins::app_registry_mut( + Controller {}, + suins, + ); + let mut data = *registry.get_data(nft.domain()); + let domain = nft.domain(); + + registry.assert_nft_is_authorized(nft, clock); + + if (data.contains(&key)) { + data.remove(&key); + }; + + registry.set_data(domain, data); +} + +// === Testing === + +#[test_only] +public fun set_target_address_for_testing( + suins: &mut SuiNS, + nft: &SuinsRegistration, + new_target: Option
, + clock: &Clock, +) { + set_target_address(suins, nft, new_target, clock) +} + +#[test_only] +public fun set_reverse_lookup_for_testing( + suins: &mut SuiNS, + domain_name: String, + ctx: &TxContext, +) { + set_reverse_lookup(suins, domain_name, ctx) +} + +#[test_only] +public fun unset_reverse_lookup_for_testing( + suins: &mut SuiNS, + ctx: &TxContext, +) { + unset_reverse_lookup(suins, ctx) +} + +#[test_only] +public fun set_user_data_for_testing( + suins: &mut SuiNS, + nft: &SuinsRegistration, + key: String, + value: String, + clock: &Clock, +) { + set_user_data(suins, nft, key, value, clock); +} + +#[test_only] +public fun unset_user_data_for_testing( + suins: &mut SuiNS, + nft: &SuinsRegistration, + key: String, + clock: &Clock, +) { + unset_user_data(suins, nft, key, clock); } diff --git a/packages/suins/sources/domain.move b/packages/suins/sources/domain.move index 9b9d30d4..55dbcdfe 100644 --- a/packages/suins/sources/domain.move +++ b/packages/suins/sources/domain.move @@ -6,315 +6,346 @@ /// Domains are structured similar to their web2 counterpart and the rules /// determining what a valid domain is can be found here: /// https://en.wikipedia.org/wiki/Domain_name#Domain_name_syntax -module suins::domain { - use std::string::{Self, String, utf8}; - - const EInvalidDomain: u64 = 0; - - /// The maximum length of a full domain - const MAX_DOMAIN_LENGTH: u64 = 235; - /// The minimum length of an individual label in a domain. - const MIN_LABEL_LENGTH: u64 = 1; - /// The maximum length of an individual label in a domain. - const MAX_LABEL_LENGTH: u64 = 63; - - /// Representation of a valid SuiNS `Domain`. - public struct Domain has copy, drop, store { - /// Vector of labels that make up a domain. - /// - /// Labels are stored in reverse order such that the TLD is always in position `0`. - /// e.g. domain "pay.name.sui" will be stored in the vector as ["sui", "name", "pay"]. - labels: vector, - } - - // Construct a `Domain` by parsing and validating the provided string - public fun new(domain: String): Domain { - assert!(domain.length() <= MAX_DOMAIN_LENGTH, EInvalidDomain); +module suins::domain; - let mut labels = split_by_dot(domain); - validate_labels(&labels); - labels.reverse(); - Domain { - labels - } - } +use std::string::{Self, String, utf8}; - /// Converts a domain into a fully-qualified string representation. - public fun to_string(self: &Domain): String { - let dot = utf8(b"."); - let len = self.labels.length(); - let mut i = 0; - let mut out = string::utf8(vector::empty()); - - while (i < len) { - let part = &self.labels[(len - i) - 1]; - out.append(*part); - - i = i + 1; - if (i != len) { - out.append(dot); - } - }; +const EInvalidDomain: u64 = 0; - out - } +/// The maximum length of a full domain +const MAX_DOMAIN_LENGTH: u64 = 235; +/// The minimum length of an individual label in a domain. +const MIN_LABEL_LENGTH: u64 = 1; +/// The maximum length of an individual label in a domain. +const MAX_LABEL_LENGTH: u64 = 63; - /// Returns the `label` in a domain specified by `level`. +/// Representation of a valid SuiNS `Domain`. +public struct Domain has copy, drop, store { + /// Vector of labels that make up a domain. /// - /// Given the domain "pay.name.sui" the individual labels have the following levels: - /// - "pay" - `2` - /// - "name" - `1` - /// - "sui" - `0` - /// - /// This means that the TLD will always be at level `0`. - public fun label(self: &Domain, level: u64): &String { - &self.labels[level] - } - - /// Returns the TLD (Top-Level Domain) of a `Domain`. - /// - /// "name.sui" -> "sui" - public fun tld(self: &Domain): &String { - label(self, 0) - } + /// Labels are stored in reverse order such that the TLD is always in + /// position `0`. + /// e.g. domain "pay.name.sui" will be stored in the vector as ["sui", + /// "name", "pay"]. + labels: vector, +} - /// Returns the SLD (Second-Level Domain) of a `Domain`. - /// - /// "name.sui" -> "sui" - public fun sld(self: &Domain): &String { - label(self, 1) - } +// Construct a `Domain` by parsing and validating the provided string +public fun new(domain: String): Domain { + assert!(domain.length() <= MAX_DOMAIN_LENGTH, EInvalidDomain); - public fun number_of_levels(self: &Domain): u64 { - self.labels.length() + let mut labels = split_by_dot(domain); + validate_labels(&labels); + labels.reverse(); + Domain { + labels, } +} - public fun is_subdomain(domain: &Domain): bool { - number_of_levels(domain) > 2 - } +/// Converts a domain into a fully-qualified string representation. +public fun to_string(self: &Domain): String { + let dot = utf8(b"."); + let len = self.labels.length(); + let mut i = 0; + let mut out = string::utf8(vector::empty()); - /// Derive the parent of a subdomain. - /// e.g. `subdomain.example.sui` -> `example.sui` - public fun parent(domain: &Domain): Domain { - let mut labels = domain.labels; - // we pop the last element and construct the parent from the remaining labels. - labels.pop_back(); + while (i < len) { + let part = &self.labels[(len - i) - 1]; + out.append(*part); - Domain { - labels + i = i + 1; + if (i != len) { + out.append(dot); } + }; + + out +} + +/// Returns the `label` in a domain specified by `level`. +/// +/// Given the domain "pay.name.sui" the individual labels have the following +/// levels: +/// - "pay" - `2` +/// - "name" - `1` +/// - "sui" - `0` +/// +/// This means that the TLD will always be at level `0`. +public fun label(self: &Domain, level: u64): &String { + &self.labels[level] +} + +/// Returns the TLD (Top-Level Domain) of a `Domain`. +/// +/// "name.sui" -> "sui" +public fun tld(self: &Domain): &String { + label(self, 0) +} + +/// Returns the SLD (Second-Level Domain) of a `Domain`. +/// +/// "name.sui" -> "sui" +public fun sld(self: &Domain): &String { + label(self, 1) +} + +public fun number_of_levels(self: &Domain): u64 { + self.labels.length() +} + +public fun is_subdomain(domain: &Domain): bool { + number_of_levels(domain) > 2 +} + +/// Derive the parent of a subdomain. +/// e.g. `subdomain.example.sui` -> `example.sui` +public fun parent(domain: &Domain): Domain { + let mut labels = domain.labels; + // we pop the last element and construct the parent from the remaining + // labels. + labels.pop_back(); + + Domain { + labels, } - - /// Checks if `parent` domain is a valid parent for `child`. - public fun is_parent_of(parent: &Domain, child: &Domain): bool { - number_of_levels(parent) < number_of_levels(child) && +} + +/// Checks if `parent` domain is a valid parent for `child`. +public fun is_parent_of(parent: &Domain, child: &Domain): bool { + number_of_levels(parent) < number_of_levels(child) && &parent(child).labels == &parent.labels - } +} - fun validate_labels(labels: &vector) { - assert!(!labels.is_empty(), EInvalidDomain); +fun validate_labels(labels: &vector) { + assert!(!labels.is_empty(), EInvalidDomain); - let len = labels.length(); - let mut index = 0; + let len = labels.length(); + let mut index = 0; - while (index < len) { - let label = &labels[index]; - assert!(is_valid_label(label), EInvalidDomain); - index = index + 1; - } + while (index < len) { + let label = &labels[index]; + assert!(is_valid_label(label), EInvalidDomain); + index = index + 1; } +} - fun is_valid_label(label: &String): bool { - let len = label.length(); - let label_bytes = label.bytes(); - let mut index = 0; +fun is_valid_label(label: &String): bool { + let len = label.length(); + let label_bytes = label.bytes(); + let mut index = 0; - if (!(len >= MIN_LABEL_LENGTH && len <= MAX_LABEL_LENGTH)) { - return false - }; + if (!(len >= MIN_LABEL_LENGTH && len <= MAX_LABEL_LENGTH)) { + return false + }; - while (index < len) { - let character = label_bytes[index]; - let is_valid_character = - (0x61 <= character && character <= 0x7A) // a-z + while (index < len) { + let character = label_bytes[index]; + let is_valid_character = + (0x61 <= character && character <= 0x7A) // a-z || (0x30 <= character && character <= 0x39) // 0-9 - || (character == 0x2D && index != 0 && index != len - 1); // '-' not at beginning or end - - if (!is_valid_character) { - return false - }; + || (character == 0x2D && index != 0 && index != len - 1); // '-' not at beginning or end - index = index + 1; + if (!is_valid_character) { + return false }; - true - } + index = index + 1; + }; + + true +} - /// Splits a string `s` by the character `.` into a vector of subslices, excluding the `.` - fun split_by_dot(mut s: String): vector { - let dot = utf8(b"."); - let mut parts: vector = vector[]; - while (!s.is_empty()) { - let index_of_next_dot = s.index_of(&dot); - let part = s.sub_string(0, index_of_next_dot); - parts.push_back(part); - - let len = s.length(); - let start_of_next_part = if (index_of_next_dot == len) { - len - } else { - index_of_next_dot + 1 - }; - - s = s.sub_string(start_of_next_part, len); +/// Splits a string `s` by the character `.` into a vector of subslices, +/// excluding the `.` +fun split_by_dot(mut s: String): vector { + let dot = utf8(b"."); + let mut parts: vector = vector[]; + while (!s.is_empty()) { + let index_of_next_dot = s.index_of(&dot); + let part = s.sub_string(0, index_of_next_dot); + parts.push_back(part); + + let len = s.length(); + let start_of_next_part = if (index_of_next_dot == len) { + len + } else { + index_of_next_dot + 1 }; - parts - } + s = s.sub_string(start_of_next_part, len); + }; - // === Tests === + parts +} - #[test_only] - use sui::test_utils::assert_eq; +// === Tests === - #[test_only] - fun test_valid_domain(name: vector, expected_labels: vector>) { - let name = utf8(name); - let domain = new(name); - let expected_labels = prep_expected_labels(expected_labels); - assert_eq(domain.labels, expected_labels); - assert_eq(name, to_string(&domain)); +#[test_only] +use sui::test_utils::assert_eq; - // Validate `domain::label` function - let len = vector::length(&expected_labels); - let mut index = 0; +#[test_only] +fun test_valid_domain(name: vector, expected_labels: vector>) { + let name = utf8(name); + let domain = new(name); + let expected_labels = prep_expected_labels(expected_labels); + assert_eq(domain.labels, expected_labels); + assert_eq(name, to_string(&domain)); - while (index < len) { - let label = &expected_labels[index]; - assert_eq(*label, *label(&domain, index)); - index = index + 1; - } - } + // Validate `domain::label` function + let len = vector::length(&expected_labels); + let mut index = 0; - #[test_only] - fun prep_expected_labels(mut labels: vector>): vector { - let mut out = vector[]; - while (!labels.is_empty()) { - let label = labels.pop_back(); - out.push_back(utf8(label)); - }; - out + while (index < len) { + let label = &expected_labels[index]; + assert_eq(*label, *label(&domain, index)); + index = index + 1; } +} - #[test] - fun valid_domains() { - test_valid_domain(b"abc.123", vector[b"abc", b"123"]); - test_valid_domain(b"suins.sui", vector[b"suins", b"sui"]); - test_valid_domain(b"1.2.3.4.5.6.7.8.9.0.sui", vector[b"1", b"2", b"3", b"4", b"5", b"6", b"7", b"8", b"9", b"0", b"sui"]); - test_valid_domain(b"pay.mysten.sui", vector[b"pay", b"mysten", b"sui"]); - test_valid_domain(b"abcdefghijklmnopqrstuvxyz0123456789.move", vector[b"abcdefghijklmnopqrstuvxyz0123456789", b"move"]); - test_valid_domain(b"a----b.sui", vector[b"a----b", b"sui"]); - } +#[test_only] +fun prep_expected_labels(mut labels: vector>): vector { + let mut out = vector[]; + while (!labels.is_empty()) { + let label = labels.pop_back(); + out.push_back(utf8(label)); + }; + out +} - #[test_only] - fun expect_valid_label(label: vector, is_valid: bool) { - let label = utf8(label); - assert_eq(is_valid_label(&label), is_valid); - } +#[test] +fun valid_domains() { + test_valid_domain(b"abc.123", vector[b"abc", b"123"]); + test_valid_domain(b"suins.sui", vector[b"suins", b"sui"]); + test_valid_domain( + b"1.2.3.4.5.6.7.8.9.0.sui", + vector[ + b"1", + b"2", + b"3", + b"4", + b"5", + b"6", + b"7", + b"8", + b"9", + b"0", + b"sui", + ], + ); + test_valid_domain(b"pay.mysten.sui", vector[b"pay", b"mysten", b"sui"]); + test_valid_domain( + b"abcdefghijklmnopqrstuvxyz0123456789.move", + vector[b"abcdefghijklmnopqrstuvxyz0123456789", b"move"], + ); + test_valid_domain(b"a----b.sui", vector[b"a----b", b"sui"]); +} - #[test] - fun test_valid_labels() { - expect_valid_label(b"", false); - expect_valid_label(b"-", false); - expect_valid_label(b"-aaa", false); - expect_valid_label(b"aaa-", false); - expect_valid_label(b"a-a", true); - expect_valid_label(b"abcdefghijklmnopqrstuvxyz-0123456789", true); - } +#[test_only] +fun expect_valid_label(label: vector, is_valid: bool) { + let label = utf8(label); + assert_eq(is_valid_label(&label), is_valid); +} - #[test_only] - fun expect_is_subdomain(domain: vector, subdomain: vector, expected: bool) { - let domain = new(utf8(domain)); - let subdomain = new(utf8(subdomain)); - assert_eq(is_parent_of(&domain, &subdomain), expected); - } +#[test] +fun test_valid_labels() { + expect_valid_label(b"", false); + expect_valid_label(b"-", false); + expect_valid_label(b"-aaa", false); + expect_valid_label(b"aaa-", false); + expect_valid_label(b"a-a", true); + expect_valid_label(b"abcdefghijklmnopqrstuvxyz-0123456789", true); +} - #[test] - fun test_is_subdomain() { - expect_is_subdomain(b"mysten.sui", b"pay.mysten.sui", true); - expect_is_subdomain(b"pay.mysten.sui", b"mysten.sui", false); - expect_is_subdomain(b"mysten.sui", b"pay.move.sui", false); - } +#[test_only] +fun expect_is_subdomain( + domain: vector, + subdomain: vector, + expected: bool, +) { + let domain = new(utf8(domain)); + let subdomain = new(utf8(subdomain)); + assert_eq(is_parent_of(&domain, &subdomain), expected); +} - #[test] - fun split_simple() { - let s = utf8(b"a.b"); - let expected = vector[utf8(b"a"), utf8(b"b")]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun test_is_subdomain() { + expect_is_subdomain(b"mysten.sui", b"pay.mysten.sui", true); + expect_is_subdomain(b"pay.mysten.sui", b"mysten.sui", false); + expect_is_subdomain(b"mysten.sui", b"pay.move.sui", false); +} - #[test] - fun split_simple_2() { - let s = utf8(b"pay.narwhal.sui"); - let expected = vector[utf8(b"pay"), utf8(b"narwhal"), utf8(b"sui")]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_simple() { + let s = utf8(b"a.b"); + let expected = vector[utf8(b"a"), utf8(b"b")]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun split_string_with_no_dots() { - let s = utf8(b"abc"); - let expected = vector[utf8(b"abc")]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_simple_2() { + let s = utf8(b"pay.narwhal.sui"); + let expected = vector[utf8(b"pay"), utf8(b"narwhal"), utf8(b"sui")]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun split_string_surrounded_by_dots() { - let s = utf8(b".a."); - let expected = vector[utf8(vector::empty()), utf8(b"a")]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_string_with_no_dots() { + let s = utf8(b"abc"); + let expected = vector[utf8(b"abc")]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun split_empty_string() { - let s = utf8(vector::empty()); - let expected = vector[]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_string_surrounded_by_dots() { + let s = utf8(b".a."); + let expected = vector[utf8(vector::empty()), utf8(b"a")]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun split_one_dot() { - let s = utf8(b"."); - let expected = vector[utf8(vector::empty())]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_empty_string() { + let s = utf8(vector::empty()); + let expected = vector[]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun split_two_dots() { - let s = utf8(b".."); - let expected = vector[utf8(vector::empty()), utf8(vector::empty())]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_one_dot() { + let s = utf8(b"."); + let expected = vector[utf8(vector::empty())]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun split_three_dots() { - let s = utf8(b"..."); - let expected = vector[utf8(vector::empty()), utf8(vector::empty()), utf8(vector::empty())]; - let actual = split_by_dot(s); - assert_eq(actual, expected); - } +#[test] +fun split_two_dots() { + let s = utf8(b".."); + let expected = vector[utf8(vector::empty()), utf8(vector::empty())]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - #[test] - fun derive_parent(){ - let parent = new(utf8(b"parent.sui")); - let child = new(utf8(b"child.parent.sui")); +#[test] +fun split_three_dots() { + let s = utf8(b"..."); + let expected = vector[ + utf8(vector::empty()), + utf8(vector::empty()), + utf8(vector::empty()), + ]; + let actual = split_by_dot(s); + assert_eq(actual, expected); +} - assert!(parent(&child) == parent, 0); - } +#[test] +fun derive_parent() { + let parent = new(utf8(b"parent.sui")); + let child = new(utf8(b"child.parent.sui")); + + assert!(parent(&child) == parent, 0); } diff --git a/packages/suins/sources/name_record.move b/packages/suins/sources/name_record.move index cd6a57a8..4b442b06 100644 --- a/packages/suins/sources/name_record.move +++ b/packages/suins/sources/name_record.move @@ -1,116 +1,123 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// The `NameRecord` is a struct that represents a single record in the registry. +/// The `NameRecord` is a struct that represents a single record in the +/// registry. /// Can be replaced by any other data structure due to the way `NameRecord`s are /// stored and managed. SuiNS has no direct and permanent dependency on this /// module. -module suins::name_record { - use std::string::String; - - use sui::{clock::{timestamp_ms, Clock}, vec_map::{Self, VecMap}}; - - use suins::constants; - - /// A single record in the registry. - public struct NameRecord has copy, store, drop { - /// The ID of the `SuinsRegistration` assigned to this record. - /// - /// The owner of the corrisponding `SuinsRegistration` has the rights to - /// be able to change and adjust the `target_address` of this domain. - /// - /// It is possible that the ID changes if the record expires and is - /// purchased by someone else. - nft_id: ID, - /// Timestamp in milliseconds when the record expires. - expiration_timestamp_ms: u64, - /// The target address that this domain points to - target_address: Option
, - /// Additional data which may be stored in a record - data: VecMap, - } - - /// Create a new NameRecord. - public fun new( - nft_id: ID, - expiration_timestamp_ms: u64, - ): NameRecord { - NameRecord { - nft_id, - expiration_timestamp_ms, - target_address: option::none(), - data: vec_map::empty(), - } - } +module suins::name_record; - /// Create a `leaf` NameRecord. - public fun new_leaf( - parent_id: ID, - target_address: Option
- ): NameRecord { - NameRecord { - nft_id: parent_id, - expiration_timestamp_ms: constants::leaf_expiration_timestamp(), - target_address, - data: vec_map::empty() - } - } - - // === Setters === +use std::string::String; +use sui::clock::{timestamp_ms, Clock}; +use sui::vec_map::{Self, VecMap}; +use suins::constants; - /// Set data as a vec_map directly overriding the data set in the - /// registration self. This simplifies the editing flow and gives - /// the user and clients a fine-grained control over custom data. +/// A single record in the registry. +public struct NameRecord has copy, store, drop { + /// The ID of the `SuinsRegistration` assigned to this record. /// - /// Here's a meta example of how a PTB would look like: - /// ``` - /// let record = moveCall('data', [domain_name]); - /// moveCall('vec_map::insert', [record.data, key, value]); - /// moveCall('vec_map::remove', [record.data, other_key]); - /// moveCall('set_data', [domain_name, record.data]); - /// ``` - public fun set_data(self: &mut NameRecord, data: VecMap) { - self.data = data; - } + /// The owner of the corrisponding `SuinsRegistration` has the rights to + /// be able to change and adjust the `target_address` of this domain. + /// + /// It is possible that the ID changes if the record expires and is + /// purchased by someone else. + nft_id: ID, + /// Timestamp in milliseconds when the record expires. + expiration_timestamp_ms: u64, + /// The target address that this domain points to + target_address: Option
, + /// Additional data which may be stored in a record + data: VecMap, +} - /// Set the `target_address` field of the `NameRecord`. - public fun set_target_address(self: &mut NameRecord, new_address: Option
) { - self.target_address = new_address; +/// Create a new NameRecord. +public fun new(nft_id: ID, expiration_timestamp_ms: u64): NameRecord { + NameRecord { + nft_id, + expiration_timestamp_ms, + target_address: option::none(), + data: vec_map::empty(), } +} - public fun set_expiration_timestamp_ms( - self: &mut NameRecord, - expiration_timestamp_ms: u64, - ) { - self.expiration_timestamp_ms = expiration_timestamp_ms; +/// Create a `leaf` NameRecord. +public fun new_leaf( + parent_id: ID, + target_address: Option
, +): NameRecord { + NameRecord { + nft_id: parent_id, + expiration_timestamp_ms: constants::leaf_expiration_timestamp(), + target_address, + data: vec_map::empty(), } +} - // === Getters === +// === Setters === + +/// Set data as a vec_map directly overriding the data set in the +/// registration self. This simplifies the editing flow and gives +/// the user and clients a fine-grained control over custom data. +/// +/// Here's a meta example of how a PTB would look like: +/// ``` +/// let record = moveCall('data', [domain_name]); +/// moveCall('vec_map::insert', [record.data, key, value]); +/// moveCall('vec_map::remove', [record.data, other_key]); +/// moveCall('set_data', [domain_name, record.data]); +/// ``` +public fun set_data(self: &mut NameRecord, data: VecMap) { + self.data = data; +} - /// Check if the record has expired. - public fun has_expired(self: &NameRecord, clock: &Clock): bool { - self.expiration_timestamp_ms < timestamp_ms(clock) - } +/// Set the `target_address` field of the `NameRecord`. +public fun set_target_address( + self: &mut NameRecord, + new_address: Option
, +) { + self.target_address = new_address; +} - /// Check if the record has expired, taking into account the grace period. - public fun has_expired_past_grace_period(self: &NameRecord, clock: &Clock): bool { - (self.expiration_timestamp_ms + constants::grace_period_ms()) < timestamp_ms(clock) - } +public fun set_expiration_timestamp_ms( + self: &mut NameRecord, + expiration_timestamp_ms: u64, +) { + self.expiration_timestamp_ms = expiration_timestamp_ms; +} - /// Checks whether a name_record is a `leaf` record. - public fun is_leaf_record(self: &NameRecord): bool { - self.expiration_timestamp_ms == constants::leaf_expiration_timestamp() - } +// === Getters === - /// Read the `data` field from the `NameRecord`. - public fun data(self: &NameRecord): &VecMap { &self.data } +/// Check if the record has expired. +public fun has_expired(self: &NameRecord, clock: &Clock): bool { + self.expiration_timestamp_ms < timestamp_ms(clock) +} - /// Read the `target_address` field from the `NameRecord`. - public fun target_address(self: &NameRecord): Option
{ self.target_address } +/// Check if the record has expired, taking into account the grace period. +public fun has_expired_past_grace_period( + self: &NameRecord, + clock: &Clock, +): bool { + (self.expiration_timestamp_ms + constants::grace_period_ms()) < timestamp_ms(clock) +} + +/// Checks whether a name_record is a `leaf` record. +public fun is_leaf_record(self: &NameRecord): bool { + self.expiration_timestamp_ms == constants::leaf_expiration_timestamp() +} + +/// Read the `data` field from the `NameRecord`. +public fun data(self: &NameRecord): &VecMap { &self.data } + +/// Read the `target_address` field from the `NameRecord`. +public fun target_address(self: &NameRecord): Option
{ + self.target_address +} - /// Read the `nft_id` field from the `NameRecord`. - public fun nft_id(self: &NameRecord): ID { self.nft_id } +/// Read the `nft_id` field from the `NameRecord`. +public fun nft_id(self: &NameRecord): ID { self.nft_id } - /// Read the `expiration_timestamp_ms` field from the `NameRecord`. - public fun expiration_timestamp_ms(self: &NameRecord): u64 { self.expiration_timestamp_ms } +/// Read the `expiration_timestamp_ms` field from the `NameRecord`. +public fun expiration_timestamp_ms(self: &NameRecord): u64 { + self.expiration_timestamp_ms } diff --git a/packages/suins/sources/registry.move b/packages/suins/sources/registry.move index f3f3b209..da6e5a21 100644 --- a/packages/suins/sources/registry.move +++ b/packages/suins/sources/registry.move @@ -1,451 +1,493 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -module suins::registry { - use std::{option::{none, some}, string::String}; +module suins::registry; + +use std::option::{none, some}; +use std::string::String; +use sui::clock::Clock; +use sui::table::{Self, Table}; +use sui::vec_map::VecMap; +use suins::domain::Domain; +use suins::name_record::{Self, NameRecord}; +use suins::subdomain_registration::{Self, SubDomainRegistration}; +use suins::suins::AdminCap; +use suins::suins_registration::{Self as nft, SuinsRegistration}; + +/// The `SuinsRegistration` has expired. +const ENftExpired: u64 = 0; +/// Trying to override a record that is not expired. +const ERecordNotExpired: u64 = 1; +/// The `SuinsRegistration` does not match the `NameRecord`. +const EIdMismatch: u64 = 2; +/// The `NameRecord` has expired. +const ERecordExpired: u64 = 3; +/// The reverse lookup record does not match the `NameRecord`. +const ERecordMismatch: u64 = 4; +/// Trying to add a reverse lookup record while the target is empty. +const ETargetNotSet: u64 = 5; +/// Trying to remove or operate on a non-leaf record as if it were a leaf +/// record. +const ENotLeafRecord: u64 = 6; +/// Trying to add a leaf record for a TLD or SLD. +const EInvalidDepth: u64 = 7; +/// Trying to lookup a record that doesn't exist. +const ERecordNotFound: u64 = 8; + +/// The `Registry` object. Attached as a dynamic field to the `SuiNS` object, +/// and the `suins` module controls the access to the `Registry`. +/// +/// Contains two tables necessary for the lookup. +public struct Registry has store { + /// The `registry` table maps `Domain` to `NameRecord`. + /// Added / replaced in the `add_record` function. + registry: Table, + /// The `reverse_registry` table maps `address` to `domain_name`. + /// Updated in the `set_reverse_lookup` function. + reverse_registry: Table, +} - use sui::{table::{Self, Table}, clock::Clock, vec_map::VecMap}; +public fun new(_: &AdminCap, ctx: &mut TxContext): Registry { + Registry { + registry: table::new(ctx), + reverse_registry: table::new(ctx), + } +} - use suins::{ - suins_registration::{Self as nft, SuinsRegistration}, - name_record::{Self, NameRecord}, - domain::Domain, - suins::AdminCap, - subdomain_registration::{Self, SubDomainRegistration} - }; +/// Attemps to add a new record to the registry without looking at the grace +/// period. +/// Currently used for subdomains where there's no grace period to respect. +/// Returns a `SuinsRegistration` upon success. +public fun add_record_ignoring_grace_period( + self: &mut Registry, + domain: Domain, + no_years: u8, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + self.internal_add_record(domain, no_years, clock, false, ctx) +} - /// The `SuinsRegistration` has expired. - const ENftExpired: u64 = 0; - /// Trying to override a record that is not expired. - const ERecordNotExpired: u64 = 1; - /// The `SuinsRegistration` does not match the `NameRecord`. - const EIdMismatch: u64 = 2; - /// The `NameRecord` has expired. - const ERecordExpired: u64 = 3; - /// The reverse lookup record does not match the `NameRecord`. - const ERecordMismatch: u64 = 4; - /// Trying to add a reverse lookup record while the target is empty. - const ETargetNotSet: u64 = 5; - /// Trying to remove or operate on a non-leaf record as if it were a leaf record. - const ENotLeafRecord: u64 = 6; - /// Trying to add a leaf record for a TLD or SLD. - const EInvalidDepth: u64 = 7; - /// Trying to lookup a record that doesn't exist. - const ERecordNotFound: u64 = 8; - - /// The `Registry` object. Attached as a dynamic field to the `SuiNS` object, - /// and the `suins` module controls the access to the `Registry`. - /// - /// Contains two tables necessary for the lookup. - public struct Registry has store { - /// The `registry` table maps `Domain` to `NameRecord`. - /// Added / replaced in the `add_record` function. - registry: Table, - /// The `reverse_registry` table maps `address` to `domain_name`. - /// Updated in the `set_reverse_lookup` function. - reverse_registry: Table, - } +/// Attempts to add a new record to the registry and returns a +/// `SuinsRegistration` upon success. +/// Only use with second-level names. Enforces a `grace_period` by default. +/// Not suitable for subdomains (unless a grace period is needed). +public fun add_record( + self: &mut Registry, + domain: Domain, + no_years: u8, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + self.internal_add_record(domain, no_years, clock, true, ctx) +} - public fun new(_: &AdminCap, ctx: &mut TxContext): Registry { - Registry { - registry: table::new(ctx), - reverse_registry: table::new(ctx), +/// Attempts to burn an NFT and get storage rebates. +/// Only works if the NFT has expired. +public fun burn_registration_object( + self: &mut Registry, + nft: SuinsRegistration, + clock: &Clock, +) { + // First we make sure that the SuinsRegistration object has expired. + assert!(nft.has_expired(clock), ERecordNotExpired); + + let domain = nft.domain(); + + // Then, if the registry still has a record for this domain and the NFT ID + // matches, we remove it. + if (self.registry.contains(domain)) { + let record = &self.registry[domain]; + + // We wanna remove the record only if the NFT ID matches. + if (record.nft_id() == object::id(&nft)) { + let record = self.registry.remove(domain); + self.handle_invalidate_reverse_record( + &domain, + record.target_address(), + none(), + ); } - } + }; + // burn the NFT. + nft.burn(); +} - /// Attemps to add a new record to the registry without looking at the grace period. - /// Currently used for subdomains where there's no grace period to respect. - /// Returns a `SuinsRegistration` upon success. - public fun add_record_ignoring_grace_period( - self: &mut Registry, - domain: Domain, - no_years: u8, - clock: &Clock, - ctx: &mut TxContext, - ): SuinsRegistration { - self.internal_add_record(domain, no_years, clock, false, ctx) - } +/// Allow creation of subdomain wrappers only to authorized modules. +public fun wrap_subdomain( + _: &mut Registry, + nft: SuinsRegistration, + clock: &Clock, + ctx: &mut TxContext, +): SubDomainRegistration { + subdomain_registration::new(nft, clock, ctx) +} - /// Attempts to add a new record to the registry and returns a - /// `SuinsRegistration` upon success. - /// Only use with second-level names. Enforces a `grace_period` by default. - /// Not suitable for subdomains (unless a grace period is needed). - public fun add_record( - self: &mut Registry, - domain: Domain, - no_years: u8, - clock: &Clock, - ctx: &mut TxContext, - ): SuinsRegistration { - self.internal_add_record(domain, no_years, clock, true, ctx) - } +/// Attempts to burn a subdomain registration object, +/// and also invalidates any records in the registry / reverse registry. +public fun burn_subdomain_object( + self: &mut Registry, + nft: SubDomainRegistration, + clock: &Clock, +) { + let nft = nft.burn(clock); + self.burn_registration_object(nft, clock); +} - /// Attempts to burn an NFT and get storage rebates. - /// Only works if the NFT has expired. - public fun burn_registration_object( - self: &mut Registry, - nft: SuinsRegistration, - clock: &Clock - ) { - // First we make sure that the SuinsRegistration object has expired. - assert!(nft.has_expired(clock), ERecordNotExpired); - - let domain = nft.domain(); - - // Then, if the registry still has a record for this domain and the NFT ID matches, we remove it. - if (self.registry.contains(domain)) { - let record = &self.registry[domain]; - - // We wanna remove the record only if the NFT ID matches. - if (record.nft_id() == object::id(&nft)) { - let record = self.registry.remove(domain); - self.handle_invalidate_reverse_record(&domain, record.target_address(), none()); - } - }; - // burn the NFT. - nft.burn(); - } +/// Adds a `leaf` record to the registry. +/// A `leaf` record is a record that is a subdomain and doesn't have +/// an equivalent `SuinsRegistration` object. +/// +/// Instead, the parent's `SuinsRegistration` object is used to manage +/// target_address & remove it / determine expiration. +/// +/// 1. Leaf records can't have children. They only work as a resolving +/// mechanism. +/// 2. Leaf records must always have a `target` address (can't point to `none`). +/// 3. Leaf records do not expire. Their expiration date is actually what +/// defines their type. +/// +/// Leaf record's expiration is defined by the parent's expiration. Since the +/// parent can only be a `node`, +/// we need to check that the parent's NFT_ID is valid & hasn't expired. +public fun add_leaf_record( + self: &mut Registry, + domain: Domain, + clock: &Clock, + target: address, + _ctx: &mut TxContext, +) { + assert!(domain.is_subdomain(), EInvalidDepth); + + // get the parent of the domain + let parent = domain.parent(); + let option_parent_name_record = self.lookup(parent); + + assert!(option_parent_name_record.is_some(), ERecordNotFound); + + // finds existing parent record + let parent_name_record = option_parent_name_record.borrow(); + + // Make sure that the parent isn't expired (because leaf record is invalid + // in that case). + // Ignores grace period is it's only there so you don't accidently forget to + // renew your name. + assert!(!parent_name_record.has_expired(clock), ERecordExpired); + + // Removes an existing record if it exists and is expired. + self.remove_existing_record_if_exists_and_expired(domain, clock, false); + + // adds the `leaf` record to the registry. + self + .registry + .add( + domain, + name_record::new_leaf(parent_name_record.nft_id(), some(target)), + ); +} - /// Allow creation of subdomain wrappers only to authorized modules. - public fun wrap_subdomain( - _: &mut Registry, - nft: SuinsRegistration, - clock: &Clock, - ctx: &mut TxContext - ): SubDomainRegistration { - subdomain_registration::new(nft, clock, ctx) - } +/// Can be used to remove a leaf record. +/// Leaf records do not have any symmetrical `SuinsRegistration` object. +/// Authorization of who calls this is delegated to the authorized module that +/// calls this. +public fun remove_leaf_record(self: &mut Registry, domain: Domain) { + // We can only call remove on a leaf record. + assert!(self.is_leaf_record(domain), ENotLeafRecord); + + // if it's a leaf record, there's no `SuinsRegistration` object. + // We can just go ahead and remove the name_record, and invalidate the + // reverse record (if any). + let record = self.registry.remove(domain); + let old_target_address = record.target_address(); + + self.handle_invalidate_reverse_record(&domain, old_target_address, none()); +} - /// Attempts to burn a subdomain registration object, - /// and also invalidates any records in the registry / reverse registry. - public fun burn_subdomain_object( - self: &mut Registry, - nft: SubDomainRegistration, - clock: &Clock - ) { - let nft = nft.burn(clock); - self.burn_registration_object(nft, clock); - } +public fun set_target_address( + self: &mut Registry, + domain: Domain, + new_target: Option
, +) { + let record = &mut self.registry[domain]; + let old_target = record.target_address(); - /// Adds a `leaf` record to the registry. - /// A `leaf` record is a record that is a subdomain and doesn't have - /// an equivalent `SuinsRegistration` object. - /// - /// Instead, the parent's `SuinsRegistration` object is used to manage target_address & remove it / determine expiration. - /// - /// 1. Leaf records can't have children. They only work as a resolving mechanism. - /// 2. Leaf records must always have a `target` address (can't point to `none`). - /// 3. Leaf records do not expire. Their expiration date is actually what defines their type. - /// - /// Leaf record's expiration is defined by the parent's expiration. Since the parent can only be a `node`, - /// we need to check that the parent's NFT_ID is valid & hasn't expired. - public fun add_leaf_record( - self: &mut Registry, - domain: Domain, - clock: &Clock, - target: address, - _ctx: &mut TxContext - ) { - assert!(domain.is_subdomain(), EInvalidDepth); - - // get the parent of the domain - let parent = domain.parent(); - let option_parent_name_record = self.lookup(parent); - - assert!(option_parent_name_record.is_some(), ERecordNotFound); - - // finds existing parent record - let parent_name_record = option_parent_name_record.borrow(); - - // Make sure that the parent isn't expired (because leaf record is invalid in that case). - // Ignores grace period is it's only there so you don't accidently forget to renew your name. - assert!(!parent_name_record.has_expired(clock), ERecordExpired); - - // Removes an existing record if it exists and is expired. - self.remove_existing_record_if_exists_and_expired(domain, clock, false); - - // adds the `leaf` record to the registry. - self.registry.add(domain, name_record::new_leaf(parent_name_record.nft_id(), some(target))); - } + record.set_target_address(new_target); + self.handle_invalidate_reverse_record(&domain, old_target, new_target); +} - /// Can be used to remove a leaf record. - /// Leaf records do not have any symmetrical `SuinsRegistration` object. - /// Authorization of who calls this is delegated to the authorized module that calls this. - public fun remove_leaf_record( - self: &mut Registry, - domain: Domain, - ) { - // We can only call remove on a leaf record. - assert!(self.is_leaf_record(domain), ENotLeafRecord); - - // if it's a leaf record, there's no `SuinsRegistration` object. - // We can just go ahead and remove the name_record, and invalidate the reverse record (if any). - let record = self.registry.remove(domain); - let old_target_address = record.target_address(); - - self.handle_invalidate_reverse_record(&domain, old_target_address, none()); - } +public fun unset_reverse_lookup(self: &mut Registry, address: address) { + self.reverse_registry.remove(address); +} - public fun set_target_address( - self: &mut Registry, - domain: Domain, - new_target: Option
, - ) { - let record = &mut self.registry[domain]; - let old_target = record.target_address(); +/// Reverse lookup can only be set for the record that has the target address. +public fun set_reverse_lookup( + self: &mut Registry, + address: address, + domain: Domain, +) { + let record = &self.registry[domain]; + let target = record.target_address(); + + assert!(target.is_some(), ETargetNotSet); + assert!(some(address) == target, ERecordMismatch); + + if (self.reverse_registry.contains(address)) { + *self.reverse_registry.borrow_mut(address) = domain; + } else { + self.reverse_registry.add(address, domain); + }; +} - record.set_target_address(new_target); - self.handle_invalidate_reverse_record(&domain, old_target, new_target); - } +/// Update the `expiration_timestamp_ms` of the given `SuinsRegistration` and +/// `NameRecord`. Requires the `SuinsRegistration` to make sure that both +/// timestamps are in sync. +public fun set_expiration_timestamp_ms( + self: &mut Registry, + nft: &mut SuinsRegistration, + domain: Domain, + expiration_timestamp_ms: u64, +) { + let record = &mut self.registry[domain]; + + assert!(object::id(nft) == record.nft_id(), EIdMismatch); + record.set_expiration_timestamp_ms(expiration_timestamp_ms); + nft.set_expiration_timestamp_ms(expiration_timestamp_ms); +} - public fun unset_reverse_lookup(self: &mut Registry, address: address) { - self.reverse_registry.remove(address); - } +/// Update the `data` of the given `NameRecord` using a `SuinsRegistration`. +/// Use with caution and validate(!!) that any system fields are not removed +/// (accidently), +/// when building authorized packages that can write the metadata field. +public fun set_data( + self: &mut Registry, + domain: Domain, + data: VecMap, +) { + let record = &mut self.registry[domain]; + record.set_data(data); +} - /// Reverse lookup can only be set for the record that has the target address. - public fun set_reverse_lookup( - self: &mut Registry, - address: address, - domain: Domain, - ) { - let record = &self.registry[domain]; - let target = record.target_address(); +// === Reads === - assert!(target.is_some(), ETargetNotSet); - assert!(some(address) == target, ERecordMismatch); +/// Check whether the given `domain` is registered in the `Registry`. +public fun has_record(self: &Registry, domain: Domain): bool { + self.registry.contains(domain) +} - if (self.reverse_registry.contains(address)) { - *self.reverse_registry.borrow_mut(address) = domain; - } else { - self.reverse_registry.add(address, domain); - }; +/// Returns the `NameRecord` associated with the given domain or None. +public fun lookup(self: &Registry, domain: Domain): Option { + if (self.registry.contains(domain)) { + let record = &self.registry[domain]; + some(*record) + } else { + none() } +} - /// Update the `expiration_timestamp_ms` of the given `SuinsRegistration` and - /// `NameRecord`. Requires the `SuinsRegistration` to make sure that both - /// timestamps are in sync. - public fun set_expiration_timestamp_ms( - self: &mut Registry, - nft: &mut SuinsRegistration, - domain: Domain, - expiration_timestamp_ms: u64, - ) { - let record = &mut self.registry[domain]; - - assert!(object::id(nft) == record.nft_id(), EIdMismatch); - record.set_expiration_timestamp_ms(expiration_timestamp_ms); - nft.set_expiration_timestamp_ms(expiration_timestamp_ms); +/// Returns the `domain_name` associated with the given address or None. +public fun reverse_lookup(self: &Registry, address: address): Option { + if (self.reverse_registry.contains(address)) { + some(self.reverse_registry[address]) + } else { + none() } +} - /// Update the `data` of the given `NameRecord` using a `SuinsRegistration`. - /// Use with caution and validate(!!) that any system fields are not removed (accidently), - /// when building authorized packages that can write the metadata field. - public fun set_data( - self: &mut Registry, - domain: Domain, - data: VecMap - ) { - let record = &mut self.registry[domain]; - record.set_data(data); - } +/// Asserts that the provided NFT: +/// 1. Matches the ID in the corresponding `Record` +/// 2. Has not expired (does not take into account the grace period) +public fun assert_nft_is_authorized( + self: &Registry, + nft: &SuinsRegistration, + clock: &Clock, +) { + let domain = nft.domain(); + let record = &self.registry[domain]; + + // The NFT does not + assert!(object::id(nft) == record.nft_id(), EIdMismatch); + assert!(!record.has_expired(clock), ERecordExpired); + assert!(!nft.has_expired(clock), ENftExpired); +} - // === Reads === +/// Returns the `data` associated with the given `Domain`. +public fun get_data(self: &Registry, domain: Domain): &VecMap { + let record = &self.registry[domain]; + record.data() +} - /// Check whether the given `domain` is registered in the `Registry`. - public fun has_record(self: &Registry, domain: Domain): bool { - self.registry.contains(domain) - } +// === Private Functions === + +/// Checks whether a subdomain record is `leaf`. +/// `leaf` record: a record whose target address can only be set by the parent, +/// hence the nft_id points to the parent's ID. Leaf records can't create +/// subdomains +/// and don't have their own `SuinsRegistration` object Cap. The +/// `SuinsRegistration` of the parent +/// is the one that manages them. +/// +fun is_leaf_record(self: &Registry, domain: Domain): bool { + if (!domain.is_subdomain()) { + return false + }; - /// Returns the `NameRecord` associated with the given domain or None. - public fun lookup(self: &Registry, domain: Domain): Option { - if (self.registry.contains(domain)) { - let record = &self.registry[domain]; - some(*record) - } else { - none() - } - } + let option_name_record = self.lookup(domain); - /// Returns the `domain_name` associated with the given address or None. - public fun reverse_lookup(self: &Registry, address: address): Option { - if (self.reverse_registry.contains(address)) { - some(self.reverse_registry[address]) - } else { - none() - } - } + if (option_name_record.is_none()) { + return false + }; - /// Asserts that the provided NFT: - /// 1. Matches the ID in the corresponding `Record` - /// 2. Has not expired (does not take into account the grace period) - public fun assert_nft_is_authorized(self: &Registry, nft: &SuinsRegistration, clock: &Clock) { - let domain = nft.domain(); - let record = &self.registry[domain]; + option_name_record.borrow().is_leaf_record() +} - // The NFT does not - assert!(object::id(nft) == record.nft_id(), EIdMismatch); - assert!(!record.has_expired(clock), ERecordExpired); - assert!(!nft.has_expired(clock), ENftExpired); - } +/// An internal helper to add a record +fun internal_add_record( + self: &mut Registry, + domain: Domain, + no_years: u8, + clock: &Clock, + with_grace_period: bool, + ctx: &mut TxContext, +): SuinsRegistration { + self.remove_existing_record_if_exists_and_expired( + domain, + clock, + with_grace_period, + ); + + // If we've made it to this point then we know that we are able to + // register an entry for this domain. + let nft = nft::new(domain, no_years, clock, ctx); + let name_record = name_record::new( + object::id(&nft), + nft.expiration_timestamp_ms(), + ); + self.registry.add(domain, name_record); + nft +} - /// Returns the `data` associated with the given `Domain`. - public fun get_data(self: &Registry, domain: Domain): &VecMap { - let record = &self.registry[domain]; - record.data() - } +fun remove_existing_record_if_exists_and_expired( + self: &mut Registry, + domain: Domain, + clock: &Clock, + with_grace_period: bool, +) { + // if the domain is not part of the registry, we can override. + if (!self.registry.contains(domain)) return; + + // Remove the record and assert that it has expired (past the grace period + // if applicable) + let record = self.registry.remove(domain); + + // Special case for leaf records, we can override them iff their parent has + // changed or has expired. + if (record.is_leaf_record()) { + // find the parent of the leaf record. + let option_parent_name_record = self.lookup(domain.parent()); + + // if there's a parent (if not, we can just remove it), we need to check + // if the parent is valid. + // -> If the parent is valid, we need to check if the parent is expired. + // -> If the parent is not valid (nft_id has changed), or if the parent + // doesn't exist anymore (owner burned it), we can override the leaf + // record. + if (option_parent_name_record.is_some()) { + let parent_name_record = option_parent_name_record.borrow(); + + // If the parent is the same and hasn't expired, we can't override + // the leaf record like this. + // We need to first remove + then call create (to protect accidental + // overrides). + if (parent_name_record.nft_id() == record.nft_id()) { + assert!( + parent_name_record.has_expired(clock), + ERecordNotExpired, + ); + }; + } + } else if (with_grace_period) { + assert!(record.has_expired_past_grace_period(clock), ERecordNotExpired); + } else { + assert!(record.has_expired(clock), ERecordNotExpired); + }; - // === Private Functions === - - /// Checks whether a subdomain record is `leaf`. - /// `leaf` record: a record whose target address can only be set by the parent, - /// hence the nft_id points to the parent's ID. Leaf records can't create subdomains - /// and don't have their own `SuinsRegistration` object Cap. The `SuinsRegistration` of the parent - /// is the one that manages them. - /// - fun is_leaf_record( - self: &Registry, - domain: Domain - ): bool { - if (!domain.is_subdomain()) { - return false - }; - - let option_name_record = self.lookup(domain); - - if (option_name_record.is_none()) { - return false - }; - - option_name_record.borrow().is_leaf_record() - } + let old_target_address = record.target_address(); + self.handle_invalidate_reverse_record(&domain, old_target_address, none()); +} - /// An internal helper to add a record - fun internal_add_record( - self: &mut Registry, - domain: Domain, - no_years: u8, - clock: &Clock, - with_grace_period: bool, - ctx: &mut TxContext, - ): SuinsRegistration { - self.remove_existing_record_if_exists_and_expired(domain, clock, with_grace_period); - - // If we've made it to this point then we know that we are able to - // register an entry for this domain. - let nft = nft::new(domain, no_years, clock, ctx); - let name_record = name_record::new(object::id(&nft), nft.expiration_timestamp_ms()); - self.registry.add(domain, name_record); - nft - } +fun handle_invalidate_reverse_record( + self: &mut Registry, + domain: &Domain, + old_target_address: Option
, + new_target_address: Option
, +) { + if (old_target_address == new_target_address) { + return + }; - fun remove_existing_record_if_exists_and_expired( - self: &mut Registry, - domain: Domain, - clock: &Clock, - with_grace_period: bool, - ) { - // if the domain is not part of the registry, we can override. - if (!self.registry.contains(domain)) return; - - // Remove the record and assert that it has expired (past the grace period if applicable) - let record = self.registry.remove(domain); - - // Special case for leaf records, we can override them iff their parent has changed or has expired. - if (record.is_leaf_record()) { - // find the parent of the leaf record. - let option_parent_name_record = self.lookup(domain.parent()); - - // if there's a parent (if not, we can just remove it), we need to check if the parent is valid. - // -> If the parent is valid, we need to check if the parent is expired. - // -> If the parent is not valid (nft_id has changed), or if the parent doesn't exist anymore (owner burned it), we can override the leaf record. - if (option_parent_name_record.is_some()) { - let parent_name_record = option_parent_name_record.borrow(); - - // If the parent is the same and hasn't expired, we can't override the leaf record like this. - // We need to first remove + then call create (to protect accidental overrides). - if (parent_name_record.nft_id() == record.nft_id()) { - assert!(parent_name_record.has_expired(clock), ERecordNotExpired); - }; - } - }else if (with_grace_period) { - assert!(record.has_expired_past_grace_period(clock), ERecordNotExpired); - } else { - assert!(record.has_expired(clock), ERecordNotExpired); - }; - - let old_target_address = record.target_address(); - self.handle_invalidate_reverse_record(&domain, old_target_address, none()); - } + if (old_target_address.is_none()) { + return + }; - fun handle_invalidate_reverse_record( - self: &mut Registry, - domain: &Domain, - old_target_address: Option
, - new_target_address: Option
, - ) { - if (old_target_address == new_target_address) { - return - }; - - if (old_target_address.is_none()) { - return - }; - - let old_target_address = old_target_address.destroy_some(); - let reverse_registry = &mut self.reverse_registry; - - if (reverse_registry.contains(old_target_address)) { - let default_domain = &reverse_registry[old_target_address]; - if (default_domain == domain) { - reverse_registry.remove(old_target_address); - } - }; - } + let old_target_address = old_target_address.destroy_some(); + let reverse_registry = &mut self.reverse_registry; - // === Test Functions === - #[test_only] use suins::suins::{add_registry, SuiNS}; + if (reverse_registry.contains(old_target_address)) { + let default_domain = &reverse_registry[old_target_address]; + if (default_domain == domain) { + reverse_registry.remove(old_target_address); + } + }; +} - #[test_only] - public fun init_for_testing(cap: &AdminCap, suins: &mut SuiNS, ctx: &mut TxContext) { - add_registry(cap, suins, new(cap, ctx)); - } +// === Test Functions === +#[test_only] +use suins::suins::{add_registry, SuiNS}; + +#[test_only] +public fun init_for_testing( + cap: &AdminCap, + suins: &mut SuiNS, + ctx: &mut TxContext, +) { + add_registry(cap, suins, new(cap, ctx)); +} - #[test_only] - /// Create a new `Registry` for testing Purposes. - public fun new_for_testing(ctx: &mut TxContext): Registry { - Registry { - registry: table::new(ctx), - reverse_registry: table::new(ctx), - } +#[test_only] +/// Create a new `Registry` for testing Purposes. +public fun new_for_testing(ctx: &mut TxContext): Registry { + Registry { + registry: table::new(ctx), + reverse_registry: table::new(ctx), } +} - #[test_only] - public fun remove_record_for_testing( - self: &mut Registry, - domain: Domain, - ): NameRecord { - self.registry.remove(domain) - } +#[test_only] +public fun remove_record_for_testing( + self: &mut Registry, + domain: Domain, +): NameRecord { + self.registry.remove(domain) +} - #[test_only] - public fun destroy_empty_for_testing(self: Registry) { - let Registry { - registry, - reverse_registry, - } = self; +#[test_only] +public fun destroy_empty_for_testing(self: Registry) { + let Registry { + registry, + reverse_registry, + } = self; - registry.destroy_empty(); - reverse_registry.destroy_empty(); - } + registry.destroy_empty(); + reverse_registry.destroy_empty(); +} - #[test_only] - public fun destroy_for_testing(self: Registry) { - let Registry { - registry, - reverse_registry, - } = self; +#[test_only] +public fun destroy_for_testing(self: Registry) { + let Registry { + registry, + reverse_registry, + } = self; - registry.drop(); - reverse_registry.drop(); - } + registry.drop(); + reverse_registry.drop(); } diff --git a/packages/suins/sources/subdomain_registration.move b/packages/suins/sources/subdomain_registration.move index 724e928f..e9f5336e 100644 --- a/packages/suins/sources/subdomain_registration.move +++ b/packages/suins/sources/subdomain_registration.move @@ -2,67 +2,75 @@ // SPDX-License-Identifier: Apache-2.0 /// A wrapper for `SuinsRegistration` subdomain objects. -/// -/// With the wrapper, we are allowing easier distinction between second +/// +/// With the wrapper, we are allowing easier distinction between second /// level names & subdomains in RPC Querying | filtering. -/// +/// /// We maintain all core functionality unchanged for registry, expiration etc. -module suins::subdomain_registration { - use sui::clock::Clock; +module suins::subdomain_registration; - use suins::suins_registration::SuinsRegistration; +use sui::clock::Clock; +use suins::suins_registration::SuinsRegistration; - /* friend suins::registry; */ - /* #[test_only] */ /* friend suins::sub_name_tests; */ +/* friend suins::registry; */ +/* #[test_only] */ +/* friend suins::sub_name_tests; */ - /// === Error codes === - /// - /// NFT is expired. - const EExpired: u64 = 1; - /// NFT is not a subdomain. - const ENotSubdomain: u64 = 2; - /// Tries to destroy a subdomain that has not expired. - const ENameNotExpired: u64 = 3; +/// === Error codes === +/// +/// NFT is expired. +const EExpired: u64 = 1; +/// NFT is not a subdomain. +const ENotSubdomain: u64 = 2; +/// Tries to destroy a subdomain that has not expired. +const ENameNotExpired: u64 = 3; - /// A wrapper for SuinsRegistration object specifically for SubNames. - public struct SubDomainRegistration has key, store { - id: UID, - nft: SuinsRegistration - } +/// A wrapper for SuinsRegistration object specifically for SubNames. +public struct SubDomainRegistration has key, store { + id: UID, + nft: SuinsRegistration, +} - /// Creates a `SubName` wrapper for SuinsRegistration object - /// (as long as it's used for a subdomain). - public(package) fun new(nft: SuinsRegistration, clock: &Clock, ctx: &mut TxContext): SubDomainRegistration { - // Can't wrap a non-subdomain NFT. - assert!(nft.domain().is_subdomain(), ENotSubdomain); - // Can't wrap an expired NFT. - assert!(!nft.has_expired(clock), EExpired); +/// Creates a `SubName` wrapper for SuinsRegistration object +/// (as long as it's used for a subdomain). +public(package) fun new( + nft: SuinsRegistration, + clock: &Clock, + ctx: &mut TxContext, +): SubDomainRegistration { + // Can't wrap a non-subdomain NFT. + assert!(nft.domain().is_subdomain(), ENotSubdomain); + // Can't wrap an expired NFT. + assert!(!nft.has_expired(clock), EExpired); - SubDomainRegistration { - id: object::new(ctx), - nft: nft - } + SubDomainRegistration { + id: object::new(ctx), + nft: nft, } +} - /// Destroys the wrapper and returns the SuinsRegistration object. - /// Fails if the subname is not expired. - public(package) fun burn(name: SubDomainRegistration, clock: &Clock): SuinsRegistration { - // tries to unwrap a non-expired subname. - assert!(name.nft.has_expired(clock), ENameNotExpired); - - let SubDomainRegistration { - id, nft - } = name; +/// Destroys the wrapper and returns the SuinsRegistration object. +/// Fails if the subname is not expired. +public(package) fun burn( + name: SubDomainRegistration, + clock: &Clock, +): SuinsRegistration { + // tries to unwrap a non-expired subname. + assert!(name.nft.has_expired(clock), ENameNotExpired); - id.delete(); - nft - } + let SubDomainRegistration { + id, + nft, + } = name; - public fun nft(name: &SubDomainRegistration): &SuinsRegistration { - &name.nft - } + id.delete(); + nft +} - public fun nft_mut(name: &mut SubDomainRegistration): &mut SuinsRegistration { - &mut name.nft - } +public fun nft(name: &SubDomainRegistration): &SuinsRegistration { + &name.nft +} + +public fun nft_mut(name: &mut SubDomainRegistration): &mut SuinsRegistration { + &mut name.nft } diff --git a/packages/suins/sources/suins.move b/packages/suins/sources/suins.move index 4b2a2a09..cc197ea6 100644 --- a/packages/suins/sources/suins.move +++ b/packages/suins/sources/suins.move @@ -20,216 +20,248 @@ /// the registry without breaking the SuiNS compatibility. /// - Any of the old modules can be deauthorized hence disabling its access to /// the registry and the balance. -module suins::suins { - use sui::{balance::{Self, Balance}, coin::{Self, Coin}, dynamic_field as df, sui::SUI}; - - /// Trying to withdraw from an empty balance. - const ENoProfits: u64 = 0; - /// An application is not authorized to access the feature. - const EAppNotAuthorized: u64 = 1; - - /// An admin capability. The admin has full control over the application. - /// This object must be issued only once during module initialization. - public struct AdminCap has key, store { id: UID } - - /// The main application object. Stores the state of the application, - /// used for adding / removing and reading name records. - /// - /// Dynamic fields: - /// - `registry: RegistryKey -> R` - /// - `config: ConfigKey -> C` - public struct SuiNS has key { - id: UID, - /// The total balance of the SuiNS. Can be added to by authorized apps. - /// Can be withdrawn only by the application Admin. - balance: Balance, - } - - /// The one-time-witness used to claim Publisher object. - public struct SUINS has drop {} - - // === Keys === - - /// Key under which a configuration is stored. It is type dependent, so - /// that different configurations can be stored at the same time. Eg - /// currently we store application `Config` (and `Promotion` configuration). - public struct ConfigKey has copy, store, drop {} - - /// Key under which the Registry object is stored. - /// - /// In the V1, the object stored under this key is `Registry`, however, for - /// future migration purposes (if we ever need to change the Registry), we - /// keep the phantom parameter so two different Registries can co-exist. - public struct RegistryKey has copy, store, drop {} - - /// Module initializer: - /// - create SuiNS object - /// - create admin capability - /// - claim Publisher object (for Display and TransferPolicy) - fun init(otw: SUINS, ctx: &mut TxContext) { - sui::package::claim_and_keep(otw, ctx); - - // Create the admin capability; only performed once. - transfer::transfer(AdminCap { - id: object::new(ctx), - }, tx_context::sender(ctx)); +module suins::suins; - let suins = SuiNS { - id: object::new(ctx), - balance: balance::zero(), - }; - - transfer::share_object(suins); - } - - // === Admin actions === - - /// Withdraw from the SuiNS balance directly and access the Coins within the same - /// transaction. This is useful for the admin to withdraw funds from the SuiNS - /// and then send them somewhere specific or keep at the address. - public fun withdraw(_: &AdminCap, self: &mut SuiNS, ctx: &mut TxContext): Coin { - let amount = self.balance.value(); - assert!(amount > 0, ENoProfits); - coin::take(&mut self.balance, amount, ctx) - } - - // === App Auth === - - /// An authorization Key kept in the SuiNS - allows applications access - /// protected features of the SuiNS (such as app_add_balance, etc.) - /// The `App` type parameter is a witness which should be defined in the - /// original module (Controller, Registry, Registrar - whatever). - public struct AppKey has copy, store, drop {} - - /// Authorize an application to access protected features of the SuiNS. - public fun authorize_app(_: &AdminCap, self: &mut SuiNS) { - df::add(&mut self.id, AppKey{}, true); - } - - /// Deauthorize an application by removing its authorization key. - public fun deauthorize_app(_: &AdminCap, self: &mut SuiNS): bool { - df::remove(&mut self.id, AppKey{}) - } - - /// Check if an application is authorized to access protected features of - /// the SuiNS. - public fun is_app_authorized(self: &SuiNS): bool { - df::exists_(&self.id, AppKey{}) - } - - /// Assert that an application is authorized to access protected features of - /// the SuiNS. Aborts with `EAppNotAuthorized` if not. - public fun assert_app_is_authorized(self: &SuiNS) { - assert!(is_app_authorized(self), EAppNotAuthorized); - } - - // === Protected features === - - /// Adds balance to the SuiNS. - public fun app_add_balance(_: App, self: &mut SuiNS, balance: Balance) { - assert_app_is_authorized(self); - self.balance.join(balance); - } - - /// Get a mutable access to the `Registry` object. Can only be performed by authorized - /// applications. - public fun app_registry_mut(_: App, self: &mut SuiNS): &mut R { - assert_app_is_authorized(self); - df::borrow_mut(&mut self.id, RegistryKey {}) - } - - // === Config management === - - /// Attach dynamic configuration object to the application. - public fun add_config(_: &AdminCap, self: &mut SuiNS, config: Config) { - df::add(&mut self.id, ConfigKey {}, config); - } - - /// Borrow configuration object. Read-only mode for applications. - public fun get_config(self: &SuiNS): &Config { - df::borrow(&self.id, ConfigKey {}) - } - - /// Get the configuration object for editing. The admin should put it back - /// after editing (no extra check performed). Can be used to swap - /// configuration since the `T` has `drop`. Eg nothing is stopping the admin - /// from removing the configuration object and adding a new one. - /// - /// Fully taking the config also allows for edits within a transaction. - public fun remove_config(_: &AdminCap, self: &mut SuiNS): Config { - df::remove(&mut self.id, ConfigKey {}) - } - - // === Registry === - - /// Get a read-only access to the `Registry` object. - public fun registry(self: &SuiNS): &R { - df::borrow(&self.id, RegistryKey {}) - } - - /// Add a registry to the SuiNS. Can only be performed by the admin. - public fun add_registry(_: &AdminCap, self: &mut SuiNS, registry: R) { - df::add(&mut self.id, RegistryKey {}, registry); - } - - // === Testing === - - #[test_only] use suins::config; - #[test_only] public struct Test has drop {} - - #[test_only] - public fun new_for_testing(ctx: &mut TxContext): (SuiNS, AdminCap) { - ( - SuiNS { id: object::new(ctx), balance: balance::zero() }, - AdminCap { id: object::new(ctx) } - ) - } - - #[test_only] - /// Wrapper of module initializer for testing - public fun init_for_testing(ctx: &mut TxContext): SuiNS { - let admin_cap = AdminCap { id: object::new(ctx) }; - let mut suins = SuiNS { +use sui::balance::{Self, Balance}; +use sui::coin::{Self, Coin}; +use sui::dynamic_field as df; +use sui::sui::SUI; + +/// Trying to withdraw from an empty balance. +const ENoProfits: u64 = 0; +/// An application is not authorized to access the feature. +const EAppNotAuthorized: u64 = 1; + +/// An admin capability. The admin has full control over the application. +/// This object must be issued only once during module initialization. +public struct AdminCap has key, store { id: UID } + +/// The main application object. Stores the state of the application, +/// used for adding / removing and reading name records. +/// +/// Dynamic fields: +/// - `registry: RegistryKey -> R` +/// - `config: ConfigKey -> C` +public struct SuiNS has key { + id: UID, + /// The total balance of the SuiNS. Can be added to by authorized apps. + /// Can be withdrawn only by the application Admin. + balance: Balance, +} + +/// The one-time-witness used to claim Publisher object. +public struct SUINS has drop {} + +// === Keys === + +/// Key under which a configuration is stored. It is type dependent, so +/// that different configurations can be stored at the same time. Eg +/// currently we store application `Config` (and `Promotion` configuration). +public struct ConfigKey has copy, store, drop {} + +/// Key under which the Registry object is stored. +/// +/// In the V1, the object stored under this key is `Registry`, however, for +/// future migration purposes (if we ever need to change the Registry), we +/// keep the phantom parameter so two different Registries can co-exist. +public struct RegistryKey has copy, store, drop {} + +/// Module initializer: +/// - create SuiNS object +/// - create admin capability +/// - claim Publisher object (for Display and TransferPolicy) +fun init(otw: SUINS, ctx: &mut TxContext) { + sui::package::claim_and_keep(otw, ctx); + + // Create the admin capability; only performed once. + transfer::transfer( + AdminCap { id: object::new(ctx), - balance: balance::zero(), - }; + }, + tx_context::sender(ctx), + ); + + let suins = SuiNS { + id: object::new(ctx), + balance: balance::zero(), + }; + + transfer::share_object(suins); +} + +// === Admin actions === + +/// Withdraw from the SuiNS balance directly and access the Coins within the +/// same +/// transaction. This is useful for the admin to withdraw funds from the SuiNS +/// and then send them somewhere specific or keep at the address. +public fun withdraw( + _: &AdminCap, + self: &mut SuiNS, + ctx: &mut TxContext, +): Coin { + let amount = self.balance.value(); + assert!(amount > 0, ENoProfits); + coin::take(&mut self.balance, amount, ctx) +} + +// === App Auth === + +/// An authorization Key kept in the SuiNS - allows applications access +/// protected features of the SuiNS (such as app_add_balance, etc.) +/// The `App` type parameter is a witness which should be defined in the +/// original module (Controller, Registry, Registrar - whatever). +public struct AppKey has copy, store, drop {} + +/// Authorize an application to access protected features of the SuiNS. +public fun authorize_app(_: &AdminCap, self: &mut SuiNS) { + df::add(&mut self.id, AppKey {}, true); +} + +/// Deauthorize an application by removing its authorization key. +public fun deauthorize_app(_: &AdminCap, self: &mut SuiNS): bool { + df::remove(&mut self.id, AppKey {}) +} + +/// Check if an application is authorized to access protected features of +/// the SuiNS. +public fun is_app_authorized(self: &SuiNS): bool { + df::exists_(&self.id, AppKey {}) +} + +/// Assert that an application is authorized to access protected features of +/// the SuiNS. Aborts with `EAppNotAuthorized` if not. +public fun assert_app_is_authorized(self: &SuiNS) { + assert!(is_app_authorized(self), EAppNotAuthorized); +} + +// === Protected features === + +/// Adds balance to the SuiNS. +public fun app_add_balance( + _: App, + self: &mut SuiNS, + balance: Balance, +) { + assert_app_is_authorized(self); + self.balance.join(balance); +} - authorize_app(&admin_cap, &mut suins); - add_config(&admin_cap, &mut suins, config::new( +/// Get a mutable access to the `Registry` object. Can only be performed by +/// authorized +/// applications. +public fun app_registry_mut( + _: App, + self: &mut SuiNS, +): &mut R { + assert_app_is_authorized(self); + df::borrow_mut(&mut self.id, RegistryKey {}) +} + +// === Config management === + +/// Attach dynamic configuration object to the application. +public fun add_config( + _: &AdminCap, + self: &mut SuiNS, + config: Config, +) { + df::add(&mut self.id, ConfigKey {}, config); +} + +/// Borrow configuration object. Read-only mode for applications. +public fun get_config(self: &SuiNS): &Config { + df::borrow(&self.id, ConfigKey {}) +} + +/// Get the configuration object for editing. The admin should put it back +/// after editing (no extra check performed). Can be used to swap +/// configuration since the `T` has `drop`. Eg nothing is stopping the admin +/// from removing the configuration object and adding a new one. +/// +/// Fully taking the config also allows for edits within a transaction. +public fun remove_config( + _: &AdminCap, + self: &mut SuiNS, +): Config { + df::remove(&mut self.id, ConfigKey {}) +} + +// === Registry === + +/// Get a read-only access to the `Registry` object. +public fun registry(self: &SuiNS): &R { + df::borrow(&self.id, RegistryKey {}) +} + +/// Add a registry to the SuiNS. Can only be performed by the admin. +public fun add_registry(_: &AdminCap, self: &mut SuiNS, registry: R) { + df::add(&mut self.id, RegistryKey {}, registry); +} + +// === Testing === + +#[test_only] +use suins::config; +#[test_only] +public struct Test has drop {} + +#[test_only] +public fun new_for_testing(ctx: &mut TxContext): (SuiNS, AdminCap) { + ( + SuiNS { id: object::new(ctx), balance: balance::zero() }, + AdminCap { id: object::new(ctx) }, + ) +} + +#[test_only] +/// Wrapper of module initializer for testing +public fun init_for_testing(ctx: &mut TxContext): SuiNS { + let admin_cap = AdminCap { id: object::new(ctx) }; + let mut suins = SuiNS { + id: object::new(ctx), + balance: balance::zero(), + }; + + authorize_app(&admin_cap, &mut suins); + add_config( + &admin_cap, + &mut suins, + config::new( b"000000000000000000000000000000000", 1200 * suins::constants::mist_per_sui(), 200 * suins::constants::mist_per_sui(), 50 * suins::constants::mist_per_sui(), - )); - transfer::transfer(admin_cap, tx_context::sender(ctx)); - suins - } - - #[test_only] - public fun share_for_testing(self: SuiNS) { - transfer::share_object(self) - } - - #[test_only] - /// Create an admin cap - only for testing. - public fun create_admin_cap_for_testing(ctx: &mut TxContext): AdminCap { - AdminCap { id: object::new(ctx) } - } - - #[test_only] - /// Burn the admin cap - only for testing. - public fun burn_admin_cap_for_testing(admin_cap: AdminCap) { - let AdminCap { id } = admin_cap; - id.delete(); - } - - #[test_only] - public fun authorize_app_for_testing(self: &mut SuiNS) { - df::add(&mut self.id, AppKey {}, true) - } - - #[test_only] - public fun total_balance(self: &SuiNS): u64 { - self.balance.value() - } + ), + ); + transfer::transfer(admin_cap, tx_context::sender(ctx)); + suins +} + +#[test_only] +public fun share_for_testing(self: SuiNS) { + transfer::share_object(self) +} + +#[test_only] +/// Create an admin cap - only for testing. +public fun create_admin_cap_for_testing(ctx: &mut TxContext): AdminCap { + AdminCap { id: object::new(ctx) } +} + +#[test_only] +/// Burn the admin cap - only for testing. +public fun burn_admin_cap_for_testing(admin_cap: AdminCap) { + let AdminCap { id } = admin_cap; + id.delete(); +} + +#[test_only] +public fun authorize_app_for_testing(self: &mut SuiNS) { + df::add(&mut self.id, AppKey {}, true) +} + +#[test_only] +public fun total_balance(self: &SuiNS): u64 { + self.balance.value() } diff --git a/packages/suins/sources/suins_registration.move b/packages/suins/sources/suins_registration.move index 9c8a0a7d..f0a5ab54 100644 --- a/packages/suins/sources/suins_registration.move +++ b/packages/suins/sources/suins_registration.move @@ -1,7 +1,8 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -/// Handles creation of the `SuinsRegistration`s. Separates the logic of creating +/// Handles creation of the `SuinsRegistration`s. Separates the logic of +/// creating /// a `SuinsRegistration`. New `SuinsRegistration`s can be created only by the /// `registry` and this module is tightly coupled with it. /// @@ -10,145 +11,157 @@ /// - mutable functions can't be called directly by the owner /// - all getters are public and take an immutable reference /// -module suins::suins_registration { - use std::string::{String}; - use sui::clock::{timestamp_ms, Clock}; - - use suins::{constants, domain::Domain}; - - /* friend suins::registry; */ - /* friend suins::update_image; */ - - /// The main access point for the user. - public struct SuinsRegistration has key, store { - id: UID, - /// The parsed domain. - domain: Domain, - /// The domain name that the NFT is for. - domain_name: String, - /// Timestamp in milliseconds when this NFT expires. - expiration_timestamp_ms: u64, - /// Short IPFS hash of the image to be displayed for the NFT. - image_url: String, - } +module suins::suins_registration; + +use std::string::String; +use sui::clock::{timestamp_ms, Clock}; +use suins::constants; +use suins::domain::Domain; + +/* friend suins::registry; */ +/* friend suins::update_image; */ + +/// The main access point for the user. +public struct SuinsRegistration has key, store { + id: UID, + /// The parsed domain. + domain: Domain, + /// The domain name that the NFT is for. + domain_name: String, + /// Timestamp in milliseconds when this NFT expires. + expiration_timestamp_ms: u64, + /// Short IPFS hash of the image to be displayed for the NFT. + image_url: String, +} - // === Protected methods === - - /// Creates a new `SuinsRegistration`. - /// Can only be called by the `registry` module. - public(package) fun new( - domain: Domain, - no_years: u8, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - SuinsRegistration { - id: object::new(ctx), - domain_name: domain.to_string(), - domain, - expiration_timestamp_ms: timestamp_ms(clock) + ((no_years as u64) * constants::year_ms()), - image_url: constants::default_image(), - } +// === Protected methods === + +/// Creates a new `SuinsRegistration`. +/// Can only be called by the `registry` module. +public(package) fun new( + domain: Domain, + no_years: u8, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + SuinsRegistration { + id: object::new(ctx), + domain_name: domain.to_string(), + domain, + expiration_timestamp_ms: timestamp_ms(clock) + ((no_years as u64) * constants::year_ms()), + image_url: constants::default_image(), } +} - /// Sets the `expiration_timestamp_ms` for this NFT. - public(package) fun set_expiration_timestamp_ms(self: &mut SuinsRegistration, expiration_timestamp_ms: u64) { - self.expiration_timestamp_ms = expiration_timestamp_ms; - } +/// Sets the `expiration_timestamp_ms` for this NFT. +public(package) fun set_expiration_timestamp_ms( + self: &mut SuinsRegistration, + expiration_timestamp_ms: u64, +) { + self.expiration_timestamp_ms = expiration_timestamp_ms; +} - /// Updates the `image_url` field for this NFT. Is only called in the `update_image` for now. - public(package) fun update_image_url(self: &mut SuinsRegistration, image_url: String) { - self.image_url = image_url; - } +/// Updates the `image_url` field for this NFT. Is only called in the +/// `update_image` for now. +public(package) fun update_image_url( + self: &mut SuinsRegistration, + image_url: String, +) { + self.image_url = image_url; +} - /// Destroys the `SuinsRegistration` by deleting it from the store, returning - /// storage rebates to the caller. - /// Can only be called by the `registry` module. - public(package) fun burn(self: SuinsRegistration) { - let SuinsRegistration { - id, - image_url: _, - domain: _, - domain_name: _, - expiration_timestamp_ms: _ - } = self; - - id.delete(); - } +/// Destroys the `SuinsRegistration` by deleting it from the store, returning +/// storage rebates to the caller. +/// Can only be called by the `registry` module. +public(package) fun burn(self: SuinsRegistration) { + let SuinsRegistration { + id, + image_url: _, + domain: _, + domain_name: _, + expiration_timestamp_ms: _, + } = self; + + id.delete(); +} - // === Public methods === +// === Public methods === - /// Check whether the `SuinsRegistration` has expired by comparing the - /// expiration timeout with the current time. - public fun has_expired(self: &SuinsRegistration, clock: &Clock): bool { - self.expiration_timestamp_ms < timestamp_ms(clock) - } +/// Check whether the `SuinsRegistration` has expired by comparing the +/// expiration timeout with the current time. +public fun has_expired(self: &SuinsRegistration, clock: &Clock): bool { + self.expiration_timestamp_ms < timestamp_ms(clock) +} - /// Check whether the `SuinsRegistration` has expired by comparing the - /// expiration timeout with the current time. This function also takes into - /// account the grace period. - public fun has_expired_past_grace_period(self: &SuinsRegistration, clock: &Clock): bool { - (self.expiration_timestamp_ms + constants::grace_period_ms()) < timestamp_ms(clock) - } +/// Check whether the `SuinsRegistration` has expired by comparing the +/// expiration timeout with the current time. This function also takes into +/// account the grace period. +public fun has_expired_past_grace_period( + self: &SuinsRegistration, + clock: &Clock, +): bool { + (self.expiration_timestamp_ms + constants::grace_period_ms()) < timestamp_ms(clock) +} - // === Getters === +// === Getters === - /// Get the `domain` field of the `SuinsRegistration`. - public fun domain(self: &SuinsRegistration): Domain { self.domain } +/// Get the `domain` field of the `SuinsRegistration`. +public fun domain(self: &SuinsRegistration): Domain { self.domain } - /// Get the `domain_name` field of the `SuinsRegistration`. - public fun domain_name(self: &SuinsRegistration): String { self.domain_name } +/// Get the `domain_name` field of the `SuinsRegistration`. +public fun domain_name(self: &SuinsRegistration): String { self.domain_name } - /// Get the `expiration_timestamp_ms` field of the `SuinsRegistration`. - public fun expiration_timestamp_ms(self: &SuinsRegistration): u64 { self.expiration_timestamp_ms } +/// Get the `expiration_timestamp_ms` field of the `SuinsRegistration`. +public fun expiration_timestamp_ms(self: &SuinsRegistration): u64 { + self.expiration_timestamp_ms +} - /// Get the `image_url` field of the `SuinsRegistration`. - public fun image_url(self: &SuinsRegistration): String { self.image_url } +/// Get the `image_url` field of the `SuinsRegistration`. +public fun image_url(self: &SuinsRegistration): String { self.image_url } - // get a read-only `uid` field of `SuinsRegistration`. - public fun uid(self: &SuinsRegistration): &UID { &self.id } +// get a read-only `uid` field of `SuinsRegistration`. +public fun uid(self: &SuinsRegistration): &UID { &self.id } - /// Get the mutable `id` field of the `SuinsRegistration`. - public fun uid_mut(self: &mut SuinsRegistration): &mut UID { &mut self.id } +/// Get the mutable `id` field of the `SuinsRegistration`. +public fun uid_mut(self: &mut SuinsRegistration): &mut UID { &mut self.id } - // === Testing === +// === Testing === - #[test_only] - public fun new_for_testing( - domain: Domain, - no_years: u8, - clock: &Clock, - ctx: &mut TxContext - ): SuinsRegistration { - new(domain, no_years, clock, ctx) - } +#[test_only] +public fun new_for_testing( + domain: Domain, + no_years: u8, + clock: &Clock, + ctx: &mut TxContext, +): SuinsRegistration { + new(domain, no_years, clock, ctx) +} - #[test_only] - public fun set_expiration_timestamp_ms_for_testing( - self: &mut SuinsRegistration, - expiration_timestamp_ms: u64 - ) { - set_expiration_timestamp_ms(self, expiration_timestamp_ms); - } +#[test_only] +public fun set_expiration_timestamp_ms_for_testing( + self: &mut SuinsRegistration, + expiration_timestamp_ms: u64, +) { + set_expiration_timestamp_ms(self, expiration_timestamp_ms); +} - #[test_only] - public fun update_image_url_for_testing( - self: &mut SuinsRegistration, - image_url: String - ) { - update_image_url(self, image_url); - } +#[test_only] +public fun update_image_url_for_testing( + self: &mut SuinsRegistration, + image_url: String, +) { + update_image_url(self, image_url); +} - #[test_only] - public fun burn_for_testing(nft: SuinsRegistration) { - let SuinsRegistration { - id, - image_url: _, - domain: _, - domain_name: _, - expiration_timestamp_ms: _ - } = nft; - - id.delete(); - } +#[test_only] +public fun burn_for_testing(nft: SuinsRegistration) { + let SuinsRegistration { + id, + image_url: _, + domain: _, + domain_name: _, + expiration_timestamp_ms: _, + } = nft; + + id.delete(); }