diff --git a/cli/package-lock.json b/cli/package-lock.json index cd95209..48e076f 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@commander-js/extra-typings": "^12.1.0", "@listr2/prompt-adapter-enquirer": "^2.0.11", - "@massalabs/massa-web3": "^5.0.1-dev", + "@massalabs/massa-web3": "5.0.1-dev.20241212140726", "commander": "^12.1.0", "enquirer": "^2.4.1", "js-sha256": "^0.11.0", diff --git a/cli/package.json b/cli/package.json index 77589dc..6973ff7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -28,7 +28,7 @@ "dependencies": { "@commander-js/extra-typings": "^12.1.0", "@listr2/prompt-adapter-enquirer": "^2.0.11", - "@massalabs/massa-web3": "^5.0.1-dev", + "@massalabs/massa-web3": "5.0.1-dev.20241212140726", "commander": "^12.1.0", "enquirer": "^2.4.1", "js-sha256": "^0.11.0", diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts index d2a1b98..cd021b5 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -22,6 +22,7 @@ export const uploadCommand = new Command('upload') .option('-a, --address
', 'Address of the website to edit') .option('-s, --chunkSize ', 'Chunk size in bytes') .option('-y, --yes', 'Skip confirmation prompt', false) + .option('--noIndex', 'Skip DeWeb index update', false) .action(async (websiteDirPath, options, command) => { const globalOptions = command.optsWithGlobals() @@ -36,7 +37,8 @@ export const uploadCommand = new Command('upload') provider, chunkSize, websiteDirPath, - options.yes + options.yes, + options.noIndex ) if (options.address) { @@ -76,7 +78,8 @@ async function createUploadCtx( provider: Web3Provider, chunkSize: number, websiteDirPath: string, - skipConfirm: boolean + skipConfirm: boolean, + noIndex: boolean ): Promise { return { provider: provider, @@ -90,6 +93,7 @@ async function createUploadCtx( chunkSize: chunkSize, websiteDirPath: websiteDirPath, skipConfirm: skipConfirm, + noIndex: noIndex, currentTotalEstimation: 0n, maxConcurrentOps: 4, minimalFees: await provider.client.getMinimalFee(), diff --git a/cli/src/lib/index/const.ts b/cli/src/lib/index/const.ts new file mode 100644 index 0000000..04b5f33 --- /dev/null +++ b/cli/src/lib/index/const.ts @@ -0,0 +1,7 @@ +export const BUILDNET_INDEX_ADDRESS = + 'AS1TmA4GNpSYBseNNMXpbAp2trUwZxZy3T1sZ9Qd3Qdn9L8wGbMS' + +// TODO: Replace with mainnet address when available +export const MAINNET_INDEX_ADDRESS = '' + +export const updateWebsiteFunctionName = 'updateWebsite' diff --git a/cli/src/lib/index/index.ts b/cli/src/lib/index/index.ts new file mode 100644 index 0000000..df9aece --- /dev/null +++ b/cli/src/lib/index/index.ts @@ -0,0 +1,72 @@ +import { + Args, + Mas, + Operation, + Provider, + SmartContract, +} from '@massalabs/massa-web3' + +import { storageCostForEntry } from '../utils/storage' +import { updateWebsiteFunctionName } from './const' +import { addressToOwnerBaseKey, indexByOwnerBaseKey } from './keys' +import { getWebsiteOwner } from './read' +import { getOwnerFromWebsiteSC, getSCAddress } from './utils' + +/** + * Get the owner of a website using its 'OWNER' storage key + * @param provider - The provider instance + * @param address - The address of the website + */ +export async function updateWebsite( + provider: Provider, + address: string +): Promise { + const args = new Args().addString(address) + + const scAddress = getSCAddress((await provider.networkInfos()).chainId) + const sc = new SmartContract(provider, scAddress) + + const estimatedCost = await estimateCost(sc, address) + + return sc.call(updateWebsiteFunctionName, args, { + coins: estimatedCost, + }) +} + +/** + * Estimate the cost in coins to update a website. + * @param sc - The smart contract instance + */ +async function estimateCost( + sc: SmartContract, + address: string +): Promise { + return getWebsiteOwner(sc.provider, address) + .then(async (registeredOwner) => { + const scOwner = await getOwnerFromWebsiteSC(sc, address) + + return storageCostForEntry( + BigInt(Math.abs(scOwner.length - registeredOwner.length)), + 0n + ) + }) + .catch(async () => { + // The website does not exist in the index, we have to create it + const owner = await getOwnerFromWebsiteSC(sc, address) + const addressToOwnerPrefix = addressToOwnerBaseKey(address) + const indexByOwnerPrefix = indexByOwnerBaseKey(owner) + + const addressToOwnerKeyCost = storageCostForEntry( + BigInt(addressToOwnerPrefix.length) + BigInt(owner.length), + 0n + ) + const indexByOwnerKeyCost = storageCostForEntry( + BigInt(indexByOwnerPrefix.length) + BigInt(address.length), + 0n + ) + + const totalCost = addressToOwnerKeyCost + indexByOwnerKeyCost + + return totalCost + }) +} diff --git a/cli/src/lib/index/keys.ts b/cli/src/lib/index/keys.ts new file mode 100644 index 0000000..fe91fa6 --- /dev/null +++ b/cli/src/lib/index/keys.ts @@ -0,0 +1,41 @@ +import { I32, strToBytes } from '@massalabs/massa-web3' + +/** + * Returns the base key for the owner's address. + * @param address - The website address + * @returns The base key for the owner's address + */ +export function addressToOwnerBaseKey(address: string): Uint8Array { + const prefix = strToBytes('\x01') + const lengthBytes = I32.toBytes(BigInt(address.length)) + const addressBytes = strToBytes(address) + + const result = new Uint8Array( + prefix.length + lengthBytes.length + addressBytes.length + ) + result.set(prefix, 0) + result.set(lengthBytes, prefix.length) + result.set(addressBytes, prefix.length + lengthBytes.length) + + return result +} + +/** + * Returns the base key for the owner's list of websites. + * @param owner - The owner's address + * @returns The base key for the owner's list of websites + */ +export function indexByOwnerBaseKey(owner: string): Uint8Array { + const prefix = strToBytes('\x00') + const lengthBytes = I32.toBytes(BigInt(owner.length)) + const ownerBytes = strToBytes(owner) + + const result = new Uint8Array( + prefix.length + lengthBytes.length + ownerBytes.length + ) + result.set(prefix, 0) + result.set(lengthBytes, prefix.length) + result.set(ownerBytes, prefix.length + lengthBytes.length) + + return result +} diff --git a/cli/src/lib/index/read.ts b/cli/src/lib/index/read.ts new file mode 100644 index 0000000..6461f08 --- /dev/null +++ b/cli/src/lib/index/read.ts @@ -0,0 +1,27 @@ +import { bytesToStr, Provider } from '@massalabs/massa-web3' + +import { addressToOwnerBaseKey } from './keys' +import { getSCAddress } from './utils' + +/** + * Get the owner of a website according to the index smart contract. + * @param sc - The smart contract instance + * @param address - The address of the website + * @returns The owner of the website + */ +export async function getWebsiteOwner( + provider: Provider, + address: string +): Promise { + const scAddress = getSCAddress((await provider.networkInfos()).chainId) + const prefix = addressToOwnerBaseKey(address) + + const keys = await provider.getStorageKeys(scAddress, prefix) + if (keys.length === 0) { + return '' + } + + const ownerKey = keys[0] + const ownerKeySliced = ownerKey.slice(prefix.length) + return bytesToStr(ownerKeySliced) +} diff --git a/cli/src/lib/index/utils.ts b/cli/src/lib/index/utils.ts new file mode 100644 index 0000000..3ae3404 --- /dev/null +++ b/cli/src/lib/index/utils.ts @@ -0,0 +1,37 @@ +import { bytesToStr, CHAIN_ID, SmartContract } from '@massalabs/massa-web3' + +import { BUILDNET_INDEX_ADDRESS } from './const' + +/** + * Get the owner of a website using its 'OWNER' storage key + * @param sc - The smart contract instance + * @param address - The address of the website + * @returns The owner of the website + */ +export async function getOwnerFromWebsiteSC( + sc: SmartContract, + address: string +): Promise { + const ownerAddress = await sc.provider.readStorage(address, ['OWNER'], true) + if (ownerAddress.length === 0) { + throw new Error(`Could not find owner for website ${address}`) + } + + return bytesToStr(ownerAddress[0]) +} + +/** + * Get the index smart contract address for a given chain id + * @param chainId - The chain id of the network to get the index smart contract address for + * @returns The index smart contract address + */ +export function getSCAddress(chainId: bigint): string { + switch (chainId) { + case CHAIN_ID.Mainnet: + throw new Error('Mainnet is not supported yet') + case CHAIN_ID.Buildnet: + return BUILDNET_INDEX_ADDRESS + default: + throw new Error('Unsupported network') + } +} diff --git a/cli/src/tasks/deploy.ts b/cli/src/tasks/deploy.ts index 50cc623..4041298 100644 --- a/cli/src/tasks/deploy.ts +++ b/cli/src/tasks/deploy.ts @@ -2,6 +2,7 @@ import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer' import { formatMas } from '@massalabs/massa-web3' import { ListrTask } from 'listr2' +import { updateWebsite } from '../lib/index' import { deployCost, deploySC } from '../lib/website/deploySC' import { UploadCtx } from './tasks' @@ -59,6 +60,23 @@ export function deploySCTask(): ListrTask { persistentOutput: true, }, }, + { + title: 'Update DeWeb Index', + task: async (ctx, subTask) => { + if (ctx.noIndex) { + subTask.skip('Skipping DeWeb Index update') + return + } + + subTask.output = + 'Updating the DeWeb Index with the new SC address' + await updateWebsite(provider, ctx.sc.address) + }, + rendererOptions: { + outputBar: Infinity, + persistentOutput: true, + }, + }, ], { concurrent: false, diff --git a/cli/src/tasks/tasks.ts b/cli/src/tasks/tasks.ts index 4c5c190..d839ad8 100644 --- a/cli/src/tasks/tasks.ts +++ b/cli/src/tasks/tasks.ts @@ -11,6 +11,7 @@ export interface UploadCtx { provider: Provider sc?: SmartContract + noIndex: boolean skipConfirm: boolean websiteDirPath: string currentTotalEstimation: bigint