From e8a8082afe9e31f771ed1376d5fab84c93339766 Mon Sep 17 00:00:00 2001 From: Peterjah Date: Mon, 30 Dec 2024 13:52:21 +0100 Subject: [PATCH] domains key migration --- smart-contract/.nvmrc | 1 + smart-contract/assembly/contracts/main_mig.ts | 491 +++++++++++++ .../assembly/contracts/main_prev.ts | 543 +++++++++++++++ .../assembly/contracts/main_prev_dev.ts | 655 ++++++++++++++++++ smart-contract/package.json | 1 + smart-contract/src/config.ts | 8 +- smart-contract/src/deploy.ts | 61 +- smart-contract/src/infos.ts | 13 + smart-contract/src/migrate-step2.ts | 64 ++ smart-contract/src/migrate.ts | 59 +- smart-contract/src/utils.ts | 31 + 11 files changed, 1886 insertions(+), 41 deletions(-) create mode 100644 smart-contract/.nvmrc create mode 100644 smart-contract/assembly/contracts/main_mig.ts create mode 100644 smart-contract/assembly/contracts/main_prev.ts create mode 100644 smart-contract/assembly/contracts/main_prev_dev.ts create mode 100644 smart-contract/src/migrate-step2.ts 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_mig.ts b/smart-contract/assembly/contracts/main_mig.ts new file mode 100644 index 0000000..4fcff2a --- /dev/null +++ b/smart-contract/assembly/contracts/main_mig.ts @@ -0,0 +1,491 @@ +import { + Address, + Context, + Storage, + balance, + generateEvent, + getKeys, + setBytecode, + transferCoins, +} from '@massalabs/massa-as-sdk'; +import { + Args, + 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]; +const ADDRESS_KEY_PREFIX_V2: StaticArray = [0x06]; + +// 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; +} + +export function isValidTarget(targetBytes: StaticArray): bool { + return targetBytes.length <= 150; +} + +function domainToTokenIdKey(domainBytes: StaticArray): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(TOKEN_ID_KEY_PREFIX.concat(domainBytes)); +} + +function tokenIdToDomainKey(tokenIdBytes: StaticArray): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(DOMAIN_KEY_PREFIX.concat(tokenIdBytes)); +} + +function domainToTargetKey(domainBytes: StaticArray): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(TARGET_KEY_PREFIX.concat(domainBytes)); +} + +function targetToDomainKeyPrefix( + targetBytes: StaticArray, +): StaticArray { + const lenPrefix = StaticArray.fromArray([targetBytes.length]); + return DOMAIN_SEPARATOR_KEY.concat( + ADDRESS_KEY_PREFIX_V2.concat(lenPrefix).concat(targetBytes), + ); +} + +function targetToDomainKey( + targetBytes: StaticArray, + domainBytes: StaticArray, +): StaticArray { + return targetToDomainKeyPrefix(targetBytes).concat(domainBytes); +} + +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(); + + const targetBytes = stringToBytes(target); + const domainBytes = stringToBytes(domain); + + assert(isValidDomain(domain), 'Invalid domain'); + assert(isValidTarget(targetBytes), 'Invalid target'); + + const domainToTargetKay = domainToTargetKey(domainBytes); + assert(!Storage.has(domainToTargetKay), 'Domain already registered'); + + const counterBytes = Storage.get(COUNTER_KEY); + + const counter = bytesToU256(counterBytes); + + // Mint the token + _update(owner, counter, ''); + + // Store the domain and token ID + + Storage.set(domainToTargetKay, targetBytes); + Storage.set(targetToDomainKey(targetBytes, domainBytes), []); + Storage.set(tokenIdToDomainKey(counterBytes), domainBytes); + Storage.set(domainToTokenIdKey(domainBytes), counterBytes); + // @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, ''); + + const tokenIdBytes = u256ToBytes(tokenId); + // Retrieve the domain + const idToDomainKey = tokenIdToDomainKey(tokenIdBytes); + assert(Storage.has(idToDomainKey), 'Domain not registered'); + + const domainBytes = Storage.get(idToDomainKey); + + // Retrieve and delete the target + const domainToTargetK = domainToTargetKey(domainBytes); + const targetBytes = Storage.get(domainToTargetK); + + // Delete all associated keys + Storage.del(domainToTargetK); + Storage.del(targetToDomainKey(targetBytes, domainBytes)); + Storage.del(idToDomainKey); + Storage.del(domainToTokenIdKey(domainBytes)); + + const finalBalance = balance(); + const storageCostsRefunded = finalBalance - initialBalance; + + const refundTotal = + calculateCreationCost(domainBytes.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(stringToBytes(domain))); +} + +/** Get a list of domain associated with an address + * @param args - (targetAddress: string) + * + * @returns List of domains as string separated by comma + */ +export function dnsReverseResolve(args: StaticArray): StaticArray { + const argsObj = new Args(args); + const targetAddress = argsObj + .nextString() + .expect('address argument is missing or invalid'); + + const prefix = targetToDomainKeyPrefix(stringToBytes(targetAddress)); + const keys = getKeys(prefix); + + const prefixLength = prefix.length; + let domains: u8[] = []; + + for (let i = 0; i < keys.length; i++) { + const domain = keys[i].slice(prefixLength); + + 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 newTargetBytes = stringToBytes(newTarget); + assert(isValidTarget(newTargetBytes), 'Invalid target'); + + const domainBytes = stringToBytes(domain); + const tokenId = bytesToU256(Storage.get(domainToTokenIdKey(domainBytes))); + const owner = _ownerOf(tokenId); + + assert( + new Address(owner) == Context.caller(), + 'Only owner can update target', + ); + + // remove the old target + const domainToTargetKay = domainToTargetKey(domainBytes); + const oldTarget = Storage.get(domainToTargetKay); + Storage.del(targetToDomainKey(oldTarget, domainBytes)); + // Add the domain to the new target + Storage.set(targetToDomainKey(newTargetBytes, domainBytes), []); + // Update the target for the domain + Storage.set(domainToTargetKay, newTargetBytes); +} + +/** + * 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'); + const domainBytes = stringToBytes(domain); + if (!Storage.has(domainToTokenIdKey(domainBytes))) { + throw new Error('Domain not found'); + } + return Storage.get(domainToTokenIdKey(domainBytes)); +} + +/** + * 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'); + const tokenIdBytes = u256ToBytes(tokenId); + return Storage.get(tokenIdToDomainKey(tokenIdBytes)); +} + +/** + * 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'; + +const MIGRATE_COUNTER_KEY: StaticArray = [0x06]; + +function migrateCounterdKey(): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(MIGRATE_COUNTER_KEY); +} + +export function clean(_binaryArgs: StaticArray): void { + _onlyOwner(); + const initBal = balance(); + const targetToDomainsPrefix = DOMAIN_SEPARATOR_KEY.concat(ADDRESS_KEY_PREFIX); + const targetToDomainsKeys = Storage.getKeys(targetToDomainsPrefix); + + for (let i = 0; i < targetToDomainsKeys.length; i++) { + const key = targetToDomainsKeys[i]; + if (!Storage.has(key)) { + generateEvent('Should not happen, key not found'); + continue; + } + Storage.del(key); + } + Storage.del(migrateCounterdKey()); + const refund = balance() - initBal; + if (refund > 0) { + transferCoins(Context.caller(), refund); + generateEvent('Refunded MAS: ' + refund.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..2d28b7b --- /dev/null +++ b/smart-contract/assembly/contracts/main_prev.ts @@ -0,0 +1,543 @@ +import { + Address, + Context, + Storage, + balance, + generateEvent, + setBytecode, + transferCoins, +} from '@massalabs/massa-as-sdk'; +import { + Args, + bytesToI32, + bytesToString, + bytesToU256, + i32ToBytes, + 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(buildLockedKey(), u256ToBytes(u256.Zero)); + Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(u256.Zero)); +} + +const ADDRESS_KEY_PREFIX_V2: StaticArray = [0x06]; + +function targetToDomainKeyPrefix( + targetBytes: StaticArray, +): StaticArray { + const lenPrefix = StaticArray.fromArray([targetBytes.length]); + return DOMAIN_SEPARATOR_KEY.concat( + ADDRESS_KEY_PREFIX_V2.concat(lenPrefix).concat(targetBytes), + ); +} + +function targetToDomainKey( + targetBytes: StaticArray, + domainBytes: StaticArray, +): StaticArray { + return targetToDomainKeyPrefix(targetBytes).concat(domainBytes); +} + +const MIGRATE_COUNTER_KEY: StaticArray = [0x06]; + +function migrateCounterdKey(): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(MIGRATE_COUNTER_KEY); +} + +export function migrate(binaryArgs: StaticArray): void { + _onlyOwner(); + + const BATCH_SIZE = new Args(binaryArgs) + .nextI32() + .expect('batch size is missing or invalid'); + + let migrateCounter = bytesToI32(Storage.get(migrateCounterdKey())); + + let keyIndex = migrateCounter; + + const targetToDomainsPrefix = DOMAIN_SEPARATOR_KEY.concat(ADDRESS_KEY_PREFIX); + const targetToDomainsKeys = Storage.getKeys(targetToDomainsPrefix); + const totalKeys = targetToDomainsKeys.length; + + generateEvent( + 'start migration. nb total target keys: ' + totalKeys.toString(), + ); + + while (keyIndex < migrateCounter + 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); + const domainByteLen = domainsBytes.length; + + const targetBytes = StaticArray.fromArray( + key.slice(targetToDomainsPrefix.length), + ); + + let domainStart = 0; + for (let idx = 0; idx < domainByteLen; idx++) { + let lastDomainOffset = 0; + if (idx == domainByteLen - 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 = targetToDomainKey(targetBytes, domainBytes); + + Storage.set(newKey, []); + domainStart = idx + 1; + } + } + // update migration counter + Storage.set(migrateCounterdKey(), i32ToBytes(keyIndex)); + generateEvent( + 'done. processed keys:' + (keyIndex - migrateCounter).toString(), + ); +} + +export function initMig(_binaryArgs: StaticArray): void { + _onlyOwner(); + Storage.set(migrateCounterdKey(), i32ToBytes(0)); +} + +// 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/assembly/contracts/main_prev_dev.ts b/smart-contract/assembly/contracts/main_prev_dev.ts new file mode 100644 index 0000000..ef7d4d8 --- /dev/null +++ b/smart-contract/assembly/contracts/main_prev_dev.ts @@ -0,0 +1,655 @@ +import { + Address, + Context, + Storage, + balance, + generateEvent, + getKeysOf, + setBytecode, + transferCoins, +} from '@massalabs/massa-as-sdk'; +import { + Args, + bytesToI32, + bytesToString, + bytesToU256, + i32ToBytes, + 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 = 'AS1ZTEiyBCyVAdpMfdU7br3xxPSj99kNZTxVXhnuYH7DkDF6h9YK'; + +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 { + _onlyOwner(); + const NB_DOMAINS = 2000; + _mintBatch(NB_DOMAINS); +} + +export function mintBatchMixedTarget(_: StaticArray): void { + _onlyOwner(); + + 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(); + let lastTokenId: u256 = counter; + + generateEvent('Minting batch from id ' + counter.toI32().toString()); + + const batchEnd = domainCounterI32 + batchSize; + const batchStart = counter.toI32() + 1; + + for ( + domainCounterI32 = batchStart; + domainCounterI32 <= batchEnd; + domainCounterI32++ + ) { + const domain = 'mixed' + domainCounterI32.toString(); + const target = 'target' + domainCounterI32.toString(); + + const addressKey = buildAddressKey(target); + + lastTokenId = u256.fromI32(domainCounterI32); + // Transfer ownership of the domain to the caller + _update(owner, lastTokenId, ''); + + const domainBytes = stringToBytes(domain); + Storage.set(buildDomainKey(lastTokenId), domainBytes); + Storage.set(buildTokenIdKey(domain), u256ToBytes(lastTokenId)); + + Storage.set(addressKey, domainBytes); + } + generateEvent('Minted to tokenId ' + (domainCounterI32 - 1).toString()); + Storage.set(COUNTER_KEY, u256ToBytes(lastTokenId)); +} + +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 = 0; + + if (Storage.has(addressKey)) { + domains = Storage.get(addressKey); + } + + const batchStart = counter.toI32() + 1; + const batchEnd = batchStart + batchSize; + + generateEvent('Minting batch from id ' + batchStart.toString()); + + let lastTokenId: u256 = counter; + for ( + domainCounterI32 = batchStart; + domainCounterI32 <= batchEnd; + domainCounterI32++ + ) { + const domain = 'y'.repeat(15) + domainCounterI32.toString(); + // const domain = DOMAINS[domainCounterI32 % DOMAINS.length]; + + lastTokenId = u256.fromI32(domainCounterI32); + // Transfer ownership of the domain to the caller + _update(owner, lastTokenId, ''); + + const domainBytes = stringToBytes(domain); + Storage.set(buildDomainKey(lastTokenId), domainBytes); + Storage.set(buildTokenIdKey(domain), u256ToBytes(lastTokenId)); + + // add a coma, except for the first domain + if (domainCounterI32 != batchStart) { + domains = domains.concat(coma); + } + + domains = domains.concat(domainBytes); + } + generateEvent('Minted to tokenId ' + (domainCounterI32 - 1).toString()); + Storage.set(addressKey, domains); + Storage.set(COUNTER_KEY, u256ToBytes(lastTokenId)); +} + +const ADDRESS_KEY_PREFIX_V2: StaticArray = [0x06]; + +function targetToDomainKeyPrefix( + targetBytes: StaticArray, +): StaticArray { + const lenPrefix = StaticArray.fromArray([targetBytes.length]); + return DOMAIN_SEPARATOR_KEY.concat( + ADDRESS_KEY_PREFIX_V2.concat(lenPrefix).concat(targetBytes), + ); +} + +function targetToDomainKey( + targetBytes: StaticArray, + domainBytes: StaticArray, +): StaticArray { + return targetToDomainKeyPrefix(targetBytes).concat(domainBytes); +} + +const MIGRATE_COUNTER_KEY: StaticArray = [0x06]; + +function migrateCounterdKey(): StaticArray { + return DOMAIN_SEPARATOR_KEY.concat(MIGRATE_COUNTER_KEY); +} + +export function migrate(binaryArgs: StaticArray): void { + _onlyOwner(); + + const BATCH_SIZE = new Args(binaryArgs) + .nextI32() + .expect('batch size is missing or invalid'); + + let migrateCounter = bytesToI32(Storage.get(migrateCounterdKey())); + + let keyIndex = migrateCounter; + + const targetToDomainsPrefix = DOMAIN_SEPARATOR_KEY.concat(ADDRESS_KEY_PREFIX); + const targetToDomainsKeys = Storage.getKeys(targetToDomainsPrefix); + const totalKeys = targetToDomainsKeys.length; + + generateEvent( + 'start migration. nb total target keys: ' + totalKeys.toString(), + ); + + while (keyIndex < migrateCounter + 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); + const domainByteLen = domainsBytes.length; + + const targetBytes = StaticArray.fromArray( + key.slice(targetToDomainsPrefix.length), + ); + + let domainStart = 0; + for (let idx = 0; idx < domainByteLen; idx++) { + let lastDomainOffset = 0; + if (idx == domainByteLen - 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 = targetToDomainKey(targetBytes, domainBytes); + + Storage.set(newKey, []); + domainStart = idx + 1; + } + } + // update migration counter + Storage.set(migrateCounterdKey(), i32ToBytes(keyIndex)); + generateEvent( + 'done. processed keys:' + (keyIndex - migrateCounter).toString(), + ); +} + +export function initMig(_binaryArgs: StaticArray): void { + _onlyOwner(); + Storage.set(migrateCounterdKey(), i32ToBytes(0)); +} + +// 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/package.json b/smart-contract/package.json index 9f0d4b2..4054905 100644 --- a/smart-contract/package.json +++ b/smart-contract/package.json @@ -11,6 +11,7 @@ "testDns": "tsx src/test.ts", "info": "tsx src/infos.ts", "migrate": "tsx src/migrate.ts", + "migrate2": "tsx src/migrate-step2.ts", "pause": "tsx src/pause.ts", "unpause": "tsx src/unpause.ts", "prettier": "prettier '**/src/**/*.ts' --check && as-prettier --check assembly", diff --git a/smart-contract/src/config.ts b/smart-contract/src/config.ts index 8f3da17..d9f5a11 100644 --- a/smart-contract/src/config.ts +++ b/smart-contract/src/config.ts @@ -1,6 +1,6 @@ import { MNS_CONTRACTS } from '@massalabs/massa-web3'; -// export const MNS_CONTRACT = -// 'AS12kMSTQKkfeydAHrju2nXa4b4kpZ8J6yeS623LPSqxpepR8cr14'; -export const MNS_CONTRACT = MNS_CONTRACTS.mainnet; -export const IS_MAINNET = true; +export const MNS_CONTRACT = + 'AS12ZnyeRrV98p8CpUq4e9CpamSfThVJ5axNgiNSHBmhJ7LzkYEyU'; +// export const MNS_CONTRACT = MNS_CONTRACTS.mainnet; +export const IS_MAINNET = false; diff --git a/smart-contract/src/deploy.ts b/smart-contract/src/deploy.ts index 1bb2127..c815a17 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_dev.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,47 @@ 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)), +); + +let NB_BATCHS = 6; +while (NB_BATCHS--) { + console.log('Mint batch...', NB_BATCHS); + + op = await contract.call('mintBatch', undefined, { + coins: Mas.fromString('80'), + 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)), + ); +} + +let NB_BATCHS_MIXED = 6; +while (NB_BATCHS_MIXED--) { + 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)), + ); +} diff --git a/smart-contract/src/infos.ts b/smart-contract/src/infos.ts index 32f41de..052edc5 100644 --- a/smart-contract/src/infos.ts +++ b/smart-contract/src/infos.ts @@ -1,5 +1,7 @@ import { getDomains, + getDomainsFromTarget, + getDomainsFromTargetV2, getOwner, getTokenCounter, getTotalSupply, @@ -24,3 +26,14 @@ try { } catch (e) { console.log('No total supply found'); } + +// reverse resolve + +const target = 'AS1ZTEiyBCyVAdpMfdU7br3xxPSj99kNZTxVXhnuYH7DkDF6h9YK'; + +// before migration +const revRes = await getDomainsFromTarget(provider, target); +console.log('target domains:', revRes); +// after migration +const revRes2 = await getDomainsFromTargetV2(provider, target); +console.log('target domains V2 (should be same same):', revRes2); diff --git a/smart-contract/src/migrate-step2.ts b/smart-contract/src/migrate-step2.ts new file mode 100644 index 0000000..de2e779 --- /dev/null +++ b/smart-contract/src/migrate-step2.ts @@ -0,0 +1,64 @@ +import { + SmartContract, + Mas, + Operation, + rpcTypes, + Args, + MAX_GAS_CALL, +} from '@massalabs/massa-web3'; +import { getScByteCode, initProvider } from './utils'; +import { MNS_CONTRACT } from './config'; + +let op: Operation; +let events: rpcTypes.OutputEvents; + +const provider = await initProvider(); +const contract = new SmartContract(provider, MNS_CONTRACT); + +console.log( + 'Contract initial balance:', + Mas.toString(await provider.client.getBalance(MNS_CONTRACT)), +); + +const byteCode = getScByteCode('build', 'main_mig.wasm'); + +op = await contract.call('upgradeSC', byteCode, { + coins: Mas.fromString('3'), + fee: Mas.fromString('0.1'), +}); +events = await op.getFinalEvents(); + +for (const event of events) { + console.log('upgradeSC Events:', event.data); +} +console.log('upgradeSC done ! operation:', op.id); + +console.log('cleaning old keys...'); +op = await contract.call('clean', undefined, { + fee: Mas.fromString('0.1'), + maxGas: MAX_GAS_CALL, +}); + +events = await op.getSpeculativeEvents(); +for (const event of events) { + console.log('Event message:', event.data); +} + +console.log('Upgrading new bytecode...'); + +const cleaned = getScByteCode('build', 'main.wasm'); + +op = await contract.call('upgradeSC', cleaned, { + fee: Mas.fromString('0.1'), +}); +events = await op.getFinalEvents(); + +for (const event of events) { + console.log('upgradeSC Events:', event.data); +} +console.log('upgradeSC done ! operation:', op.id); + +console.log( + 'Contract Final balance:', + Mas.toString(await provider.client.getBalance(MNS_CONTRACT)), +); diff --git a/smart-contract/src/migrate.ts b/smart-contract/src/migrate.ts index 80ca083..240a6be 100644 --- a/smart-contract/src/migrate.ts +++ b/smart-contract/src/migrate.ts @@ -1,15 +1,16 @@ -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(); -const byteCode = getScByteCode('build', 'main_mig.wasm'); +const byteCode = getScByteCode('build', 'main_prev.wasm'); const contract = new SmartContract(provider, MNS_CONTRACT); console.log( @@ -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,22 @@ 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'); +console.log('Initialize migration'); + +op = await contract.call('initMig', byteCode, { + fee: Mas.fromString('0.1'), +}); +events = await op.getFinalEvents(); + +for (const event of events) { + console.log('upgradeSC Events:', event.data); } +console.log('Initialize done ! operation:', op.id); -while (migrateCount < counter) { - console.log('migrating batch from tokenID', migrateCount.toString()); - op = await contract.call('migrate', undefined, { - coins: Mas.fromString('30'), +let batchSize = 3000; +while (true) { + op = await contract.call('migrate', new Args().addI32(BigInt(batchSize)), { + coins: Mas.fromString('25'), fee: Mas.fromString('0.1'), }); @@ -50,24 +56,11 @@ 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 (migrateCount === counter) { - const cleaned = getScByteCode('build', 'main.wasm'); - - op = await contract.call('upgradeSC', cleaned, { - fee: Mas.fromString('0.1'), - }); - events = await op.getFinalEvents(); - for (const event of events) { - console.log('upgradeSC Events:', event.data); + if (events.some((event) => event.data.includes('Migration done'))) { + console.log('Migration done ! yallahh:'); + break; } - console.log('upgradeSC done ! operation:', op.id); } console.log( diff --git a/smart-contract/src/utils.ts b/smart-contract/src/utils.ts index f321654..969f1e6 100644 --- a/smart-contract/src/utils.ts +++ b/smart-contract/src/utils.ts @@ -6,6 +6,7 @@ import { bytesToStr, Provider, SmartContract, + strToBytes, U256, Web3Provider, } from '@massalabs/massa-web3'; @@ -72,3 +73,33 @@ export async function getOwner(provider: Provider): Promise { const { value } = await contract.read('ownerAddress'); return bytesToStr(value); } + +export async function getDomainsFromTarget( + provider: Provider, + target: string, +): Promise { + const ADDRESS_KEY_PREFIX = [0x4]; + const prefix = Uint8Array.from([ + ...DOMAIN_SEPARATOR_KEY, + ...ADDRESS_KEY_PREFIX, + ...strToBytes(target), + ]); + const keys = await provider.getStorageKeys(MNS_CONTRACT, prefix); + return keys.map((key) => bytesToStr(key.slice(prefix.length))); +} + +export async function getDomainsFromTargetV2( + provider: Provider, + target: string, +): Promise { + const ADDRESS_KEY_PREFIX_V2 = [0x6]; + const targetBytes = strToBytes(target); + const prefix = Uint8Array.from([ + ...DOMAIN_SEPARATOR_KEY, + ...ADDRESS_KEY_PREFIX_V2, + targetBytes.length, + ...targetBytes, + ]); + const keys = await provider.getStorageKeys(MNS_CONTRACT, prefix); + return keys.map((key) => bytesToStr(key.slice(prefix.length))); +}