diff --git a/smart-contract/.nvmrc b/smart-contract/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/smart-contract/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/smart-contract/assembly/contracts/main.ts b/smart-contract/assembly/contracts/main.ts index 5c9de17..5589174 100644 --- a/smart-contract/assembly/contracts/main.ts +++ b/smart-contract/assembly/contracts/main.ts @@ -294,20 +294,19 @@ export function dnsReverseResolve(args: StaticArray): StaticArray { assert(keys.length > 0, 'No domain found for the address'); - let domains = new StaticArray(0); + let domains: u8[] = []; for (let i = 0; i < keys.length; i++) { - const domain: StaticArray = StaticArray.fromArray( - keys[i].slice(prefix.length), - ); + const domain = keys[i].slice(prefix.length); + domains = domains.concat(domain); if (i < keys.length - 1) { - domains = domains.concat(stringToBytes(',')); + domains.push(44 /* coma */); } } - return domains; + return StaticArray.fromArray(domains); } /** diff --git a/smart-contract/assembly/contracts/main_mig.ts b/smart-contract/assembly/contracts/main_mig.ts new file mode 100644 index 0000000..6fa5215 --- /dev/null +++ b/smart-contract/assembly/contracts/main_mig.ts @@ -0,0 +1,516 @@ +import { + Address, + Context, + Storage, + balance, + generateEvent, + getKeys, + setBytecode, + transferCoins, +} from '@massalabs/massa-as-sdk'; +import { + Args, + bytesToString, + bytesToU256, + stringToBytes, + u256ToBytes, + u64ToBytes, +} from '@massalabs/as-types'; +import { + _update, + _ownerOf, + TOTAL_SUPPLY_KEY, +} from '@massalabs/sc-standards/assembly/contracts/MRC721/enumerable/MRC721Enumerable-internals'; +import { + transferFrom as _transferFrom, + mrc721Constructor, +} from '@massalabs/sc-standards/assembly/contracts/MRC721/enumerable/MRC721Enumerable'; +import { + _onlyOwner, + _isOwner, +} from '@massalabs/sc-standards/assembly/contracts/utils/ownership-internal'; + +import { u256 } from 'as-bignum/assembly'; + +export function constructor(_: StaticArray): void { + mrc721Constructor('MassaNameService', 'MNS'); + Storage.set(COUNTER_KEY, u256ToBytes(u256.Zero)); + Storage.set(lockedKey(), u256ToBytes(u256.Zero)); + Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(u256.Zero)); +} + +// DNS RELATED FUNCTIONS + +const DOMAIN_SEPARATOR_KEY: StaticArray = [0x42]; + +const COUNTER_KEY: StaticArray = [0x00]; +const TOKEN_ID_KEY_PREFIX: StaticArray = [0x01]; +const TARGET_KEY_PREFIX: StaticArray = [0x02]; +const DOMAIN_KEY_PREFIX: StaticArray = [0x03]; +const ADDRESS_KEY_PREFIX: StaticArray = [0x04]; +const LOCKED_KEY_PREFIX: StaticArray = [0x05]; + +// Be careful if we edit the values here to increase the price, it requires to change the refund +// logic in dnsFree function to avoid refunding more than the user paid with the old prices. +function calculateCreationCost(sizeDomain: u64): u64 { + if (sizeDomain <= 2) { + return 10_000_000_000_000; + } else if (sizeDomain == 3) { + return 1_000_000_000_000; + } else if (sizeDomain == 4) { + return 100_000_000_000; + } else if (sizeDomain == 5) { + return 10_000_000_000; + } + return 1_000_000_000; +} + +// @ts-ignore (fix for IDE) +@inline +function isNotNumber(c: i32): bool { + const zero = 48; + const nine = 57; + return c < zero || c > nine; +} + +// @ts-ignore (fix for IDE) +@inline +function isNotLowercaseLetter(c: i32): bool { + const a = 97; + const z = 122; + return c < a || c > z; +} + +// @ts-ignore (fix for IDE) +@inline +function isNotHyphen(c: i32): bool { + return c != 45; +} + +export function isValidDomain(domain: string): bool { + if (domain.length < 2 || domain.length > 100) { + return false; + } + for (let i = 0; i < domain.length; i++) { + const c = domain.charCodeAt(i); + // Must be lowercase or hyphen + if (isNotNumber(c) && isNotLowercaseLetter(c) && isNotHyphen(c)) { + return false; + } + } + return true; +} + +function domainToTokenIdKey(domain: string): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + TOKEN_ID_KEY_PREFIX.concat(stringToBytes(domain)), + ); +} + +function tokenIdToDomainKey(tokenId: u256): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + DOMAIN_KEY_PREFIX.concat(u256ToBytes(tokenId)), + ); +} + +function domainToTargetKey(domain: string): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + TARGET_KEY_PREFIX.concat(stringToBytes(domain)), + ); +} + +function targetToDomainKeyPrefix(address: string): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + ADDRESS_KEY_PREFIX.concat(stringToBytes(address)), + ); +} + +function targetToDomainKey(address: string, domain: string): StaticArray { + return targetToDomainKeyPrefix(address).concat(stringToBytes(domain)); +} + +function lockedKey(): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(LOCKED_KEY_PREFIX); +} + +/** + * Lock the contract + */ +export function dnsLock(_: StaticArray): void { + _onlyOwner(); + Storage.set(lockedKey(), u256ToBytes(u256.Zero)); +} + +/** + * Unlock the contract + */ +export function dnsUnlock(_: StaticArray): void { + _onlyOwner(); + Storage.del(lockedKey()); +} + +/** + * Calculate the cost of the dns allocation + * @param binaryArgs - (domain: string, target: string) + * + * @returns cost of the dns allocation as u64 + */ +export function dnsAllocCost(binaryArgs: StaticArray): StaticArray { + const args = new Args(binaryArgs); + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + assert(isValidDomain(domain), 'Invalid domain'); + return u64ToBytes(calculateCreationCost(domain.length) + 100_000_000); +} + +/** + * Register a domain + * @param binaryArgs - (domain: string, target: string) + * @returns tokenId of the dns as u256 + */ +export function dnsAlloc(binaryArgs: StaticArray): StaticArray { + if (Storage.has(lockedKey()) && !_isOwner(Context.caller().toString())) { + throw new Error('Domain allocation is locked'); + } + + const initialBalance = balance(); + const args = new Args(binaryArgs); + + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + const target = args + .nextString() + .expect('target argument is missing or invalid'); + const owner = Context.caller().toString(); + + assert(isValidDomain(domain), 'Invalid domain'); + assert(!Storage.has(domainToTargetKey(domain)), 'Domain already registered'); + + const counter = bytesToU256(Storage.get(COUNTER_KEY)); + + // Mint the token + _update(owner, counter, ''); + + // Store the domain and token ID + Storage.set(domainToTargetKey(domain), stringToBytes(target)); + Storage.set(targetToDomainKey(target, domain), []); + Storage.set(tokenIdToDomainKey(counter), stringToBytes(domain)); + Storage.set(domainToTokenIdKey(domain), u256ToBytes(counter)); + // @ts-ignore (fix for IDE) + Storage.set(COUNTER_KEY, u256ToBytes(counter + u256.One)); + + const storageCosts = initialBalance - balance(); + const totalCost = calculateCreationCost(domain.length) + storageCosts; + const transferredCoins = Context.transferredCoins(); + + assert( + transferredCoins >= totalCost, + `Insufficient funds to register domain. Provided: ${transferredCoins.toString()}, Needed: ${totalCost.toString()}.`, + ); + if (transferredCoins > totalCost) { + transferCoins(Context.caller(), transferredCoins - totalCost); + } + return u256ToBytes(counter); +} + +/** + * Free domain and refund half of the registration fee + * @param binaryArgs - (tokenId: u256) + * @returns void + */ +export function dnsFree(binaryArgs: StaticArray): void { + if (Storage.has(lockedKey()) && !_isOwner(Context.caller().toString())) { + throw new Error('Free is locked'); + } + + const initialBalance = balance(); + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('tokenId argument is missing or invalid'); + + assert( + new Address(_ownerOf(tokenId)) == Context.caller(), + 'Only owner can free domain', + ); + + // Burn the token + _update('', tokenId, ''); + + // Retrieve the domain + const idToDomainKey = tokenIdToDomainKey(tokenId); + assert(Storage.has(idToDomainKey), 'Domain not registered'); + const domain = bytesToString(Storage.get(idToDomainKey)); + + // Retrieve and delete the target + const domainToTargetK = domainToTargetKey(domain); + const target = bytesToString(Storage.get(domainToTargetK)); + + // Delete all associated keys + Storage.del(domainToTargetK); + Storage.del(targetToDomainKey(target, domain)); + Storage.del(idToDomainKey); + Storage.del(domainToTokenIdKey(domain)); + + const finalBalance = balance(); + const storageCostsRefunded = finalBalance - initialBalance; + + const refundTotal = + calculateCreationCost(domain.length) / 2 + + storageCostsRefunded + + Context.transferredCoins(); + + transferCoins(Context.caller(), refundTotal); +} + +/** + * Get the target address associated with a domain + * @param args - (domain: string) + * @returns Address target of the domain + */ +export function dnsResolve(args: StaticArray): StaticArray { + const argsObj = new Args(args); + const domain = argsObj + .nextString() + .expect('domain argument is missing or invalid'); + + return Storage.get(domainToTargetKey(domain)); +} + +/** Get a list of domain associated with an address + * @param args - (address: string) + * + * @returns List of domains as string separated by comma + */ +export function dnsReverseResolve(args: StaticArray): StaticArray { + const argsObj = new Args(args); + const address = argsObj + .nextString() + .expect('address argument is missing or invalid'); + + const prefix = targetToDomainKeyPrefix(address); + const keys = getKeys(prefix); + + assert(keys.length > 0, 'No domain found for the address'); + + let domains: u8[] = []; + + for (let i = 0; i < keys.length; i++) { + const domain = keys[i].slice(prefix.length); + + domains = domains.concat(domain); + + if (i < keys.length - 1) { + domains.push(44 /* coma */); + } + } + + return StaticArray.fromArray(domains); +} + +/** + * Update the target address associated with a domain. Only the owner can update the target. + * @param binaryArgs - (domain: string, newTarget: string) + */ +export function dnsUpdateTarget(binaryArgs: StaticArray): void { + if (Storage.has(lockedKey()) && !_isOwner(Context.caller().toString())) { + throw new Error('Update Target is locked'); + } + + const args = new Args(binaryArgs); + + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + + const newTarget = args + .nextString() + .expect('target argument is missing or invalid'); + + const tokenId = bytesToU256(Storage.get(domainToTokenIdKey(domain))); + const owner = _ownerOf(tokenId); + + assert( + new Address(owner) == Context.caller(), + 'Only owner can update target', + ); + + // remove the old target + const oldTarget = bytesToString(Storage.get(domainToTargetKey(domain))); + Storage.del(targetToDomainKey(oldTarget, domain)); + // Add the domain to the new target + Storage.set(targetToDomainKey(newTarget, domain), []); + // Update the target for the domain + Storage.set(domainToTargetKey(domain), stringToBytes(newTarget)); +} + +/** + * Upgrade the DNS smart contract bytecode + * @param args - new bytecode + * @returns void + */ +export function upgradeSC(args: StaticArray): void { + _onlyOwner(); + setBytecode(args); +} + +/** + * Transfer internal coins to another address + * @param binaryArgs - (to: string, amount: u64) + * @returns void + */ +export function transferInternalCoins(binaryArgs: StaticArray): void { + _onlyOwner(); + const argsObj = new Args(binaryArgs); + const to = argsObj.nextString().expect('to argument is missing or invalid'); + const amount = argsObj + .nextU64() + .expect('amount argument is missing or invalid'); + transferCoins(new Address(to), amount); +} + +/** + * Get the tokenId of the domain + * @param binaryArgs - (domain: string) + * @returns tokenId of the domain as u256 + */ +export function getTokenIdFromDomain( + binaryArgs: StaticArray, +): StaticArray { + const args = new Args(binaryArgs); + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + if (!Storage.has(domainToTokenIdKey(domain))) { + throw new Error('Domain not found'); + } + return Storage.get(domainToTokenIdKey(domain)); +} + +/** + * Get the domain from the tokenId + * @param binaryArgs - (tokenId: u256) + * @returns domain of the tokenId + */ +export function getDomainFromTokenId( + binaryArgs: StaticArray, +): StaticArray { + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('tokenId argument is missing or invalid'); + return Storage.get(tokenIdToDomainKey(tokenId)); +} + +/** + * Get the owner of the token + * @param binaryArgs - (tokenId: u256) + * @returns Address of the owner of the token + */ +export function ownerOf(binaryArgs: StaticArray): StaticArray { + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('tokenId argument is missing or invalid'); + const owner = _ownerOf(tokenId); + if (owner == '') { + throw new Error('Token id not found'); + } + return stringToBytes(owner); +} + +export function transferFrom(binaryArgs: StaticArray): void { + assert(!Storage.has(lockedKey()), 'Contract is locked'); + _transferFrom(binaryArgs); +} + +export { + setOwner, + ownerAddress, +} from '@massalabs/sc-standards/assembly/contracts/utils/ownership'; + +export { + isApprovedForAll, + setApprovalForAll, + getApproved, + approve, + balanceOf, + symbol, + name, + totalSupply, +} from '@massalabs/sc-standards/assembly/contracts/MRC721/enumerable/MRC721Enumerable'; + +// we iterate over all the targetToDomains keys and migrate them to the new format. +// The tricky part is that "getKeys" will also return already migrated keys. + +export function migrate(binaryArgs: StaticArray): void { + _onlyOwner(); + + const BATCH_SIZE = new Args(binaryArgs) + .nextI32() + .expect('batch size is missing or invalid'); + + let processedKeys = 0; + let keyIndex = 0; + + const targetToDomainsPrefix = DOMAIN_SEPARATOR_KEY.concat(ADDRESS_KEY_PREFIX); + const prefixLength = targetToDomainsPrefix.length; + const targetToDomainsKeys = Storage.getKeys(targetToDomainsPrefix); + const totalKeys = targetToDomainsKeys.length; + + generateEvent( + 'start migration. nb total target keys: ' + totalKeys.toString(), + ); + + while (processedKeys < BATCH_SIZE) { + if (keyIndex == targetToDomainsKeys.length) { + generateEvent('Migration done.'); + break; + } + const key = targetToDomainsKeys[keyIndex]; + keyIndex++; + + if (!Storage.has(key)) { + generateEvent('Should not happen, key not found'); + continue; + } + const domainsBytes = Storage.get(key); + + if (domainsBytes.length == 0) { + // already migrated key + continue; + } + + const target = key.slice(prefixLength); + + let domainStart = 0; + for (let idx = 0; idx < domainsBytes.length; idx++) { + let lastDomainOffset = 0; + if (idx == domainsBytes.length - 1) { + // if last byte is not a coma, migrate the key + lastDomainOffset = 1; + } else { + // migrate key only if coma is found + if (domainsBytes[idx] != 44) { + continue; + } + } + + const domainBytes = StaticArray.fromArray( + domainsBytes.slice(domainStart, idx + lastDomainOffset), + ); + const newKey = targetToDomainsPrefix + .concat(StaticArray.fromArray(target)) + .concat(domainBytes); + + Storage.set(newKey, []); + domainStart = idx + 1; + } + // delete old key + Storage.del(key); + processedKeys++; + } + generateEvent('done. processed keys:' + processedKeys.toString()); +} diff --git a/smart-contract/assembly/contracts/main_prev.ts b/smart-contract/assembly/contracts/main_prev.ts new file mode 100644 index 0000000..0c7ab27 --- /dev/null +++ b/smart-contract/assembly/contracts/main_prev.ts @@ -0,0 +1,624 @@ +import { + Address, + Context, + Storage, + balance, + generateEvent, + getKeysOf, + setBytecode, + transferCoins, +} from '@massalabs/massa-as-sdk'; +import { + Args, + bytesToString, + bytesToU256, + stringToBytes, + u256ToBytes, + u64ToBytes, +} from '@massalabs/as-types'; +import { + _update, + _ownerOf, + TOTAL_SUPPLY_KEY, +} from '@massalabs/sc-standards/assembly/contracts/MRC721/enumerable/MRC721Enumerable-internals'; +import { + transferFrom as _transferFrom, + mrc721Constructor, +} from '@massalabs/sc-standards/assembly/contracts/MRC721/enumerable/MRC721Enumerable'; +import { + _onlyOwner, + _isOwner, +} from '@massalabs/sc-standards/assembly/contracts/utils/ownership-internal'; + +import { u256 } from 'as-bignum/assembly'; + +const TARGET = 'AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep'; + +export function constructor(_: StaticArray): void { + mrc721Constructor('MassaNameService', 'MNS'); + Storage.set(COUNTER_KEY, u256ToBytes(u256.Zero)); + Storage.set(buildLockedKey(), u256ToBytes(u256.Zero)); + Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(u256.Zero)); + + const buildnetContract = + 'AS12qKAVjU1nr66JSkQ6N4Lqu4iwuVc6rAbRTrxFoynPrPdP1sj3G'; + const keys = getKeysOf(buildnetContract); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = Storage.getOf(new Address(buildnetContract), key); + Storage.set(key, value); + } + generateEvent('Buildnet contract cloned '); + + // const NB_DOMAINS = 3000; + // _mintBatch(NB_DOMAINS); +} + +export function mintBatch(_: StaticArray): void { + const NB_DOMAINS = 3000; + _mintBatch(NB_DOMAINS); +} + +export function mintBatchMixedTarget(_: StaticArray): void { + const NB_DOMAINS = 3000; + _mintBatchMixedTarget(NB_DOMAINS); +} + +function _mintBatchMixedTarget(batchSize: i32): void { + const owner = Context.caller().toString(); + + let counter = bytesToU256(Storage.get(COUNTER_KEY)); + let domainCounterI32 = counter.toI32(); + generateEvent('Minting batch from id ' + counter.toI32().toString()); + + const batchEnd = domainCounterI32 + batchSize; + for ( + domainCounterI32 = counter.toI32(); + domainCounterI32 < batchEnd; + domainCounterI32++ + ) { + const domain = 'mixed' + domainCounterI32.toString(); + const target = 'target' + domainCounterI32.toString(); + + const addressKey = buildAddressKey(target); + + const u256Counter = u256.fromI32(domainCounterI32); + // Transfer ownership of the domain to the caller + _update(owner, u256Counter, ''); + + const domainBytes = stringToBytes(domain); + Storage.set(buildDomainKey(u256Counter), domainBytes); + Storage.set(buildTokenIdKey(domain), u256ToBytes(u256Counter)); + + Storage.set(addressKey, domainBytes); + } + generateEvent('Minted to tokenId ' + domainCounterI32.toString()); + Storage.set(COUNTER_KEY, u256ToBytes(u256.fromI32(domainCounterI32))); +} + +function _mintBatch(batchSize: i32): void { + const owner = Context.caller().toString(); + + const addressKey = buildAddressKey(TARGET); + let counter = bytesToU256(Storage.get(COUNTER_KEY)); + let domains: StaticArray = []; + let coma: StaticArray = [44]; + let domainCounterI32 = counter.toI32(); + generateEvent('Minting batch from id ' + counter.toI32().toString()); + + if (Storage.has(addressKey)) { + domains = Storage.get(addressKey); + } + + const batchEnd = domainCounterI32 + batchSize; + for ( + domainCounterI32 = counter.toI32(); + domainCounterI32 < batchEnd; + domainCounterI32++ + ) { + const domain = 'domain' + domainCounterI32.toString(); + + const u256Counter = u256.fromI32(domainCounterI32); + // Transfer ownership of the domain to the caller + _update(owner, u256Counter, ''); + + const domainBytes = stringToBytes(domain); + Storage.set(buildDomainKey(u256Counter), domainBytes); + Storage.set(buildTokenIdKey(domain), u256ToBytes(u256Counter)); + + if (domainCounterI32 != counter.toI32()) { + domains = domains.concat(coma); + } + + domains = domains.concat(domainBytes); + } + generateEvent('Minted to tokenId ' + domainCounterI32.toString()); + Storage.set(addressKey, domains); + Storage.set(COUNTER_KEY, u256ToBytes(u256.fromI32(domainCounterI32))); +} + +export function migrate(binaryArgs: StaticArray): void { + _onlyOwner(); + + const BATCH_SIZE = new Args(binaryArgs) + .nextI32() + .expect('batch size is missing or invalid'); + + let processedKeys = 0; + let keyIndex = 0; + + const targetToDomainsPrefix = DOMAIN_SEPARATOR_KEY.concat(ADDRESS_KEY_PREFIX); + const prefixLength = targetToDomainsPrefix.length; + const targetToDomainsKeys = Storage.getKeys(targetToDomainsPrefix); + const totalKeys = targetToDomainsKeys.length; + + generateEvent( + 'start migration. nb total target keys: ' + totalKeys.toString(), + ); + + while (processedKeys < BATCH_SIZE) { + let nbDomainsTargeted = 0; + + if (keyIndex == targetToDomainsKeys.length) { + generateEvent('Migration done.'); + break; + } + const key = targetToDomainsKeys[keyIndex]; + keyIndex++; + + if (!Storage.has(key)) { + generateEvent('Should not happen, key not found'); + continue; + } + const domainsBytes = Storage.get(key); + + if (domainsBytes.length == 0) { + // already migrated key + continue; + } + + const target = key.slice(prefixLength); + + let domainStart = 0; + // iterate over the domains bytes to extract each domain + for (let idx = 0; idx < domainsBytes.length; idx++) { + let lastDomainOffset = 0; + if (idx == domainsBytes.length - 1) { + // for last byte, migrate the key + lastDomainOffset = 1; + } else { + // migrate key only if coma is found + if (domainsBytes[idx] != 44) { + continue; + } + } + + const domainBytes = StaticArray.fromArray( + domainsBytes.slice(domainStart, idx + lastDomainOffset), + ); + const newKey = targetToDomainsPrefix + .concat(StaticArray.fromArray(target)) + .concat(domainBytes); + + Storage.set(newKey, []); + domainStart = idx + 1; + nbDomainsTargeted++; + } + if (nbDomainsTargeted > 1) { + generateEvent( + 'big boy!!! nbDomainsTargeted: ' + nbDomainsTargeted.toString(), + ); + } + + // delete old key + Storage.del(key); + processedKeys++; + } + generateEvent('done. processed keys:' + processedKeys.toString()); +} + +// DNS RELATED FUNCTIONS + +const DOMAIN_SEPARATOR_KEY: StaticArray = [0x42]; + +const COUNTER_KEY: StaticArray = [0x00]; +const TOKEN_ID_KEY_PREFIX: StaticArray = [0x01]; +const TARGET_KEY_PREFIX: StaticArray = [0x02]; +const DOMAIN_KEY_PREFIX: StaticArray = [0x03]; +const ADDRESS_KEY_PREFIX: StaticArray = [0x04]; +const LOCKED_KEY_PREFIX: StaticArray = [0x05]; + +// Be careful if we edit the values here to increase the price, it requires to change the refund +// logic in dnsFree function to avoid refunding more than the user paid with the old prices. +function calculateCreationCost(sizeDomain: u64): u64 { + if (sizeDomain <= 2) { + return 10_000_000_000_000; + } else if (sizeDomain == 3) { + return 1_000_000_000_000; + } else if (sizeDomain == 4) { + return 100_000_000_000; + } else if (sizeDomain == 5) { + return 10_000_000_000; + } + return 1_000_000_000; +} + +// @ts-ignore (fix for IDE) +@inline +function isNotNumber(c: i32): bool { + const zero = 48; + const nine = 57; + return c < zero || c > nine; +} + +// @ts-ignore (fix for IDE) +@inline +function isNotLowercaseLetter(c: i32): bool { + const a = 97; + const z = 122; + return c < a || c > z; +} + +// @ts-ignore (fix for IDE) +@inline +function isNotHyphen(c: i32): bool { + return c != 45; +} + +export function isValidDomain(domain: string): bool { + if (domain.length < 2 || domain.length > 100) { + return false; + } + for (let i = 0; i < domain.length; i++) { + const c = domain.charCodeAt(i); + // Must be lowercase or hyphen + if (isNotNumber(c) && isNotLowercaseLetter(c) && isNotHyphen(c)) { + return false; + } + } + return true; +} + +function buildTokenIdKey(domain: string): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + TOKEN_ID_KEY_PREFIX.concat(stringToBytes(domain)), + ); +} + +function buildDomainKey(tokenId: u256): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + DOMAIN_KEY_PREFIX.concat(u256ToBytes(tokenId)), + ); +} + +function buildTargetKey(domain: string): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + TARGET_KEY_PREFIX.concat(stringToBytes(domain)), + ); +} + +function buildAddressKey(address: string): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat( + ADDRESS_KEY_PREFIX.concat(stringToBytes(address)), + ); +} + +function buildLockedKey(): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(LOCKED_KEY_PREFIX); +} + +/** + * Lock the contract + */ +export function dnsLock(_: StaticArray): void { + _onlyOwner(); + Storage.set(buildLockedKey(), u256ToBytes(u256.Zero)); +} + +/** + * Unlock the contract + */ +export function dnsUnlock(_: StaticArray): void { + _onlyOwner(); + Storage.del(buildLockedKey()); +} + +/** + * Calculate the cost of the dns allocation + * @param binaryArgs - (domain: string, target: string) + * + * @returns cost of the dns allocation as u64 + */ +export function dnsAllocCost(binaryArgs: StaticArray): StaticArray { + const args = new Args(binaryArgs); + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + assert(isValidDomain(domain), 'Invalid domain'); + return u64ToBytes(calculateCreationCost(domain.length) + 100_000_000); +} + +/** + * Register domain + * @param binaryArgs - (domain: string, target: string) + * @returns tokenId of the dns as u256 + */ +export function dnsAlloc(binaryArgs: StaticArray): StaticArray { + if (Storage.has(buildLockedKey()) && !_isOwner(Context.caller().toString())) { + throw new Error('Domain allocation is locked'); + } + const initialBalance = balance(); + const args = new Args(binaryArgs); + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + const target = args + .nextString() + .expect('target argument is missing or invalid'); + const owner = Context.caller().toString(); + + assert(isValidDomain(domain), 'Invalid domain'); + const targetKey = buildTargetKey(domain); + assert(!Storage.has(targetKey), 'Domain already registered'); + Storage.set(targetKey, stringToBytes(target)); + + assert(Storage.has(COUNTER_KEY), 'Counter not initialized'); + const counter = bytesToU256(Storage.get(COUNTER_KEY)); + // Transfer ownership of the domain to the caller + _update(owner, counter, ''); + + Storage.set(buildDomainKey(counter), stringToBytes(domain)); + Storage.set(buildTokenIdKey(domain), u256ToBytes(counter)); + + let entries: string[] = []; + const addressKey = buildAddressKey(target); + if (Storage.has(addressKey)) { + entries = bytesToString(Storage.get(addressKey)).split(','); + } + entries.push(domain); + Storage.set(addressKey, stringToBytes(entries.join(','))); + + // @ts-ignore (fix for IDE) + Storage.set(COUNTER_KEY, u256ToBytes(counter + u256.One)); + const finalBalance = balance(); + const storageCosts = initialBalance - finalBalance; + const totalCost = calculateCreationCost(domain.length) + storageCosts; + const transferredCoins = Context.transferredCoins(); + assert( + transferredCoins >= totalCost, + 'Insufficient funds to register domain. Provided:' + + transferredCoins.toString() + + '. Needed: ' + + totalCost.toString() + + '.', + ); + if (transferredCoins > totalCost) { + const amountToSend = transferredCoins - totalCost; + transferCoins(Context.caller(), amountToSend); + } + return u256ToBytes(counter); +} + +/** + * Free domain and refund half of the registration fee + * @param binaryArgs - (tokenId: u256) + * @returns void + */ +export function dnsFree(binaryArgs: StaticArray): void { + if (Storage.has(buildLockedKey()) && !_isOwner(Context.caller().toString())) { + throw new Error('Free is locked'); + } + const initialBalance = balance(); + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('tokenId argument is missing or invalid'); + + const domainKey = buildDomainKey(tokenId); + assert(Storage.has(domainKey), 'Domain not registered'); + const owner = _ownerOf(tokenId); + assert(new Address(owner) == Context.caller(), 'Only owner can free domain'); + + const domain = bytesToString(Storage.get(domainKey)); + Storage.del(domainKey); + // Transfer ownership of the domain to empty address + _update('', tokenId, ''); + + let targetKey = buildTargetKey(domain); + let target = bytesToString(Storage.get(targetKey)); + let addressDomains = bytesToString( + Storage.get(buildAddressKey(target)), + ).split(','); + const index = addressDomains.indexOf(domain); + addressDomains.splice(index, 1); + if (addressDomains.length == 0) { + Storage.del(buildAddressKey(target)); + } else { + Storage.set( + buildAddressKey(target), + stringToBytes(addressDomains.join(',')), + ); + } + + Storage.del(targetKey); + Storage.del(buildTokenIdKey(domain)); + const finalBalance = balance(); + const storageCostsRefunded = finalBalance - initialBalance; + const refundTotal = + calculateCreationCost(domain.length) / 2 + + storageCostsRefunded + + Context.transferredCoins(); + transferCoins(Context.caller(), refundTotal); +} + +/** + * Get the target address associated with a domain + * @param args - (domain: string) + * @returns Address target of the domain + */ +export function dnsResolve(args: StaticArray): StaticArray { + const argsObj = new Args(args); + const domain = argsObj + .nextString() + .expect('domain argument is missing or invalid'); + const target = Storage.get(buildTargetKey(domain)); + return target; +} + +/** Get a list of domain associated with an address + * @param args - (address: string) + * + * @returns List of domains as string separated by comma + */ +export function dnsReverseResolve(args: StaticArray): StaticArray { + const argsObj = new Args(args); + const address = argsObj + .nextString() + .expect('address argument is missing or invalid'); + return Storage.get(buildAddressKey(address)); +} + +/** + * Update the target address associated with a domain. Only the owner can update the target. + * @param binaryArgs - (domain: string, newTarget: string) + */ +export function dnsUpdateTarget(binaryArgs: StaticArray): void { + if (Storage.has(buildLockedKey()) && !_isOwner(Context.caller().toString())) { + throw new Error('Update Target is locked'); + } + const argsObj = new Args(binaryArgs); + const domain = argsObj + .nextString() + .expect('domain argument is missing or invalid'); + const newTarget = argsObj + .nextString() + .expect('target argument is missing or invalid'); + + const tokenId = bytesToU256(Storage.get(buildTokenIdKey(domain))); + const owner = _ownerOf(tokenId); + assert( + new Address(owner) == Context.caller(), + 'Only owner can update target', + ); + + const previousTarget = bytesToString(Storage.get(buildTargetKey(domain))); + const addressDomains = bytesToString( + Storage.get(buildAddressKey(previousTarget)), + ).split(','); + const index = addressDomains.indexOf(domain); + addressDomains.splice(index, 1); + if (addressDomains.length == 0) { + Storage.del(buildAddressKey(previousTarget)); + } else { + Storage.set( + buildAddressKey(previousTarget), + stringToBytes(addressDomains.join(',')), + ); + } + + let entries: string[] = []; + const addressKey = buildAddressKey(newTarget); + if (Storage.has(addressKey)) { + entries = bytesToString(Storage.get(addressKey)).split(','); + } + entries.push(domain); + Storage.set(addressKey, stringToBytes(entries.join(','))); + + Storage.set(buildTargetKey(domain), stringToBytes(newTarget)); +} + +/** + * Upgrade the DNS smart contract bytecode + * @param args - new bytecode + * @returns void + */ +export function upgradeSC(args: StaticArray): void { + _onlyOwner(); + setBytecode(args); +} + +/** + * Transfer internal coins to another address + * @param binaryArgs - (to: string, amount: u64) + * @returns void + */ +export function transferInternalCoins(binaryArgs: StaticArray): void { + _onlyOwner(); + const argsObj = new Args(binaryArgs); + const to = argsObj.nextString().expect('to argument is missing or invalid'); + const amount = argsObj + .nextU64() + .expect('amount argument is missing or invalid'); + transferCoins(new Address(to), amount); +} + +/** + * Get the tokenId of the domain + * @param binaryArgs - (domain: string) + * @returns tokenId of the domain as u256 + */ +export function getTokenIdFromDomain( + binaryArgs: StaticArray, +): StaticArray { + const args = new Args(binaryArgs); + const domain = args + .nextString() + .expect('domain argument is missing or invalid'); + if (!Storage.has(buildTokenIdKey(domain))) { + throw new Error('Domain not found'); + } + return Storage.get(buildTokenIdKey(domain)); +} + +/** + * Get the domain from the tokenId + * @param binaryArgs - (tokenId: u256) + * @returns domain of the tokenId + */ +export function getDomainFromTokenId( + binaryArgs: StaticArray, +): StaticArray { + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('tokenId argument is missing or invalid'); + return Storage.get(buildDomainKey(tokenId)); +} + +/** + * Get the owner of the token + * @param binaryArgs - (tokenId: u256) + * @returns Address of the owner of the token + */ +export function ownerOf(binaryArgs: StaticArray): StaticArray { + const args = new Args(binaryArgs); + const tokenId = args + .nextU256() + .expect('tokenId argument is missing or invalid'); + const owner = _ownerOf(tokenId); + if (owner == '') { + throw new Error('Token id not found'); + } + return stringToBytes(owner); +} + +export function transferFrom(binaryArgs: StaticArray): void { + assert(!Storage.has(buildLockedKey()), 'Contract is locked'); + _transferFrom(binaryArgs); +} + +export { + setOwner, + ownerAddress, +} from '@massalabs/sc-standards/assembly/contracts/utils/ownership'; + +export { + isApprovedForAll, + setApprovalForAll, + getApproved, + approve, + balanceOf, + symbol, + name, + totalSupply, +} from '@massalabs/sc-standards/assembly/contracts/MRC721/enumerable/MRC721Enumerable'; diff --git a/smart-contract/src/deploy.ts b/smart-contract/src/deploy.ts index 1bb2127..365fba2 100644 --- a/smart-contract/src/deploy.ts +++ b/smart-contract/src/deploy.ts @@ -1,13 +1,22 @@ -import { Args, Mas, SmartContract } from '@massalabs/massa-web3'; +import { + Args, + Mas, + MAX_GAS_CALL, + Operation, + rpcTypes, + SmartContract, +} from '@massalabs/massa-web3'; import { getScByteCode, initProvider } from './utils'; +import { MNS_CONTRACT } from './config'; -let events; +let events: rpcTypes.OutputEvents; +let op: Operation; const provider = await initProvider(); console.log('Deploying contract...'); -const byteCode = getScByteCode('build', 'main.wasm'); +const byteCode = getScByteCode('build', 'main_prev.wasm'); const name = 'Massa'; const constructorArgs = new Args().addString(name); @@ -16,7 +25,7 @@ const contract = await SmartContract.deploy( provider, byteCode, constructorArgs, - { coins: Mas.fromString('10'), waitFinalExecution: true }, + { coins: Mas.fromString('20'), waitFinalExecution: false }, ); console.log('Contract deployed at:', contract.address); @@ -28,3 +37,121 @@ events = await provider.getEvents({ for (const event of events) { console.log('Event message:', event.data); } + +console.log( + 'Contract initial balance:', + Mas.toString(await provider.client.getBalance(contract.address, false)), +); + +console.log('Mint batch...'); + +// const contract = new SmartContract(provider, MNS_CONTRACT); +op = await contract.call('mintBatch', undefined, { + coins: Mas.fromString('90'), + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getFinalEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} +console.log( + 'Contract balance after mintBatch:', + Mas.toString(await provider.client.getBalance(contract.address)), +); +console.log('Mint batch...'); + +// const contract = new SmartContract(provider, MNS_CONTRACT); +op = await contract.call('mintBatch', undefined, { + coins: Mas.fromString('90'), + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getFinalEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} + +console.log( + 'Contract balance after mintBatch:', + Mas.toString(await provider.client.getBalance(contract.address)), +); +console.log('Mint mixed target batch...'); + +op = await contract.call('mintBatchMixedTarget', undefined, { + coins: Mas.fromString('95'), + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getFinalEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} +console.log( + 'Contract balance after mixed mintBatch:', + Mas.toString(await provider.client.getBalance(contract.address)), +); +console.log('Mint mixed target batch...'); + +op = await contract.call('mintBatchMixedTarget', undefined, { + coins: Mas.fromString('95'), + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getFinalEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} +console.log( + 'Contract balance after mixed mintBatch:', + Mas.toString(await provider.client.getBalance(contract.address)), +); +console.log('Mint mixed target batch...'); + +op = await contract.call('mintBatchMixedTarget', undefined, { + coins: Mas.fromString('95'), + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getFinalEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} + +console.log('Mint mixed target batch...'); + +op = await contract.call('mintBatchMixedTarget', undefined, { + coins: Mas.fromString('95'), + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getSpeculativeEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} + +// let batchSize = 3000; + +// while (true) { +// op = await contract.call('migrate', new Args().addI32(BigInt(batchSize)), { +// coins: Mas.fromString('20'), +// fee: Mas.fromString('0.1'), +// }); + +// events = await op.getFinalEvents(); + +// for (const event of events) { +// console.log('migrate Events:', event.data); +// } + +// if(events.some(event => event.data.includes('Migration done'))) { +// console.log('Migration done ! yallahh:'); +// break; +// } +// } diff --git a/smart-contract/src/infos.ts b/smart-contract/src/infos.ts index 32f41de..c1d9709 100644 --- a/smart-contract/src/infos.ts +++ b/smart-contract/src/infos.ts @@ -6,6 +6,7 @@ import { initProvider, } from './utils'; import { MNS_CONTRACT } from './config'; +import { MNS } from '@massalabs/massa-web3'; const provider = await initProvider(); @@ -24,3 +25,11 @@ try { } catch (e) { console.log('No total supply found'); } + +// reverse resolve + +const target = 'AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep'; + +const contract = new MNS(provider, MNS_CONTRACT); +const res = await contract.fromAddress(target); +console.log('target domains:', res); diff --git a/smart-contract/src/migrate.ts b/smart-contract/src/migrate.ts index 80ca083..e3d55c8 100644 --- a/smart-contract/src/migrate.ts +++ b/smart-contract/src/migrate.ts @@ -1,10 +1,11 @@ -import { SmartContract, Mas, Operation, rpcTypes } from '@massalabs/massa-web3'; import { - getMigrateCounter, - getScByteCode, - getTokenCounter, - initProvider, -} from './utils'; + SmartContract, + Mas, + Operation, + rpcTypes, + Args, +} from '@massalabs/massa-web3'; +import { getScByteCode, initProvider } from './utils'; import { MNS_CONTRACT } from './config'; const provider = await initProvider(); @@ -19,6 +20,7 @@ console.log( let op: Operation; let events: rpcTypes.OutputEvents; + op = await contract.call('upgradeSC', byteCode, { coins: Mas.fromString('3'), fee: Mas.fromString('0.1'), @@ -30,18 +32,11 @@ for (const event of events) { } console.log('upgradeSC done ! operation:', op.id); -const counter = await getTokenCounter(provider, MNS_CONTRACT); -let migrateCount = 0n; -try { - migrateCount = await getMigrateCounter(provider, MNS_CONTRACT); -} catch (e) { - console.log('no migrate counter found'); -} - -while (migrateCount < counter) { - console.log('migrating batch from tokenID', migrateCount.toString()); - op = await contract.call('migrate', undefined, { - coins: Mas.fromString('30'), +let batchSize = 3000; +let success = false; +while (true) { + op = await contract.call('migrate', new Args().addI32(BigInt(batchSize)), { + coins: Mas.fromString('20'), fee: Mas.fromString('0.1'), }); @@ -50,13 +45,15 @@ while (migrateCount < counter) { for (const event of events) { console.log('migrate Events:', event.data); } - migrateCount = await getMigrateCounter(provider, MNS_CONTRACT); - console.log('new migrate count:', migrateCount.toString()); -} -console.log('Upgrade done ! operation:'); + if (events.some((event) => event.data.includes('Migration done'))) { + console.log('Migration done ! yallahh:'); + success = true; + break; + } +} -if (migrateCount === counter) { +if (success) { const cleaned = getScByteCode('build', 'main.wasm'); op = await contract.call('upgradeSC', cleaned, {