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