diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a42fa6d..7c17e88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: CI Tests on: [push, pull_request, pull_request_target] env: - SCARB_VERSION: 0.7.0 + SCARB_VERSION: 2.3.0 jobs: scarb-tests: diff --git a/.gitignore b/.gitignore index 37cebce..f5fc33f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +Scarb.lock .env venv target diff --git a/Scarb.toml b/Scarb.toml index 9e0c8f7..afe2dba 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -5,8 +5,8 @@ version = "0.1.0" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest [dependencies] -starknet = "2.2.0" -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", branch = "main" } +starknet = "2.3.0" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.8.0-beta.0" } identity = { git = "https://github.com/starknet-id/identity.git", branch = "master" } [[target.starknet-contract]] diff --git a/src/interface/naming.cairo b/src/interface/naming.cairo index 00c24ab..7f3cc42 100644 --- a/src/interface/naming.cairo +++ b/src/interface/naming.cairo @@ -16,7 +16,7 @@ trait INaming { self: @TContractState, domain: Span, hint: Span ) -> ContractAddress; - fn address_to_domain(self: @TContractState, address: ContractAddress) -> Array; + fn address_to_domain(self: @TContractState, address: ContractAddress) -> Span; // external fn buy( @@ -43,6 +43,10 @@ trait INaming { fn reset_subdomains(ref self: TContractState, domain: Span); + fn set_address_to_domain(ref self: TContractState, domain: Span); + + fn reset_address_to_domain(ref self: TContractState); + // admin fn set_admin(ref self: TContractState, new_admin: ContractAddress); diff --git a/src/naming.cairo b/src/naming.cairo index d18669a..a967bc9 100644 --- a/src/naming.cairo +++ b/src/naming.cairo @@ -1 +1,4 @@ mod main; +mod internal; +mod asserts; +mod utils; \ No newline at end of file diff --git a/src/naming/asserts.cairo b/src/naming/asserts.cairo new file mode 100644 index 0000000..a1cbfde --- /dev/null +++ b/src/naming/asserts.cairo @@ -0,0 +1,107 @@ +use naming::{ + interface::{ + naming::{INaming, INamingDispatcher, INamingDispatcherTrait}, + resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait}, + pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait}, + referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait}, + }, + naming::main::{ + Naming, + Naming::{ + ContractStateEventEmitter, _hash_to_domain, _hash_to_domainContractMemberStateTrait, + _domain_data, _domain_dataContractMemberStateTrait, starknetid_contract, + starknetid_contractContractMemberStateTrait, discounts, + discountsContractMemberStateTrait, _address_to_domain, + _address_to_domainContractMemberStateTrait, _referral_contract, + _referral_contractContractMemberStateTrait, + } + }, +}; +use identity::interface::identity::{IIdentity, IIdentityDispatcher, IIdentityDispatcherTrait}; +use starknet::{ + contract_address::ContractAddressZeroable, ContractAddress, get_caller_address, + get_contract_address, get_block_timestamp +}; +use openzeppelin::token::erc20::interface::{ + IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait +}; +use integer::{u256_safe_divmod, u256_as_non_zero}; +use naming::naming::utils::UtilsTrait; + + +#[generate_trait] +impl AssertionsImpl of AssertionsTrait { + fn assert_purchase_is_possible( + self: @Naming::ContractState, identity: u128, domain: felt252, days: u16 + ) -> (felt252, u64, u64) { + let now = get_block_timestamp(); + + // Verify that the starknet.id doesn't already manage a domain + self.assert_id_availability(identity, now); + + // Verify that the domain is not already taken or expired + let hashed_domain = self.hash_domain(array![domain].span()); + let data = self._domain_data.read(hashed_domain); + assert(data.owner == 0 || data.expiry < now, 'unexpired domain'); + + // Verify expiration range + assert(days < 365 * 25, 'max purchase of 25 years'); + assert(days > 2 * 30, 'min purchase of 2 month'); + return (hashed_domain, now, now + 86400 * days.into()); + } + + fn assert_control_domain( + self: @Naming::ContractState, domain: Span, account: ContractAddress + ) { + // 1. account owns the domain + self.assert_is_owner(domain, account); + // 2. check domain expiration + let hashed_root_domain = self.hash_domain(domain.slice(domain.len() - 1, 1)); + let root_domain_data = self._domain_data.read(hashed_root_domain); + assert(get_block_timestamp() <= root_domain_data.expiry, 'this domain has expired'); + } + + fn assert_is_owner( + self: @Naming::ContractState, domain: Span, account: ContractAddress + ) -> u32 { + let hashed_domain = self.hash_domain(domain); + let data = self._domain_data.read(hashed_domain); + + // because erc721 crashes on zero + let owner = if data.owner == 0 { + ContractAddressZeroable::zero() + } else { + IIdentityDispatcher { contract_address: self.starknetid_contract.read() } + .owner_of(data.owner) + }; + + // if caller owns the starknet id, he owns the domain, we return the key + if owner == account { + return data.key; + }; + + // otherwise, if it is a root domain, he doesn't own it + assert(domain.len() != 1 && domain.len() != 0, 'you don\'t own this domain'); + + // if he doesn't own the starknet id, and doesn't own the domain, he might own the parent domain + let parent_key = self.assert_is_owner(domain.slice(1, domain.len() - 1), account); + // we ensure that the key is the same as the parent key + // this is to allow to revoke all subdomains in o(1) writes, by juste updating the key of the parent + if (data.parent_key != 0) { + assert(parent_key == data.parent_key, 'you no longer own this domain'); + }; + data.key + } + + // this ensures a non expired domain is not already written on this identity + fn assert_id_availability(self: @Naming::ContractState, identity: u128, timestamp: u64) { + let id_hashed_domain = IIdentityDispatcher { + contract_address: self.starknetid_contract.read() + } + .get_verifier_data(identity, 'name', get_contract_address(), 0); + assert( + id_hashed_domain == 0 || self._domain_data.read(id_hashed_domain).expiry < timestamp, + 'this id holds a domain' + ); + } +} diff --git a/src/naming/internal.cairo b/src/naming/internal.cairo new file mode 100644 index 0000000..c35ffeb --- /dev/null +++ b/src/naming/internal.cairo @@ -0,0 +1,184 @@ +use naming::{ + interface::{ + naming::{INaming, INamingDispatcher, INamingDispatcherTrait}, + resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait}, + pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait}, + referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait}, + }, + naming::main::{ + Naming, + Naming::{ + ContractStateEventEmitter, _hash_to_domain, _hash_to_domainContractMemberStateTrait, + _domain_data, _domain_dataContractMemberStateTrait, starknetid_contract, + starknetid_contractContractMemberStateTrait, discounts, + discountsContractMemberStateTrait, _address_to_domain, + _address_to_domainContractMemberStateTrait, _referral_contract, + _referral_contractContractMemberStateTrait, + } + } +}; +use identity::interface::identity::{IIdentity, IIdentityDispatcher, IIdentityDispatcherTrait}; +use starknet::{ + contract_address::ContractAddressZeroable, ContractAddress, get_caller_address, + get_contract_address, get_block_timestamp +}; +use openzeppelin::token::erc20::interface::{ + IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait +}; +use naming::naming::utils::UtilsTrait; + +#[generate_trait] +impl InternalImpl of InternalTrait { + fn read_address_to_domain( + self: @Naming::ContractState, address: ContractAddress, ref domain: Array + ) -> usize { + let subdomain = self._address_to_domain.read((address, domain.len())); + if subdomain == 0 { + domain.len() + } else { + domain.append(subdomain); + self.read_address_to_domain(address, ref domain) + } + } + + fn set_address_to_domain_util( + ref self: Naming::ContractState, address: ContractAddress, mut domain: Span + ) { + match domain.pop_back() { + Option::Some(domain_part) => { + self._address_to_domain.write((address, domain.len()), *domain_part); + self.set_address_to_domain_util(address, domain) + }, + Option::None => {} + } + } + + fn domain_to_resolver( + self: @Naming::ContractState, domain: Span, parent_start_id: u32 + ) -> (ContractAddress, u32) { + if parent_start_id == domain.len() { + return (ContractAddressZeroable::zero(), 0); + }; + + // hashing parent_domain + let hashed_domain = self + .hash_domain(domain.slice(parent_start_id, domain.len() - parent_start_id)); + + let domain_data = self._domain_data.read(hashed_domain); + + if domain_data.resolver.into() != 0 { + return (domain_data.resolver, parent_start_id); + } else { + return self.domain_to_resolver(domain, parent_start_id + 1); + } + } + + fn pay_domain( + self: @Naming::ContractState, + domain_len: usize, + erc20: ContractAddress, + price: u256, + now: u64, + days: u16, + domain: felt252, + sponsor: ContractAddress, + discount_id: felt252 + ) -> () { + // check the discount + let discounted_price = if (discount_id == 0) { + price + } else { + let discount = self.discounts.read(discount_id); + let (min, max) = discount.domain_len_range; + assert(min <= domain_len && domain_len <= max, 'invalid length for discount'); + + let (min, max) = discount.days_range; + assert(min <= days && days <= max, 'days out of discount range'); + + let (min, max) = discount.timestamp_range; + assert(min <= now && now <= max, 'time out of discount range'); + // discount.amount won't overflow as it's a value chosen by the admin to be in range (0, 100) + (price * discount.amount) / 100 + }; + + // pay the price + IERC20CamelDispatcher { contract_address: erc20 } + .transferFrom(get_caller_address(), get_contract_address(), discounted_price); + // add sponsor commission if eligible + if sponsor.into() != 0 { + IReferralDispatcher { contract_address: self._referral_contract.read() } + .add_commission(discounted_price, sponsor, sponsored_addr: get_caller_address()); + } + } + + fn mint_domain( + ref self: Naming::ContractState, + expiry: u64, + resolver: ContractAddress, + hashed_domain: felt252, + id: u128, + domain: felt252 + ) { + let data = Naming::DomainData { + owner: id, + resolver, + address: ContractAddressZeroable::zero(), // legacy native address + expiry, + key: 1, + parent_key: 0, + }; + self._hash_to_domain.write((hashed_domain, 0), domain); + self._domain_data.write(hashed_domain, data); + self.emit(Naming::Event::DomainMint(Naming::DomainMint { domain, owner: id, expiry })); + + IIdentityDispatcher { contract_address: self.starknetid_contract.read() } + .set_verifier_data(id, 'name', hashed_domain, 0); + if (resolver.into() != 0) { + self + .emit( + Naming::Event::DomainResolverUpdate( + Naming::DomainResolverUpdate { domain: array![domain].span(), resolver } + ) + ); + } + } + + // returns domain_hash (or zero) and its value for a specific field + fn resolve_util( + self: @Naming::ContractState, domain: Span, field: felt252, hint: Span + ) -> (felt252, felt252) { + let (resolver, parent_start) = self.domain_to_resolver(domain, 0); + if (resolver != ContractAddressZeroable::zero()) { + ( + 0, + IResolverDispatcher { contract_address: resolver } + .resolve(domain.slice(parent_start, domain.len() - parent_start), field, hint) + ) + } else { + let hashed_domain = self.hash_domain(domain); + let domain_data = self._domain_data.read(hashed_domain); + // circuit breaker for root domain + ( + hashed_domain, + if (domain.len() == 1) { + IIdentityDispatcher { contract_address: self.starknetid_contract.read() } + .get_crosschecked_user_data(domain_data.owner, field) + // handle reset subdomains + } else { + // todo: optimize by changing the hash definition from H(b, a) to H(a, b) + let parent_key = self + ._domain_data + .read(self.hash_domain(domain.slice(1, domain.len() - 1))) + .key; + + if parent_key == domain_data.parent_key { + IIdentityDispatcher { contract_address: self.starknetid_contract.read() } + .get_crosschecked_user_data(domain_data.owner, field) + } else { + 0 + } + } + ) + } + } +} diff --git a/src/naming/main.cairo b/src/naming/main.cairo index e1dffd8..23e2659 100644 --- a/src/naming/main.cairo +++ b/src/naming/main.cairo @@ -10,11 +10,14 @@ mod Naming { use starknet::class_hash::ClassHash; use integer::{u256_safe_divmod, u256_as_non_zero}; use core::pedersen; - use naming::interface::{ - naming::{INaming, INamingDispatcher, INamingDispatcherTrait}, - resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait}, - pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait}, - referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait}, + use naming::{ + naming::{asserts::AssertionsTrait, internal::InternalTrait, utils::UtilsTrait}, + interface::{ + naming::{INaming, INamingDispatcher, INamingDispatcherTrait}, + resolver::{IResolver, IResolverDispatcher, IResolverDispatcherTrait}, + pricing::{IPricing, IPricingDispatcher, IPricingDispatcherTrait}, + referral::{IReferral, IReferralDispatcher, IReferralDispatcherTrait}, + } }; use clone::Clone; use array::ArrayTCloneImpl; @@ -29,7 +32,8 @@ mod Naming { enum Event { DomainMint: DomainMint, DomainRenewal: DomainRenewal, - DomainToResolver: DomainToResolver, + DomainResolverUpdate: DomainResolverUpdate, + AddressToDomainUpdate: AddressToDomainUpdate, DomainTransfer: DomainTransfer, SubdomainsReset: SubdomainsReset, SaleMetadata: SaleMetadata, @@ -51,12 +55,19 @@ mod Naming { } #[derive(Drop, starknet::Event)] - struct DomainToResolver { + struct DomainResolverUpdate { #[key] domain: Span, resolver: ContractAddress } + #[derive(Drop, starknet::Event)] + struct AddressToDomainUpdate { + #[key] + address: ContractAddress, + domain: Span, + } + #[derive(Drop, starknet::Event)] struct DomainTransfer { #[key] @@ -133,32 +144,8 @@ mod Naming { fn resolve( self: @ContractState, domain: Span, field: felt252, hint: Span ) -> felt252 { - let (resolver, parent_start) = self.domain_to_resolver(domain, 0); - if (resolver != ContractAddressZeroable::zero()) { - IResolverDispatcher { contract_address: resolver } - .resolve(domain.slice(parent_start, domain.len() - parent_start), field, hint) - } else { - let domain_data = self._domain_data.read(self.hash_domain(domain)); - // circuit breaker for root domain - if (domain.len() == 1) { - IIdentityDispatcher { contract_address: self.starknetid_contract.read() } - .get_crosschecked_user_data(domain_data.owner, field) - // handle reset subdomains - } else { - // todo: optimize by changing the hash definition from H(b, a) to H(a, b) - let parent_key = self - ._domain_data - .read(self.hash_domain(domain.slice(1, domain.len() - 1))) - .key; - - if parent_key == domain_data.parent_key { - IIdentityDispatcher { contract_address: self.starknetid_contract.read() } - .get_crosschecked_user_data(domain_data.owner, field) - } else { - 0 - } - } - } + let (_, value) = self.resolve_util(domain, field, hint); + value } // This functions allows to resolve a domain to a native address. Its output is designed @@ -168,12 +155,12 @@ mod Naming { self: @ContractState, domain: Span, hint: Span ) -> ContractAddress { // resolve must be performed first because it calls untrusted resolving contracts - let resolve_result = self.resolve(domain, 'starknet', hint); - if resolve_result != 0 { - let addr: Option = resolve_result.try_into(); + let (hashed_domain, value) = self.resolve_util(domain, 'starknet', hint); + if value != 0 { + let addr: Option = value.try_into(); return addr.unwrap(); - } - let data = self._domain_data.read(self.hash_domain(domain)); + }; + let data = self._domain_data.read(hashed_domain); if data.address.into() != 0 { if domain.len() != 1 { let parent_key = self @@ -182,10 +169,10 @@ mod Naming { .key; if parent_key == data.parent_key { return data.address; - } - } + }; + }; return data.address; - } + }; IIdentityDispatcher { contract_address: self.starknetid_contract.read() } .owner_of(self.domain_to_id(domain)) } @@ -205,18 +192,18 @@ mod Naming { .key; if parent_key != data.parent_key { return 0; - } - } + }; + }; data.owner } // This function allows to find which domain to use to display an account - fn address_to_domain(self: @ContractState, address: ContractAddress) -> Array { + fn address_to_domain(self: @ContractState, address: ContractAddress) -> Span { let mut domain = ArrayTrait::new(); - self._address_to_domain_util(address, ref domain); + self.read_address_to_domain(address, ref domain); if domain.len() != 0 && self.domain_to_address(domain.span(), array![].span()) == address { - domain + domain.span() } else { let identity = IIdentityDispatcher { contract_address: self.starknetid_contract.read() @@ -227,7 +214,7 @@ mod Naming { .get_verifier_data(id, 'name', get_contract_address(), 0); let domain = self.unhash_domain(id_hashed_domain); assert( - self.domain_to_address(domain.span(), array![].span()) == address, + self.domain_to_address(domain, array![].span()) == address, 'domain not pointing back' ); domain @@ -366,6 +353,29 @@ mod Naming { self.emit(Event::SubdomainsReset(SubdomainsReset { domain: domain, })); } + + // will override your main id + fn set_address_to_domain(ref self: ContractState, domain: Span) { + let address = get_caller_address(); + assert( + self.domain_to_address(domain, array![].span()) == address, + 'domain not pointing back' + ); + self.emit(Event::AddressToDomainUpdate(AddressToDomainUpdate { address, domain })); + self.set_address_to_domain_util(address, domain); + } + + fn reset_address_to_domain(ref self: ContractState) { + let address = get_caller_address(); + self + .emit( + Event::AddressToDomainUpdate( + AddressToDomainUpdate { address, domain: array![].span() } + ) + ); + self.set_address_to_domain_util(address, array![0].span()); + } + // ADMIN fn set_admin(ref self: ContractState, new_admin: ContractAddress) { @@ -403,229 +413,5 @@ mod Naming { starknet::replace_class_syscall(new_class_hash).unwrap(); } } - - #[generate_trait] - impl InternalImpl of InternalTrait { - // hash(alpha.bravo.stark) = pedersen(bravo, pedersen(alpha, 0)) - fn hash_domain(self: @ContractState, domain: Span) -> felt252 { - if domain.len() == 0 { - return 0; - } - let new_len = domain.len() - 1; - let x = *domain[new_len]; - let y = self.hash_domain(domain.slice(0, new_len)); - let hashed_domain = pedersen::pedersen(x, y); - return hashed_domain; - } - - fn unhash_domain(self: @ContractState, domain_hash: felt252) -> Array { - let mut i = 0; - let mut domain = ArrayTrait::new(); - loop { - let domain_part = self._hash_to_domain.read((domain_hash, i)); - if domain_part == 0 { - break; - } - domain.append(domain_part); - }; - domain - } - - fn assert_purchase_is_possible( - self: @ContractState, identity: u128, domain: felt252, days: u16 - ) -> (felt252, u64, u64) { - let now = get_block_timestamp(); - - // Verify that the starknet.id doesn't already manage a domain - self.assert_id_availability(identity, now); - - // Verify that the domain is not already taken or expired - let hashed_domain = self.hash_domain(array![domain].span()); - let data = self._domain_data.read(hashed_domain); - assert(data.owner == 0 || data.expiry < now, 'unexpired domain'); - - // Verify expiration range - assert(days < 365 * 25, 'max purchase of 25 years'); - assert(days > 2 * 30, 'min purchase of 2 month'); - return (hashed_domain, now, now + 86400 * days.into()); - } - - fn assert_control_domain( - self: @ContractState, domain: Span, account: ContractAddress - ) { - // 1. account owns the domain - self._assert_is_owner(domain, account); - // 2. check domain expiration - let hashed_root_domain = self.hash_domain(domain.slice(domain.len() - 1, 1)); - let root_domain_data = self._domain_data.read(hashed_root_domain); - assert(get_block_timestamp() <= root_domain_data.expiry, 'this domain has expired'); - } - - fn _assert_is_owner( - self: @ContractState, domain: Span, account: ContractAddress - ) -> u32 { - let hashed_domain = self.hash_domain(domain); - let data = self._domain_data.read(hashed_domain); - - // because erc721 crashes on zero - let owner = if data.owner == 0 { - ContractAddressZeroable::zero() - } else { - IIdentityDispatcher { contract_address: self.starknetid_contract.read() } - .owner_of(data.owner) - }; - - // if caller owns the starknet id, he owns the domain, we return the key - if owner == account { - return data.key; - } - - // otherwise, if it is a root domain, he doesn't own it - assert(domain.len() != 1 && domain.len() != 0, 'you don\'t own this domain'); - - // if he doesn't own the starknet id, and doesn't own the domain, he might own the parent domain - let parent_key = self._assert_is_owner(domain.slice(1, domain.len() - 1), account); - // we ensure that the key is the same as the parent key - // this is to allow to revoke all subdomains in o(1) writes, by juste updating the key of the parent - if (data.parent_key != 0) { - assert(parent_key == data.parent_key, 'you no longer own this domain'); - } - data.key - } - - // this ensures a non expired domain is not already written on this identity - fn assert_id_availability(self: @ContractState, identity: u128, timestamp: u64) { - let id_hashed_domain = IIdentityDispatcher { - contract_address: self.starknetid_contract.read() - } - .get_verifier_data(identity, 'name', get_contract_address(), 0); - assert( - id_hashed_domain == 0 - || self._domain_data.read(id_hashed_domain).expiry < timestamp, - 'this id holds a domain' - ); - } - - fn _address_to_domain_util( - self: @ContractState, address: ContractAddress, ref domain: Array - ) -> usize { - let subdomain = self._address_to_domain.read((address, domain.len())); - if subdomain == 0 { - domain.len() - } else { - domain.append(subdomain); - self._address_to_domain_util(address, ref domain) - } - } - - fn domain_to_resolver( - self: @ContractState, domain: Span, parent_start_id: u32 - ) -> (ContractAddress, u32) { - if parent_start_id == domain.len() { - return (ContractAddressZeroable::zero(), 0); - } - - // hashing parent_domain - let hashed_domain = self - .hash_domain(domain.slice(parent_start_id, domain.len() - parent_start_id)); - - let domain_data = self._domain_data.read(hashed_domain); - - if domain_data.resolver.into() != 0 { - return (domain_data.resolver, parent_start_id); - } else { - return self.domain_to_resolver(domain, parent_start_id + 1); - } - } - - fn pay_domain( - self: @ContractState, - domain_len: usize, - erc20: ContractAddress, - price: u256, - now: u64, - days: u16, - domain: felt252, - sponsor: ContractAddress, - discount_id: felt252 - ) -> () { - // check the discount - let discounted_price = if (discount_id == 0) { - price - } else { - let discount = self.discounts.read(discount_id); - let (min, max) = discount.domain_len_range; - assert(min <= domain_len && domain_len <= max, 'invalid length for discount'); - - let (min, max) = discount.days_range; - assert(min <= days && days <= max, 'days out of discount range'); - - let (min, max) = discount.timestamp_range; - assert(min <= now && now <= max, 'time out of discount range'); - // discount.amount won't overflow as it's a value chosen by the admin to be in range (0, 100) - (price * discount.amount) / 100 - }; - - // pay the price - IERC20CamelDispatcher { contract_address: erc20 } - .transferFrom(get_caller_address(), get_contract_address(), discounted_price); - // add sponsor commission if eligible - if sponsor.into() != 0 { - IReferralDispatcher { contract_address: self._referral_contract.read() } - .add_commission( - discounted_price, sponsor, sponsored_addr: get_caller_address() - ); - } - } - - fn mint_domain( - ref self: ContractState, - expiry: u64, - resolver: ContractAddress, - hashed_domain: felt252, - id: u128, - domain: felt252 - ) { - let data = DomainData { - owner: id, - resolver, - address: ContractAddressZeroable::zero(), // legacy native address - expiry, - key: 1, - parent_key: 0, - }; - self._hash_to_domain.write((hashed_domain, 0), hashed_domain); - self._domain_data.write(hashed_domain, data); - self.emit(Event::DomainMint(DomainMint { domain, owner: id, expiry })); - - IIdentityDispatcher { contract_address: self.starknetid_contract.read() } - .set_verifier_data(id, 'name', hashed_domain, 0); - if (resolver.into() != 0) { - self - .emit( - Event::DomainToResolver( - DomainToResolver { domain: array![domain].span(), resolver } - ) - ); - } - } - - fn get_chars_len(self: @ContractState, domain: u256) -> usize { - if domain == (u256 { low: 0, high: 0 }) { - return 0; - } - // 38 = simple_alphabet_size - let (p, q, _) = u256_safe_divmod(domain, u256_as_non_zero(u256 { low: 38, high: 0 })); - if q == (u256 { low: 37, high: 0 }) { - // 3 = complex_alphabet_size - let (shifted_p, _, _) = u256_safe_divmod( - p, u256_as_non_zero(u256 { low: 2, high: 0 }) - ); - let next = self.get_chars_len(shifted_p); - return 1 + next; - } - let next = self.get_chars_len(p); - 1 + next - } - } } + diff --git a/src/naming/utils.cairo b/src/naming/utils.cairo new file mode 100644 index 0000000..5c73168 --- /dev/null +++ b/src/naming/utils.cairo @@ -0,0 +1,48 @@ +use naming::{ + naming::main::{Naming, Naming::{_hash_to_domain, _hash_to_domainContractMemberStateTrait}} +}; +use integer::{u256_safe_divmod, u256_as_non_zero}; + +#[generate_trait] +impl UtilsImpl of UtilsTrait { + fn hash_domain(self: @Naming::ContractState, domain: Span) -> felt252 { + if domain.len() == 0 { + return 0; + }; + let new_len = domain.len() - 1; + let x = *domain[new_len]; + let y = self.hash_domain(domain.slice(0, new_len)); + let hashed_domain = pedersen::pedersen(x, y); + return hashed_domain; + } + + fn unhash_domain(self: @Naming::ContractState, domain_hash: felt252) -> Span { + let mut i = 0; + let mut domain = ArrayTrait::new(); + loop { + let domain_part = self._hash_to_domain.read((domain_hash, i)); + if domain_part == 0 { + break; + }; + domain.append(domain_part); + i += 1; + }; + domain.span() + } + + fn get_chars_len(self: @Naming::ContractState, domain: u256) -> usize { + if domain == (u256 { low: 0, high: 0 }) { + return 0; + }; + // 38 = simple_alphabet_size + let (p, q, _) = u256_safe_divmod(domain, u256_as_non_zero(u256 { low: 38, high: 0 })); + if q == (u256 { low: 37, high: 0 }) { + // 3 = complex_alphabet_size + let (shifted_p, _, _) = u256_safe_divmod(p, u256_as_non_zero(u256 { low: 2, high: 0 })); + let next = self.get_chars_len(shifted_p); + return 1 + next; + }; + let next = self.get_chars_len(p); + 1 + next + } +} diff --git a/src/tests/naming/common.cairo b/src/tests/naming/common.cairo index 007b7fe..1e42475 100644 --- a/src/tests/naming/common.cairo +++ b/src/tests/naming/common.cairo @@ -23,7 +23,6 @@ use naming::pricing::Pricing; fn deploy() -> (IERC20CamelDispatcher, IPricingDispatcher, IIdentityDispatcher, INamingDispatcher) { //erc20 - let mut calldata = ArrayTrait::::new(); // 0, 1 = low and high of ETH supply let eth = utils::deploy(ERC20::TEST_CLASS_HASH, array!['ether', 'ETH', 0, 1, 0x123]); diff --git a/src/tests/naming/test_features.cairo b/src/tests/naming/test_features.cairo index 92e49ad..b7aae1f 100644 --- a/src/tests/naming/test_features.cairo +++ b/src/tests/naming/test_features.cairo @@ -9,7 +9,6 @@ use starknet::ContractAddress; use starknet::contract_address::ContractAddressZeroable; use starknet::contract_address_const; use starknet::testing::set_contract_address; -use super::super::utils; use openzeppelin::token::erc20::{ erc20::ERC20, interface::{IERC20Camel, IERC20CamelDispatcher, IERC20CamelDispatcherTrait} }; @@ -20,7 +19,9 @@ use naming::interface::naming::{INamingDispatcher, INamingDispatcherTrait}; use naming::interface::pricing::{IPricingDispatcher, IPricingDispatcherTrait}; use naming::naming::main::Naming; use naming::pricing::Pricing; +use naming::naming::utils::UtilsImpl; use super::common::deploy; +use super::super::utils; #[test] @@ -124,25 +125,23 @@ fn test_get_chars_len() { let mut unsafe_state = Naming::unsafe_new_contract_state(); // Should return 0 (empty string) - assert(Naming::InternalImpl::get_chars_len(@unsafe_state, 0) == 0, 'Should return 0'); + assert(UtilsImpl::get_chars_len(@unsafe_state, 0) == 0, 'Should return 0'); // Should return 2 (be) - assert(Naming::InternalImpl::get_chars_len(@unsafe_state, 153) == 2, 'Should return 0'); + assert(UtilsImpl::get_chars_len(@unsafe_state, 153) == 2, 'Should return 0'); // Should return 4 ("toto") - assert(Naming::InternalImpl::get_chars_len(@unsafe_state, 796195) == 4, 'Should return 4'); + assert(UtilsImpl::get_chars_len(@unsafe_state, 796195) == 4, 'Should return 4'); // Should return 5 ("aloha") - assert(Naming::InternalImpl::get_chars_len(@unsafe_state, 77554770) == 5, 'Should return 5'); + assert(UtilsImpl::get_chars_len(@unsafe_state, 77554770) == 5, 'Should return 5'); // Should return 9 ("chocolate") - assert( - Naming::InternalImpl::get_chars_len(@unsafe_state, 19565965532212) == 9, 'Should return 9' - ); + assert(UtilsImpl::get_chars_len(@unsafe_state, 19565965532212) == 9, 'Should return 9'); // Should return 30 ("这来abcdefghijklmopqrstuvwyq1234") assert( - Naming::InternalImpl::get_chars_len( + UtilsImpl::get_chars_len( @unsafe_state, 801855144733576077820330221438165587969903898313 ) == 30, 'Should return 30' diff --git a/src/tests/naming/test_usecases.cairo b/src/tests/naming/test_usecases.cairo index b0f5edb..15b76c7 100644 --- a/src/tests/naming/test_usecases.cairo +++ b/src/tests/naming/test_usecases.cairo @@ -205,3 +205,70 @@ fn test_non_owner_can_renew_domain() { eth.approve(naming.contract_address, price); naming.renew(domain_name, 365, ContractAddressZeroable::zero(), 0, 0); } + + +#[test] +#[available_gas(2000000000)] +fn test_set_address_to_domain() { + // setup + let (eth, pricing, identity, naming) = deploy(); + let caller = contract_address_const::<0x123>(); + set_contract_address(caller); + let id1: u128 = 1; + let id2: u128 = 2; + let first_domain_top: felt252 = 82939898252385817; + let second_domain_top: felt252 = 3151716132312299378; + + //we mint the ids + identity.mint(id1); + identity.mint(id2); + + // buy the domains + let (_, price1) = pricing.compute_buy_price(11, 365); + eth.approve(naming.contract_address, price1); + naming + .buy( + id1, + first_domain_top, + 365, + ContractAddressZeroable::zero(), + ContractAddressZeroable::zero(), + 0, + 0 + ); + + let (_, price2) = pricing.compute_buy_price(12, 365); + eth.approve(naming.contract_address, price2); + naming + .buy( + id2, + second_domain_top, + 365, + ContractAddressZeroable::zero(), + ContractAddressZeroable::zero(), + 0, + 0 + ); + + // let's try the resolving + let first_domain = array![first_domain_top].span(); + assert( + naming.domain_to_address(first_domain, array![].span()) == caller, 'wrong domain target' + ); + + // set reverse resolving + identity.set_main_id(id1); + let expect_domain1 = naming.address_to_domain(caller); + assert(expect_domain1 == first_domain, 'wrong rev resolving 1'); + + // override reverse resolving + let second_domain = array![second_domain_top].span(); + naming.set_address_to_domain(second_domain); + let expect_domain2 = naming.address_to_domain(caller); + assert(expect_domain2 == second_domain, 'wrong rev resolving 2'); + + // remove override + naming.reset_address_to_domain(); + let expect_domain1 = naming.address_to_domain(caller); + assert(expect_domain1 == first_domain, 'wrong rev resolving b'); +}