diff --git a/core/Move.toml b/core/Move.toml index 6c2a4eae..f45778c2 100644 --- a/core/Move.toml +++ b/core/Move.toml @@ -9,10 +9,10 @@ aptos_names_funds = "0x78ee3915e67ef5d19fa91d1e05e60ae08751efd12ce58e23fc1109de8 [dependencies.AptosFramework] git = 'https://github.com/aptos-labs/aptos-core.git' -rev = 'main' +rev = 'framework-mainnet' subdir = 'aptos-move/framework/aptos-framework' [dependencies.AptosToken] git = 'https://github.com/aptos-labs/aptos-core.git' -rev = 'main' +rev = 'framework-mainnet' subdir = 'aptos-move/framework/aptos-token' diff --git a/core/sources/domain_e2e_tests.move b/core/sources/domain_e2e_tests.move index 6db3bc5b..66498a5e 100644 --- a/core/sources/domain_e2e_tests.move +++ b/core/sources/domain_e2e_tests.move @@ -7,8 +7,10 @@ module aptos_names::domain_e2e_tests { use aptos_names::time_helper; use aptos_names::test_helper; use aptos_names::test_utils; + use aptos_token::token; use std::option; use std::signer; + use std::string; use std::vector; #[test(myself = @aptos_names, user = @0x077, aptos = @0x1, rando = @0x266f, foundation = @0xf01d)] @@ -125,6 +127,9 @@ module aptos_names::domain_e2e_tests { // Lets try to register it again, now that it is expired test_helper::register_name(rando, option::none(), test_helper::domain_name(), test_helper::one_year_secs(), test_helper::fq_domain_name(), 2, vector::empty()); + // Reverse lookup for |user| should be none. + assert!(option::is_none(&domains::get_reverse_lookup(signer::address_of(user))), 85); + // And again! let (_, expiration_time_sec, _) = domains::get_name_record_v1_props_for_name(option::none(), test_helper::domain_name()); timestamp::update_global_time_for_test_secs(expiration_time_sec + 5); @@ -230,6 +235,7 @@ module aptos_names::domain_e2e_tests { fun admin_can_force_seize_domain_name_e2e_test(myself: &signer, user: signer, aptos: signer, rando: signer, foundation: signer) { let users = test_helper::e2e_test_setup(myself, user, &aptos, rando, &foundation); let user = vector::borrow(&users, 0); + let user_addr = signer::address_of(user); // Register the domain test_helper::register_name(user, option::none(), test_helper::domain_name(), test_helper::one_year_secs(), test_helper::fq_domain_name(), 1, vector::empty()); @@ -244,6 +250,42 @@ module aptos_names::domain_e2e_tests { // Ensure the expiration_time_sec is set to the new far future value let (_, expiration_time_sec, _) = domains::get_name_record_v1_props_for_name(option::none(), test_helper::domain_name()); assert!(time_helper::seconds_to_years(expiration_time_sec) == 200, time_helper::seconds_to_years(expiration_time_sec)); + + // Ensure that the user's primary name is no longer set. + assert!(option::is_none(&domains::get_reverse_lookup(user_addr)), 1); + } + + #[test(myself = @aptos_names, user = @0x077, aptos = @0x1, rando = @0x266f, foundation = @0xf01d)] + fun admin_force_seize_domain_name_doesnt_clear_unrelated_primary_name_e2e_test(myself: &signer, user: signer, aptos: signer, rando: signer, foundation: signer) { + let users = test_helper::e2e_test_setup(myself, user, &aptos, rando, &foundation); + let user = vector::borrow(&users, 0); + let user_addr = signer::address_of(user); + + // Register the domain. This will be the user's reverse lookup + { + test_helper::register_name(user, option::none(), test_helper::domain_name(), test_helper::one_year_secs(), test_helper::fq_domain_name(), 1, vector::empty()); + let (is_owner, _token_id) = domains::is_owner_of_name(signer::address_of(user), option::none(), test_helper::domain_name()); + assert!(is_owner, 1); + }; + + // Register another domain. This will **not** be the user's reverse lookup + let domain_name = string::utf8(b"sets"); + let fq_domain_name = string::utf8(b"sets.apt"); + test_helper::register_name(user, option::none(), domain_name, test_helper::one_year_secs(), fq_domain_name, 1, vector::empty()); + let (is_owner, _token_id) = domains::is_owner_of_name(signer::address_of(user), option::none(), domain_name); + assert!(is_owner, 1); + + // Take the domain name for much longer than users are allowed to register it for + domains::force_create_or_seize_name(myself, option::none(), domain_name, test_helper::two_hundred_year_secs()); + let (is_owner, _token_id) = domains::is_owner_of_name(signer::address_of(myself), option::none(), domain_name); + assert!(is_owner, 2); + + // Ensure the expiration_time_sec is set to the new far future value + let (_, expiration_time_sec, _) = domains::get_name_record_v1_props_for_name(option::none(), domain_name); + assert!(time_helper::seconds_to_years(expiration_time_sec) == 200, time_helper::seconds_to_years(expiration_time_sec)); + + // Ensure that the user's primary name is still set. + assert!(option::is_some(&domains::get_reverse_lookup(user_addr)), 1); } #[test(myself = @aptos_names, user = @0x077, aptos = @0x1, rando = @0x266f, foundation = @0xf01d)] @@ -296,4 +338,78 @@ module aptos_names::domain_e2e_tests { // Take the domain name for much longer than users are allowed to register it for domains::force_create_or_seize_name(rando, option::none(), test_helper::domain_name(), test_helper::two_hundred_year_secs()); } + + #[test(myself = @aptos_names, user = @0x077, aptos = @0x1, rando = @0x266f, foundation = @0xf01d)] + fun clear_name_happy_path_e2e_test(myself: &signer, user: signer, aptos: signer, rando: signer, foundation: signer) { + let users = test_helper::e2e_test_setup(myself, user, &aptos, rando, &foundation); + let user = vector::borrow(&users, 0); + let user_addr = signer::address_of(user); + + // Register the domain + test_helper::register_name(user, option::none(), test_helper::domain_name(), test_helper::one_year_secs(), test_helper::fq_domain_name(), 1, vector::empty()); + + // Clear my reverse lookup. + domains::clear_reverse_lookup(user); + + assert!(option::is_none(&domains::get_reverse_lookup(user_addr)), 1); + } + + #[test(myself = @aptos_names, user = @0x077, aptos = @0x1, rando = @0x266f, foundation = @0xf01d)] + fun set_primary_name_after_transfer_clears_old_primary_name_e2e_test(myself: &signer, user: signer, aptos: signer, rando: signer, foundation: signer) { + let users = test_helper::e2e_test_setup(myself, user, &aptos, rando, &foundation); + let user = vector::borrow(&users, 0); + let user_addr = signer::address_of(user); + let rando = vector::borrow(&users, 1); + let rando_addr = signer::address_of(rando); + + // Register the domain + test_helper::register_name(user, option::none(), test_helper::domain_name(), test_helper::one_year_secs(), test_helper::fq_domain_name(), 1, vector::empty()); + let (is_owner, token_id) = domains::is_owner_of_name(user_addr, option::none(), test_helper::domain_name()); + assert!(is_owner, 1); + + // Transfer the domain to rando + token::direct_transfer(user, rando, token_id, 1); + + // Verify primary name for |user| hasn't changed + assert!(option::is_some(&domains::get_reverse_lookup(user_addr)), 1); + assert!(*option::borrow(&domains::name_resolved_address(option::none(), test_helper::domain_name())) == user_addr, 1); + + // |rando| sets his primary name + let subdomain_name_str = string::utf8(b""); + let domain_name_str = string::utf8(b"test"); + domains::set_reverse_lookup_entry(rando, subdomain_name_str, domain_name_str); + + // |user|'s primary name should be none. + assert!(option::is_none(&domains::get_reverse_lookup(user_addr)), 1); + assert!(*option::borrow(&domains::name_resolved_address(option::none(), test_helper::domain_name())) == rando_addr, 1); + } + + #[test(myself = @aptos_names, user = @0x077, aptos = @0x1, rando = @0x266f, foundation = @0xf01d)] + fun set_target_address_after_transfer_clears_old_primary_name_e2e_test(myself: &signer, user: signer, aptos: signer, rando: signer, foundation: signer) { + let users = test_helper::e2e_test_setup(myself, user, &aptos, rando, &foundation); + let user = vector::borrow(&users, 0); + let user_addr = signer::address_of(user); + let rando = vector::borrow(&users, 1); + let rando_addr = signer::address_of(rando); + + // Register the domain + test_helper::register_name(user, option::none(), test_helper::domain_name(), test_helper::one_year_secs(), test_helper::fq_domain_name(), 1, vector::empty()); + let (is_owner, token_id) = domains::is_owner_of_name(user_addr, option::none(), test_helper::domain_name()); + assert!(is_owner, 1); + + // Transfer the domain to rando + token::direct_transfer(user, rando, token_id, 1); + + // Verify primary name for |user| hasn't changed + assert!(option::is_some(&domains::get_reverse_lookup(user_addr)), 1); + assert!(*option::borrow(&domains::name_resolved_address(option::none(), test_helper::domain_name())) == user_addr, 1); + + // |rando| sets target address + let domain_name_str = string::utf8(b"test"); + domains::set_domain_address(rando, domain_name_str, rando_addr); + + // |user|'s primary name should be none. + assert!(option::is_none(&domains::get_reverse_lookup(user_addr)), 1); + assert!(*option::borrow(&domains::name_resolved_address(option::none(), test_helper::domain_name())) == rando_addr, 1); + } } diff --git a/core/sources/domains.move b/core/sources/domains.move index 95f13c98..fc91729a 100644 --- a/core/sources/domains.move +++ b/core/sources/domains.move @@ -17,7 +17,7 @@ module aptos_names::domains { use std::error; use std::option::{Self, Option}; use std::signer; - use std::string::String; + use std::string::{Self, String}; /// The Naming Service contract is not enabled const ENOT_ENABLED: u64 = 1; @@ -71,6 +71,19 @@ module aptos_names::domains { registry: Table, } + /// The registry for reverse lookups (aka primary names), which maps addresses to their primary name + /// - Users can set their primary name to a name that they own. This also forces the name to target their own address too. + /// - If you point your primary name to an address that isn't your own, it is no longer your primary name. + /// - If you mint a name while you don't have a primary name, your minted name is auto-set to be your primary name. (including minting subdomains) + struct ReverseLookupRegistryV1 has key, store { + registry: Table + } + + /// Holder for `SetReverseLookupEventV1` events + struct SetReverseLookupEventsV1 has key, store { + set_reverse_lookup_events: event::EventHandle, + } + /// Holder for `SetNameAddressEventV1` events struct SetNameAddressEventsV1 has key, store { set_name_events: event::EventHandle, @@ -81,6 +94,15 @@ module aptos_names::domains { register_name_events: event::EventHandle, } + /// A name has been set as the reverse lookup for an address, or + /// the reverse lookup has been cleared (in which case |target_address| + /// will be none) + struct SetReverseLookupEventV1 has drop, store { + subdomain_name: Option, + domain_name: String, + target_address: Option
, + } + /// A name (potentially subdomain) has had it's address changed /// This could be to a new address, or it could have been cleared struct SetNameAddressEventV1 has drop, store { @@ -135,7 +157,19 @@ module aptos_names::domains { token_helper::initialize(account); } - fun register_domain_generic(sign: &signer, domain_name: String, num_years: u8) acquires NameRegistryV1, RegisterNameEventsV1, SetNameAddressEventsV1 { + public fun init_reverse_lookup_registry_v1(account: &signer) { + assert!(signer::address_of(account) == @aptos_names, error::permission_denied(ENOT_AUTHORIZED)); + + move_to(account, ReverseLookupRegistryV1 { + registry: table::new() + }); + + move_to(account, SetReverseLookupEventsV1 { + set_reverse_lookup_events: account::new_event_handle(account), + }); + } + + fun register_domain_generic(sign: &signer, domain_name: String, num_years: u8) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { assert!(config::is_enabled(), error::unavailable(ENOT_ENABLED)); assert!(num_years > 0 && num_years <= config::max_number_of_years_registered(), error::out_of_range(EINVALID_NUMBER_YEARS)); @@ -155,18 +189,16 @@ module aptos_names::domains { coin::transfer(sign, config::fund_destination_address(), price); register_name_internal(sign, subdomain_name, domain_name, registration_duration_secs, price); - // Automatically set the name to point to the sender's address - set_name_address_internal(subdomain_name, domain_name, signer::address_of(sign)); } /// A wrapper around `register_name` as an entry function. /// Option is not currently serializable, so we have these convenience methods - public entry fun register_domain(sign: &signer, domain_name: String, num_years: u8) acquires NameRegistryV1, RegisterNameEventsV1, SetNameAddressEventsV1 { + public entry fun register_domain(sign: &signer, domain_name: String, num_years: u8) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { assert!(config::unrestricted_mint_enabled(), error::permission_denied(EVALID_SIGNATURE_REQUIRED)); register_domain_generic(sign, domain_name, num_years); } - public entry fun register_domain_with_signature(sign: &signer, domain_name: String, num_years: u8, signature: vector) acquires NameRegistryV1, RegisterNameEventsV1, SetNameAddressEventsV1 { + public entry fun register_domain_with_signature(sign: &signer, domain_name: String, num_years: u8, signature: vector) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { let account_address = signer::address_of(sign); verify::assert_register_domain_signature_verifies(signature, account_address, domain_name); register_domain_generic(sign, domain_name, num_years); @@ -175,7 +207,7 @@ module aptos_names::domains { /// A wrapper around `register_name` as an entry function. /// Option is not currently serializable, so we have these convenience method /// `expiration_time_sec` is the timestamp, in seconds, when the name expires - public entry fun register_subdomain(sign: &signer, subdomain_name: String, domain_name: String, expiration_time_sec: u64) acquires NameRegistryV1, RegisterNameEventsV1 { + public entry fun register_subdomain(sign: &signer, subdomain_name: String, domain_name: String, expiration_time_sec: u64) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { assert!(config::is_enabled(), error::unavailable(ENOT_ENABLED)); assert!(name_is_registerable(option::some(subdomain_name), domain_name), error::invalid_state(ENAME_NOT_AVAILABLE)); @@ -199,11 +231,15 @@ module aptos_names::domains { register_name_internal(sign, option::some(subdomain_name), domain_name, registration_duration_secs, price); } - /// Register a nane. Accepts an optional subdomain name, a required domain name, and a registration duration in seconds. + /// Register a name. Accepts an optional subdomain name, a required domain name, and a registration duration in seconds. /// For domains, the registration duration is only allowed to be in increments of 1 year, for now /// Since the owner of the domain is the only one that can create the subdomain, we allow them to decide how long they want the underlying registration to be /// The maximum subdomain registration duration is limited to the duration of its parent domain registration - fun register_name_internal(sign: &signer, subdomain_name: Option, domain_name: String, registration_duration_secs: u64, price: u64) acquires NameRegistryV1, RegisterNameEventsV1 { + fun register_name_internal(sign: &signer, subdomain_name: Option, domain_name: String, registration_duration_secs: u64, price: u64) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { + // If we're registering a name that exists but is expired, and the expired name is a primary name, + // it should get removed from being a primary name. + clear_reverse_lookup_for_name(subdomain_name, domain_name); + let aptos_names = borrow_global_mut(@aptos_names); let name_expiration_time_secs = timestamp::now_seconds() + registration_duration_secs; @@ -232,6 +268,16 @@ module aptos_names::domains { table::upsert(&mut aptos_names.registry, name_record_key, name_record); + let account_addr = signer::address_of(sign); + let reverse_lookup_result = get_reverse_lookup(account_addr); + if (option::is_none(&reverse_lookup_result)) { + // If the user has no reverse lookup set, set the user's reverse lookup. + set_reverse_lookup(sign, &NameRecordKeyV1 { subdomain_name, domain_name }); + } else if (option::is_none(&subdomain_name)) { + // Automatically set the name to point to the sender's address + set_name_address_internal(subdomain_name, domain_name, signer::address_of(sign)); + }; + event::emit_event( &mut borrow_global_mut(@aptos_names).register_name_events, RegisterNameEventV1 { @@ -247,16 +293,18 @@ module aptos_names::domains { /// Forcefully set the name of a domain. /// This is a privileged operation, used via governance, to forcefully set a domain address /// This can be used, for example, to forcefully set the domain for a system address domain - public entry fun force_set_domain_address(sign: &signer, domain_name: String, new_owner: address) acquires NameRegistryV1, SetNameAddressEventsV1 { + public entry fun force_set_domain_address(sign: &signer, domain_name: String, new_owner: address) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { force_set_name_address(sign, option::none(), domain_name, new_owner); } - public entry fun force_set_subdomain_address(sign: &signer, subdomain_name: String, domain_name: String, new_owner: address) acquires NameRegistryV1, SetNameAddressEventsV1 { + public entry fun force_set_subdomain_address(sign: &signer, subdomain_name: String, domain_name: String, new_owner: address) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { force_set_name_address(sign, option::some(subdomain_name), domain_name, new_owner); } - fun force_set_name_address(sign: &signer, subdomain_name: Option, domain_name: String, new_owner: address) acquires NameRegistryV1, SetNameAddressEventsV1 { + fun force_set_name_address(sign: &signer, subdomain_name: Option, domain_name: String, new_owner: address) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { config::assert_signer_is_admin(sign); + // If the domain name is a primary name, clear it. + clear_reverse_lookup_for_name(subdomain_name, domain_name); set_name_address_internal(subdomain_name, domain_name, new_owner); } @@ -265,16 +313,17 @@ module aptos_names::domains { /// The `registration_duration_secs` parameter is the number of seconds to register the domain for, but is not limited to the maximum set in the config for domains registered normally. /// This allows, for example, to create a domain for the system address for 100 years so we don't need to worry about expiry /// Or for moderation purposes, it allows us to seize a racist/harassing domain for 100 years, and park it somewhere safe - public entry fun force_create_or_seize_domain_name(sign: &signer, domain_name: String, registration_duration_secs: u64) acquires NameRegistryV1, RegisterNameEventsV1 { + public entry fun force_create_or_seize_domain_name(sign: &signer, domain_name: String, registration_duration_secs: u64) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { force_create_or_seize_name(sign, option::none(), domain_name, registration_duration_secs); } - public entry fun force_create_or_seize_subdomain_name(sign: &signer, subdomain_name: String, domain_name: String, registration_duration_secs: u64) acquires NameRegistryV1, RegisterNameEventsV1 { + public entry fun force_create_or_seize_subdomain_name(sign: &signer, subdomain_name: String, domain_name: String, registration_duration_secs: u64) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { force_create_or_seize_name(sign, option::some(subdomain_name), domain_name, registration_duration_secs); } - public fun force_create_or_seize_name(sign: &signer, subdomain_name: Option, domain_name: String, registration_duration_secs: u64) acquires NameRegistryV1, RegisterNameEventsV1 { + public fun force_create_or_seize_name(sign: &signer, subdomain_name: Option, domain_name: String, registration_duration_secs: u64) acquires NameRegistryV1, RegisterNameEventsV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { config::assert_signer_is_admin(sign); + // Register the name register_name_internal(sign, subdomain_name, domain_name, registration_duration_secs, 0); } @@ -361,16 +410,18 @@ module aptos_names::domains { } } - public entry fun set_domain_address(sign: &signer, domain_name: String, new_address: address) acquires NameRegistryV1, SetNameAddressEventsV1 { + public entry fun set_domain_address(sign: &signer, domain_name: String, new_address: address) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { set_name_address(sign, option::none(), domain_name, new_address); } - - public entry fun set_subdomain_address(sign: &signer, subdomain_name: String, domain_name: String, new_address: address) acquires NameRegistryV1, SetNameAddressEventsV1 { + public entry fun set_subdomain_address(sign: &signer, subdomain_name: String, domain_name: String, new_address: address) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { set_name_address(sign, option::some(subdomain_name), domain_name, new_address); } - public fun set_name_address(sign: &signer, subdomain_name: Option, domain_name: String, new_address: address) acquires NameRegistryV1, SetNameAddressEventsV1 { + public fun set_name_address(sign: &signer, subdomain_name: Option, domain_name: String, new_address: address) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { + // If the domain name is a primary name, clear it. + clear_reverse_lookup_for_name(subdomain_name, domain_name); + let signer_addr = signer::address_of(sign); let (is_owner, token_id) = is_owner_of_name(signer_addr, subdomain_name, domain_name); assert!(is_owner, error::permission_denied(ENOT_OWNER_OF_NAME)); @@ -379,6 +430,21 @@ module aptos_names::domains { let (_property_version, expiration_time_sec, _target_address) = get_name_record_v1_props(&name_record); let (property_keys, property_values, property_types) = get_name_property_map(subdomain_name, expiration_time_sec); token_helper::set_token_props(signer_addr, property_keys, property_values, property_types, token_id); + + // If the signer's reverse lookup is the domain, and the new address is not the signer, clear the signer's reverse lookup. + // Example: + // The current state is bob.apt points to @a and the reverse lookup of @a points to bob.apt. + // The owner wants to set bob.apt to point to @b. + // The new state should be bob.apt points to @b, and the reverse lookup of @a should be none. + let maybe_reverse_lookup = get_reverse_lookup(signer_addr); + if (option::is_none(&maybe_reverse_lookup)) { + return + }; + let current_reverse_lookup = option::borrow(&maybe_reverse_lookup); + let key = NameRecordKeyV1 { subdomain_name, domain_name }; + if (*current_reverse_lookup == key && signer_addr != new_address) { + clear_reverse_lookup(sign); + }; } fun set_name_address_internal(subdomain_name: Option, domain_name: String, new_address: address): NameRecordV1 acquires NameRegistryV1, SetNameAddressEventsV1 { @@ -398,20 +464,30 @@ module aptos_names::domains { *name_record } - public entry fun clear_domain_address(sign: &signer, domain_name: String) acquires NameRegistryV1, SetNameAddressEventsV1 { + public entry fun clear_domain_address(sign: &signer, domain_name: String) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { clear_name_address(sign, option::none(), domain_name); } - public entry fun clear_subdomain_address(sign: &signer, subdomain_name: String, domain_name: String) acquires NameRegistryV1, SetNameAddressEventsV1 { + public entry fun clear_subdomain_address(sign: &signer, subdomain_name: String, domain_name: String) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { clear_name_address(sign, option::some(subdomain_name), domain_name); } /// This is a shared entry point for clearing the address of a domain or subdomain /// It enforces owner permissions - fun clear_name_address(sign: &signer, subdomain_name: Option, domain_name: String) acquires NameRegistryV1, SetNameAddressEventsV1 { + fun clear_name_address(sign: &signer, subdomain_name: Option, domain_name: String) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { assert!(name_is_registered(subdomain_name, domain_name), error::not_found(ENAME_NOT_EXIST)); let signer_addr = signer::address_of(sign); + + // Clear the reverse lookup if this name is the signer's reverse lookup + let maybe_reverse_lookup = get_reverse_lookup(signer_addr); + if (option::is_some(&maybe_reverse_lookup)) { + let reverse_lookup = option::borrow(&maybe_reverse_lookup); + if (NameRecordKeyV1 { subdomain_name, domain_name } == *reverse_lookup) { + clear_reverse_lookup_internal(signer_addr); + }; + }; + // Only the owner or the registered address can clear the address let (is_owner, token_id) = is_owner_of_name(signer_addr, subdomain_name, domain_name); let is_name_resolved_address = name_resolved_address(subdomain_name, domain_name) == option::some
(signer_addr); @@ -438,6 +514,87 @@ module aptos_names::domains { }; } + /// Entry function for |set_reverse_lookup|. + public entry fun set_reverse_lookup_entry(account: &signer, subdomain_name: String, domain_name: String) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { + let key = NameRecordKeyV1 { + subdomain_name: if (string::length(&subdomain_name) > 0) { + option::some(subdomain_name) + } else { + option::none() + }, + domain_name + }; + set_reverse_lookup(account, &key); + } + + /// Sets the |account|'s reverse lookup, aka "primary name". This allows a user to specify which of their Aptos Names + /// is their "primary", so that dapps can display the user's primary name rather than their address. + public fun set_reverse_lookup(account: &signer, key: &NameRecordKeyV1) acquires NameRegistryV1, ReverseLookupRegistryV1, SetNameAddressEventsV1, SetReverseLookupEventsV1 { + let account_addr = signer::address_of(account); + let (maybe_subdomain_name, domain_name) = get_name_record_key_v1_props(key); + set_name_address(account, maybe_subdomain_name, domain_name, account_addr); + set_reverse_lookup_internal(account, key); + } + + /// Clears the user's reverse lookup. + public fun clear_reverse_lookup(account: &signer) acquires ReverseLookupRegistryV1, SetReverseLookupEventsV1 { + let account_addr = signer::address_of(account); + clear_reverse_lookup_internal(account_addr); + } + + /// Returns the reverse lookup for an address if any. + public fun get_reverse_lookup(account_addr: address): Option acquires ReverseLookupRegistryV1 { + let registry = &borrow_global_mut(@aptos_names).registry; + if (table::contains(registry, account_addr)) { + option::some(*table::borrow(registry, account_addr)) + } else { + option::none() + } + } + + fun set_reverse_lookup_internal(account: &signer, key: &NameRecordKeyV1) acquires ReverseLookupRegistryV1, SetReverseLookupEventsV1 { + let account_addr = signer::address_of(account); + let (maybe_subdomain_name, domain_name) = get_name_record_key_v1_props(key); + let (is_owner, _) = is_owner_of_name(account_addr, maybe_subdomain_name, domain_name); + assert!(is_owner, error::permission_denied(ENOT_AUTHORIZED)); + + let registry = &mut borrow_global_mut(@aptos_names).registry; + table::upsert(registry, account_addr, *key); + + emit_set_reverse_lookup_event_v1( + maybe_subdomain_name, + domain_name, + option::some(account_addr) + ); + } + + fun clear_reverse_lookup_internal(account_addr: address) acquires ReverseLookupRegistryV1, SetReverseLookupEventsV1 { + let maybe_reverse_lookup = get_reverse_lookup(account_addr); + if (option::is_none(&maybe_reverse_lookup)) { + return + }; + let NameRecordKeyV1 { subdomain_name, domain_name } = option::borrow(&maybe_reverse_lookup); + let registry = &mut borrow_global_mut(@aptos_names).registry; + table::remove(registry, account_addr); + emit_set_reverse_lookup_event_v1( + *subdomain_name, + *domain_name, + option::none() + ); + } + + fun clear_reverse_lookup_for_name(subdomain_name: Option, domain_name: String) acquires NameRegistryV1, ReverseLookupRegistryV1, SetReverseLookupEventsV1 { + // If the name is a primary name, clear it + let maybe_target_address = name_resolved_address(subdomain_name, domain_name); + if (option::is_some(&maybe_target_address)) { + let target_address = option::borrow(&maybe_target_address); + let maybe_reverse_lookup = get_reverse_lookup(*target_address); + if (option::is_some(&maybe_reverse_lookup) && NameRecordKeyV1 { subdomain_name, domain_name } == *option::borrow(&maybe_reverse_lookup)) { + clear_reverse_lookup_internal(*target_address); + }; + }; + } + fun emit_set_name_address_event_v1(subdomain_name: Option, domain_name: String, property_version: u64, expiration_time_secs: u64, new_address: Option
) acquires SetNameAddressEventsV1 { let event = SetNameAddressEventV1 { subdomain_name, @@ -453,6 +610,19 @@ module aptos_names::domains { ); } + fun emit_set_reverse_lookup_event_v1(subdomain_name: Option, domain_name: String, target_address: Option
) acquires SetReverseLookupEventsV1 { + let event = SetReverseLookupEventV1 { + subdomain_name, + domain_name, + target_address, + }; + + event::emit_event( + &mut borrow_global_mut(@aptos_names).set_reverse_lookup_events, + event, + ); + } + public fun get_name_property_map(subdomain_name: Option, expiration_time_sec: u64): (vector, vector>, vector) { let type; if (option::is_some(&subdomain_name)) { @@ -499,6 +669,7 @@ module aptos_names::domains { #[test_only] public fun init_module_for_test(account: &signer) { init_module(account); + init_reverse_lookup_registry_v1(account); } #[test_only] @@ -511,6 +682,11 @@ module aptos_names::domains { event::counter(&borrow_global(@aptos_names).register_name_events) } + #[test_only] + public fun get_set_reverse_lookup_event_v1_count(): u64 acquires SetReverseLookupEventsV1 { + event::counter(&borrow_global(@aptos_names).set_reverse_lookup_events) + } + #[test(aptos = @0x1)] fun test_time_is_expired(aptos: &signer) { timestamp::set_time_has_started_for_testing(aptos); diff --git a/core/sources/test_helper.move b/core/sources/test_helper.move index c76a75a7..f0d6d328 100644 --- a/core/sources/test_helper.move +++ b/core/sources/test_helper.move @@ -64,8 +64,17 @@ module aptos_names::test_helper { let is_subdomain = option::is_some(&subdomain_name); let user_balance_before = coin::balance(user_addr); + let user_reverse_lookup_before = domains::get_reverse_lookup(user_addr); + let maybe_target_address = domains::name_resolved_address(subdomain_name, domain_name); + let name_reverse_lookup_before = if (option::is_some(&maybe_target_address)) { + domains::get_reverse_lookup(*option::borrow(&maybe_target_address)) + } else { + option::none() + }; + let is_expired_before = domains::name_is_registered(subdomain_name, domain_name) && domains::name_is_expired(subdomain_name, domain_name); let register_name_event_v1_event_count_before = domains::get_register_name_event_v1_count(); let set_name_address_event_v1_event_count_before = domains::get_set_name_address_event_v1_count(); + let set_reverse_lookup_event_v1_event_count_before = domains::get_set_reverse_lookup_event_v1_count(); let years = (time_helper::seconds_to_years(registration_duration_secs) as u8); if (option::is_none(&subdomain_name)) { @@ -99,7 +108,7 @@ module aptos_names::test_helper { if (is_subdomain) { // If it's a subdomain, we only charge a nomincal fee expected_user_balance_after = user_balance_before - price_model::price_for_subdomain_v1(registration_duration_secs); - }else { + } else { let domain_price = price_model::price_for_domain_v1(string::length(&domain_name), years); assert!(domain_price / config::octas() == 40, domain_price / config::octas()); expected_user_balance_after = user_balance_before - domain_price; @@ -113,8 +122,13 @@ module aptos_names::test_helper { assert!(time_helper::seconds_to_days(expiration_time_sec - timestamp::now_seconds()) == 365, 10); if (is_subdomain) { - // We haven't set a target address yet! - assert!(target_address == option::none(), 11); + if (option::is_none(&user_reverse_lookup_before)) { + // Should automatically point to the users address + assert!(target_address == option::some(user_addr), 11); + } else { + // We haven't set a target address yet! + assert!(target_address == option::none(), 11); + } } else { // Should automatically point to the users address assert!(target_address == option::some(user_addr), 11); @@ -134,14 +148,56 @@ module aptos_names::test_helper { // Assert events have been correctly emmitted let register_name_event_v1_num_emitted = domains::get_register_name_event_v1_count() - register_name_event_v1_event_count_before; let set_name_address_event_v1_num_emitted = domains::get_set_name_address_event_v1_count() - set_name_address_event_v1_event_count_before; + let set_reverse_lookup_event_v1_num_emitted = domains::get_set_reverse_lookup_event_v1_count() - set_reverse_lookup_event_v1_event_count_before; test_utils::print_actual_expected(b"register_name_event_v1_num_emitted: ", register_name_event_v1_num_emitted, 1, false); assert!(register_name_event_v1_num_emitted == 1, register_name_event_v1_num_emitted); + // Reverse lookup should be set if user did not have one before + if (option::is_none(&user_reverse_lookup_before)) { + let maybe_reverse_lookup_after = domains::get_reverse_lookup(user_addr); + if (option::is_some(&maybe_reverse_lookup_after)) { + let reverse_lookup_after = option::borrow(&maybe_reverse_lookup_after); + assert!(*reverse_lookup_after == domains::create_name_record_key_v1(subdomain_name, domain_name), 36); + } else { + // Reverse lookup is not set, even though user did not have a reverse lookup before. + assert!(false, 37); + }; + // If we are registering over a name that is already registered but expired and was a primary name, + // that name should be removed from being a primary name. + if (option::is_some(&name_reverse_lookup_before) && is_expired_before) { + assert!(set_reverse_lookup_event_v1_num_emitted == 2, set_reverse_lookup_event_v1_num_emitted); + } else { + assert!(set_reverse_lookup_event_v1_num_emitted == 1, set_reverse_lookup_event_v1_num_emitted); + } + } else { + // If we are registering over a name that is already registered but expired and was the user's primary name, + // that name should be removed from being a primary name and the new one should be set. + if (option::is_some(&name_reverse_lookup_before) + && option::is_some(&user_reverse_lookup_before) + && *option::borrow(&name_reverse_lookup_before) == *option::borrow(&user_reverse_lookup_before) + && is_expired_before + ) { + assert!(set_reverse_lookup_event_v1_num_emitted == 2, set_reverse_lookup_event_v1_num_emitted); + } else if (option::is_some(&name_reverse_lookup_before) && is_expired_before) { + // If we are registering over a name that is already registered but expired and was a primary name, + // that name should be removed from being a primary name. + assert!(set_reverse_lookup_event_v1_num_emitted == 1, set_reverse_lookup_event_v1_num_emitted); + } else { + assert!(set_reverse_lookup_event_v1_num_emitted == 0, set_reverse_lookup_event_v1_num_emitted); + } + }; + if (is_subdomain) { - // We haven't set a target address yet! - test_utils::print_actual_expected(b"set_name_address_event_v1_num_emitted: ", set_name_address_event_v1_num_emitted, 0, false); - assert!(set_name_address_event_v1_num_emitted == 0, set_name_address_event_v1_num_emitted); + if (option::is_none(&user_reverse_lookup_before)) { + // Should automatically point to the users address + test_utils::print_actual_expected(b"set_name_address_event_v1_num_emitted: ", set_name_address_event_v1_num_emitted, 1, false); + assert!(set_name_address_event_v1_num_emitted == 1, set_name_address_event_v1_num_emitted); + } else { + // We haven't set a target address yet! + test_utils::print_actual_expected(b"set_name_address_event_v1_num_emitted: ", set_name_address_event_v1_num_emitted, 0, false); + assert!(set_name_address_event_v1_num_emitted == 0, set_name_address_event_v1_num_emitted); + } } else { // Should automatically point to the users address test_utils::print_actual_expected(b"set_name_address_event_v1_num_emitted: ", set_name_address_event_v1_num_emitted, 1, false); @@ -151,29 +207,51 @@ module aptos_names::test_helper { /// Set the domain address, and verify the address was set correctly public fun set_name_address(user: &signer, subdomain_name: Option, domain_name: String, expected_target_address: address) { + let user_addr = signer::address_of(user); + let register_name_event_v1_event_count_before = domains::get_register_name_event_v1_count(); let set_name_address_event_v1_event_count_before = domains::get_set_name_address_event_v1_count(); + let set_reverse_lookup_event_v1_event_count_before = domains::get_set_reverse_lookup_event_v1_count(); + let maybe_reverse_lookup_before = domains::get_reverse_lookup(user_addr); domains::set_name_address(user, subdomain_name, domain_name, expected_target_address); let (_property_version, _expiration_time_sec, target_address) = domains::get_name_record_v1_props_for_name(subdomain_name, domain_name); test_utils::print_actual_expected(b"set_domain_address: ", target_address, option::some(expected_target_address), false); assert!(target_address == option::some(expected_target_address), 33); + // When setting the target address to an address that is *not* the owner's, the reverse lookup should also be cleared + if (signer::address_of(user) != expected_target_address) { + let maybe_reverse_lookup = domains::get_reverse_lookup(user_addr); + assert!(option::is_none(&maybe_reverse_lookup), 33); + }; + // Assert events have been correctly emmitted let register_name_event_v1_num_emitted = domains::get_register_name_event_v1_count() - register_name_event_v1_event_count_before; let set_name_address_event_v1_num_emitted = domains::get_set_name_address_event_v1_count() - set_name_address_event_v1_event_count_before; + let set_reverse_lookup_event_v1_num_emitted = domains::get_set_reverse_lookup_event_v1_count() - set_reverse_lookup_event_v1_event_count_before; test_utils::print_actual_expected(b"register_name_event_v1_num_emitted: ", register_name_event_v1_num_emitted, 0, false); assert!(register_name_event_v1_num_emitted == 0, register_name_event_v1_num_emitted); test_utils::print_actual_expected(b"set_name_address_event_v1_num_emitted: ", set_name_address_event_v1_num_emitted, 1, false); assert!(set_name_address_event_v1_num_emitted == 1, set_name_address_event_v1_num_emitted); + + // If the signer had a reverse lookup before, and set his reverse lookup name to a different address, it should be cleared + if (option::is_some(&maybe_reverse_lookup_before)) { + let (maybe_reverse_subdomain, reverse_domain) = domains::get_name_record_key_v1_props(option::borrow(&maybe_reverse_lookup_before)); + if (maybe_reverse_subdomain == subdomain_name && reverse_domain == domain_name && signer::address_of(user) != expected_target_address) { + assert!(set_reverse_lookup_event_v1_num_emitted == 1, set_reverse_lookup_event_v1_num_emitted); + }; + }; } /// Clear the domain address, and verify the address was cleared public fun clear_name_address(user: &signer, subdomain_name: Option, domain_name: String) { + let user_addr = signer::address_of(user); let register_name_event_v1_event_count_before = domains::get_register_name_event_v1_count(); let set_name_address_event_v1_event_count_before = domains::get_set_name_address_event_v1_count(); + let set_reverse_lookup_event_v1_event_count_before = domains::get_set_reverse_lookup_event_v1_count(); + let maybe_reverse_lookup_before = domains::get_reverse_lookup(user_addr); // And also can clear if is registered address, but not owner if (option::is_none(&subdomain_name)) { @@ -185,6 +263,17 @@ module aptos_names::test_helper { test_utils::print_actual_expected(b"clear_domain_address: ", target_address, option::none(), false); assert!(target_address == option::none(), 32); + if (option::is_some(&maybe_reverse_lookup_before)) { + let reverse_lookup_before = option::borrow(&maybe_reverse_lookup_before); + if (*reverse_lookup_before == domains::create_name_record_key_v1(subdomain_name, domain_name)) { + let reverse_lookup_after = domains::get_reverse_lookup(user_addr); + assert!(option::is_none(&reverse_lookup_after), 35); + + let set_reverse_lookup_event_v1_num_emitted = domains::get_set_reverse_lookup_event_v1_count() - set_reverse_lookup_event_v1_event_count_before; + assert!(set_reverse_lookup_event_v1_num_emitted == 1, set_reverse_lookup_event_v1_num_emitted); + }; + }; + // Assert events have been correctly emmitted let register_name_event_v1_num_emitted = domains::get_register_name_event_v1_count() - register_name_event_v1_event_count_before; let set_name_address_event_v1_num_emitted = domains::get_set_name_address_event_v1_count() - set_name_address_event_v1_event_count_before;