diff --git a/smart-contracts/assembly/contracts/NFT/NFT.ts b/smart-contracts/assembly/contracts/NFT/NFT.ts index 444e723..5a42d4b 100644 --- a/smart-contracts/assembly/contracts/NFT/NFT.ts +++ b/smart-contracts/assembly/contracts/NFT/NFT.ts @@ -10,7 +10,6 @@ import { stringToBytes, bytesToU256, u256ToBytes, - boolToByte, u32ToBytes, } from '@massalabs/as-types'; import { u256 } from 'as-bignum/assembly'; diff --git a/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts b/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts index 827869f..952ee62 100644 --- a/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts +++ b/smart-contracts/assembly/contracts/NFT/__tests__/NFT.spec.ts @@ -35,8 +35,6 @@ import { u256 } from 'as-bignum/assembly'; const callerAddress = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'; -const userAddress = 'A12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8'; - const NFTName = 'MASSA_NFT'; const NFTSymbol = 'NFT'; const NFTBaseURI = 'my.massa/'; diff --git a/smart-contracts/assembly/contracts/utils/__tests__/ownership.spec.ts b/smart-contracts/assembly/contracts/utils/__tests__/ownership.spec.ts index f474fda..4c846fc 100644 --- a/smart-contracts/assembly/contracts/utils/__tests__/ownership.spec.ts +++ b/smart-contracts/assembly/contracts/utils/__tests__/ownership.spec.ts @@ -1,14 +1,9 @@ import { Args, boolToByte, stringToBytes } from '@massalabs/as-types'; import { Storage, changeCallStack } from '@massalabs/massa-as-sdk'; -import { - OWNER_KEY, - isOwner, - onlyOwner, - ownerAddress, - setOwner, -} from '../ownership'; +import { isOwner, onlyOwner, ownerAddress, setOwner } from '../ownership'; import { resetStorage } from '@massalabs/massa-as-sdk'; +import { OWNER_KEY } from '../ownership-internal'; // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT'; diff --git a/smart-contracts/assembly/contracts/utils/accessControl.ts b/smart-contracts/assembly/contracts/utils/accessControl.ts index 654aab3..66446d1 100644 --- a/smart-contracts/assembly/contracts/utils/accessControl.ts +++ b/smart-contracts/assembly/contracts/utils/accessControl.ts @@ -5,8 +5,9 @@ import { Context, } from '@massalabs/massa-as-sdk'; import { Args, boolToByte } from '@massalabs/as-types'; -import { _isOwner, onlyOwner } from './ownership'; +import { onlyOwner } from './ownership'; import { _hasRole, _members, _roleKey } from './accessControl-internal'; +import { _isOwner } from './ownership-internal'; export const ROLES_KEY = '_ROLES'; diff --git a/smart-contracts/assembly/contracts/utils/ownership-internal.ts b/smart-contracts/assembly/contracts/utils/ownership-internal.ts new file mode 100644 index 0000000..4e19a97 --- /dev/null +++ b/smart-contracts/assembly/contracts/utils/ownership-internal.ts @@ -0,0 +1,50 @@ +import { + Context, + Storage, + createEvent, + generateEvent, +} from '@massalabs/massa-as-sdk'; + +export const OWNER_KEY = 'OWNER'; + +export const CHANGE_OWNER_EVENT_NAME = 'CHANGE_OWNER'; + +/** + * Sets the contract owner. This function is to be called from a smart contract. + * + * @param newOwner - The address of the new contract owner. + * + * Emits a CHANGE_OWNER event upon successful execution. + */ +export function _setOwner(newOwner: string): void { + if (Storage.has(OWNER_KEY)) { + _onlyOwner(); + } + Storage.set(OWNER_KEY, newOwner); + + generateEvent(createEvent(CHANGE_OWNER_EVENT_NAME, [newOwner])); +} + +/** + * Checks if the given account is the owner of the contract. + * + * @param account - The address of the account to check. + * @returns true if the account is the owner, false otherwise. + */ +export function _isOwner(account: string): bool { + if (!Storage.has(OWNER_KEY)) { + return false; + } + return account === Storage.get(OWNER_KEY); +} + +/** + * Check if the caller is the contract owner. + * + * @throws Will throw an error if the caller is not the owner or if the owner is not set. + */ +export function _onlyOwner(): void { + assert(Storage.has(OWNER_KEY), 'Owner is not set'); + const owner = Storage.get(OWNER_KEY); + assert(Context.caller().toString() === owner, 'Caller is not the owner'); +} diff --git a/smart-contracts/assembly/contracts/utils/ownership.ts b/smart-contracts/assembly/contracts/utils/ownership.ts index 7f6bac8..fd383a3 100644 --- a/smart-contracts/assembly/contracts/utils/ownership.ts +++ b/smart-contracts/assembly/contracts/utils/ownership.ts @@ -1,14 +1,11 @@ -import { - Context, - generateEvent, - Storage, - createEvent, -} from '@massalabs/massa-as-sdk'; +import { Storage } from '@massalabs/massa-as-sdk'; import { Args, boolToByte, stringToBytes } from '@massalabs/as-types'; - -export const OWNER_KEY = 'OWNER'; - -export const CHANGE_OWNER_EVENT_NAME = 'CHANGE_OWNER'; +import { + OWNER_KEY, + _isOwner, + _onlyOwner, + _setOwner, +} from './ownership-internal'; /** * Set the contract owner @@ -16,18 +13,14 @@ export const CHANGE_OWNER_EVENT_NAME = 'CHANGE_OWNER'; * @param binaryArgs - byte string with the following format: * - the address of the new contract owner (address). */ + export function setOwner(binaryArgs: StaticArray): void { const args = new Args(binaryArgs); const newOwner = args .nextString() .expect('newOwnerAddress argument is missing or invalid'); - if (Storage.has(OWNER_KEY)) { - onlyOwner(); - } - Storage.set(OWNER_KEY, newOwner); - - generateEvent(createEvent(CHANGE_OWNER_EVENT_NAME, [newOwner])); + _setOwner(newOwner); } /** @@ -58,20 +51,11 @@ export function isOwner(binaryArgs: StaticArray): StaticArray { return boolToByte(_isOwner(address)); } -export function _isOwner(account: string): bool { - if (!Storage.has(OWNER_KEY)) { - return false; - } - return account === Storage.get(OWNER_KEY); -} - /** * Throws if the caller is not the owner. * * @param address - */ export function onlyOwner(): void { - assert(Storage.has(OWNER_KEY), 'Owner is not set'); - const owner = Storage.get(OWNER_KEY); - assert(Context.caller().toString() === owner, 'Caller is not the owner'); + _onlyOwner(); } diff --git a/smart-contracts/assembly/contracts/website-deployer/__tests__/websiteDeployer.spec.ts b/smart-contracts/assembly/contracts/website-deployer/__tests__/websiteDeployer.spec.ts index 848008d..8bc07e6 100644 --- a/smart-contracts/assembly/contracts/website-deployer/__tests__/websiteDeployer.spec.ts +++ b/smart-contracts/assembly/contracts/website-deployer/__tests__/websiteDeployer.spec.ts @@ -1,30 +1,93 @@ -import { Args } from '@massalabs/as-types'; -import { Storage } from '@massalabs/massa-as-sdk'; +import { Args, i32ToBytes, unwrapStaticArray } from '@massalabs/as-types'; import { + Storage, + resetStorage, + setDeployContext, +} from '@massalabs/massa-as-sdk'; +import { + NB_CHUNKS_KEY, appendBytesToWebsite, - initializeWebsite, - keyExpectedNbChunks, -} from '../websiteDeployer'; + constructor, + deleteWebsite, +} from '../websiteStorer'; +import { isOwner } from '../../utils'; + +const user = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq'; describe('website deployer tests', () => { - test('initializeWebsite', () => { - // execute - const value: u64 = 52; + beforeAll(() => { + resetStorage(); + setDeployContext(user); + + constructor([]); + }); + + test('owner is set', () => { + expect(isOwner(new Args().add(user).serialize())).toBeTruthy(); + }); + + throws('delete fails if website not created', () => { + const chunkId = i32(0); + const data = new Uint8Array(100); + data.fill(0xf); + deleteWebsite(new Args().add(chunkId).add(data).serialize()); + }); + + test('upload a website', () => { + const nbChunks = 20; - initializeWebsite(new Args().add(value).serialize()); - // assert - expect(Storage.get(keyExpectedNbChunks).nextU64().unwrap()).toBe(value); + for (let chunkId: i32 = 0; chunkId < nbChunks; chunkId++) { + const data = new Uint8Array(100); + data.fill(chunkId & 0xf); + const argsAppend = new Args().add(chunkId).add(data).serialize(); + appendBytesToWebsite(argsAppend); + + expect(Storage.get(NB_CHUNKS_KEY)).toStrictEqual(i32ToBytes(chunkId + 1)); + expect(Storage.get(i32ToBytes(chunkId))).toStrictEqual( + unwrapStaticArray(data), + ); + } + + expect(Storage.get(NB_CHUNKS_KEY)).toStrictEqual(i32ToBytes(nbChunks)); + }); + + test('edit a website (upload missed chunks)', () => { + const nbChunks = 15; + for (let chunkId: i32 = 0; chunkId < nbChunks; chunkId++) { + const data = new Uint8Array(100); + data.fill(chunkId & 0xf); + const argsAppend = new Args().add(chunkId).add(data).serialize(); + appendBytesToWebsite(argsAppend); + + expect(Storage.get(i32ToBytes(chunkId))).toStrictEqual( + unwrapStaticArray(data), + ); + } }); - test('append to website', () => { - const chunkId = u64(5); - const want = new Uint8Array(2); - want.set([0, 1]); - const MASSA_WEB_CHUNKS = `massa_web_${chunkId}`; - const MASSA_WEB_CHUNKS_ARR = new Args().add(MASSA_WEB_CHUNKS).serialize(); - const argsAppend = new Args().add(chunkId).add(want).serialize(); - appendBytesToWebsite(argsAppend); - const got = new Args(Storage.get(MASSA_WEB_CHUNKS_ARR)).nextUint8Array(); - expect(got.unwrap()).toStrictEqual(want); + test('delete website', () => { + deleteWebsite([]); + expect(Storage.has(NB_CHUNKS_KEY)).toBeFalsy(); + const nbChunks = 20; + for (let chunkId: i32 = 0; chunkId < nbChunks; chunkId++) { + expect(Storage.has(i32ToBytes(chunkId))).toBeFalsy(); + } + }); + + test('update website', () => { + const nbChunks = 9; + for (let chunkId: i32 = 0; chunkId < nbChunks; chunkId++) { + const data = new Uint8Array(100); + data.fill(chunkId & 0xf); + const argsAppend = new Args().add(chunkId).add(data).serialize(); + appendBytesToWebsite(argsAppend); + + expect(Storage.get(NB_CHUNKS_KEY)).toStrictEqual(i32ToBytes(chunkId + 1)); + expect(Storage.get(i32ToBytes(chunkId))).toStrictEqual( + unwrapStaticArray(data), + ); + } + + expect(Storage.get(NB_CHUNKS_KEY)).toStrictEqual(i32ToBytes(nbChunks)); }); }); diff --git a/smart-contracts/assembly/contracts/website-deployer/main-websiteDeployer.ts b/smart-contracts/assembly/contracts/website-deployer/main-websiteDeployer.ts deleted file mode 100644 index 8eae89a..0000000 --- a/smart-contracts/assembly/contracts/website-deployer/main-websiteDeployer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - createSC, - generateEvent, - fileToByteArray, -} from '@massalabs/massa-as-sdk'; - -/** - * Creates a new smart contract with the websiteDeployer.wasm file content. - * - * @param _ - not used - */ -export function main(_: StaticArray): i32 { - const bytes: StaticArray = fileToByteArray( - './build/websiteDeployer.wasm', - ); - - const websiteDeployer = createSC(bytes); - // this event is important for Thyra you need to have the address after ":" and without spaces - generateEvent( - `Website Deployer is deployed at :${websiteDeployer.toString()}`, - ); - - return 0; -} diff --git a/smart-contracts/assembly/contracts/website-deployer/websiteDeployer.ts b/smart-contracts/assembly/contracts/website-deployer/websiteDeployer.ts index d34e467..be311bc 100644 --- a/smart-contracts/assembly/contracts/website-deployer/websiteDeployer.ts +++ b/smart-contracts/assembly/contracts/website-deployer/websiteDeployer.ts @@ -1,135 +1,33 @@ -/** - * Website deployer implementation by MassaLabs. - * - * This first very simple version of a website deployer. - * - * The idea is to initialize a smart contract address to store a website by chunk. - * - * The address datastore is used to persist all the information in the following way: - * - the expected number of chunks - * The key (total_chunks) and the value (u64) are both encoded using Args. - * - the chunks themselves - * The key ("massa_web_" concatenated with the chunk id) and the value (uint8array) are both encoded using Args. - * - the website metadata - * The key ("Meta") and the values (two timestamps - creation and last updated dates) are also encoded using Args. - */ -import { Storage, Context, generateEvent } from '@massalabs/massa-as-sdk'; -import { Args } from '@massalabs/as-types'; -import { triggerError } from '../utils'; - -const metadataKey = new Args().add('META'); -const ownerKey = 'owner'; - -export const keyExpectedNbChunks = new Args().add('total_chunks'); - -// we do not create constructor function for now -// because it's Thyra that performs an ExecuteSC with the main-WebsiteDeployer wasm file. +import { Args, stringToBytes } from '@massalabs/as-types'; +import { + createSC, + generateEvent, + fileToByteArray, + call, + Context, +} from '@massalabs/massa-as-sdk'; /** - * This function is doing the following: - * - Set the the first chunk of a website - * - Block Upload if not dnsName owner - * - Save the creation timestamp - * - * @param binaryArgs - expected number of chunks as a u64 binary encoded using Args + * Creates a new smart contract with the websiteDeployer.wasm file content. * + * @param _ - not used */ -export function initializeWebsite(binaryArgs: StaticArray): void { - const args = new Args(binaryArgs); - const expectedNbChunks = args - .nextU64() - .expect('expectedNbChunks is missing or invalid'); - const expectedNbChunksBytes = new Args().add(expectedNbChunks); - - // we set the ownership of the contract - setOwnership(); +export function main(_: StaticArray): void { + const bytes: StaticArray = fileToByteArray('./build/websiteStorer.wasm'); - // we check the website's owner - if (!checkOwnership()) { - triggerError('Caller not the website Owner'); - } + const websiteAddr = createSC(bytes); - Storage.set(keyExpectedNbChunks, expectedNbChunksBytes); + const StorageCostPerByte = 1_000_000; + const StorageKeyCreation = 10 * StorageCostPerByte; + // this will be updated when charging storage key for actual size + // const StorageKeyCreation = stringToBytes(OWNER_KEY).length * StorageCostPerByte; - // add creation date to metadata - if (!Storage.has(metadataKey)) { - const timestamp = Context.timestamp(); + // cost of storing owner key + const coins = + StorageKeyCreation + + StorageCostPerByte * stringToBytes(Context.caller().toString()).length; - // metadata is composed of: - // - a creation date - // - a last update - const metadataValue = new Args().add(timestamp).add(timestamp); - Storage.set(metadataKey, metadataValue); - } - - generateEvent( - `Website initialized on ${Context.callee().toString()} with Total chunk ${expectedNbChunks}`, - ); -} + call(websiteAddr, 'constructor', new Args(), coins); -/** - * This function is doing the following: - * - Add a chunk to the newly created key massa_web_\{chunkId\}. Chunk and chunkId are sent arguments - * - Block Upload if not dnsName owner - * - Update the update date timestamp - * - * @param binaryArgs - chunk id (u64), content (uint8Array) - * - */ -export function appendBytesToWebsite(binaryArgs: StaticArray): void { - const args = new Args(binaryArgs); - const chunkId = args.nextU64().expect('chunkID is missing or invalid'); - const chunkIdSerialized = new Args().add(`massa_web_${chunkId.toString()}`); - const chunkValue = new Args().add( - args.nextUint8Array().expect('chunkValue is missing or invalid'), - ); - - if (!checkOwnership()) { - triggerError('Caller is not the owner of this website'); - } - if (!Storage.has(metadataKey)) { - triggerError( - 'Web site not initialized. This action can be performed by calling ' + - 'the function initializeWebsite() of this smart contract.', - ); - } - - setLastUpdate(); - - Storage.set(chunkIdSerialized, chunkValue); - generateEvent( - `Website chunk deployed to ${Context.callee().toString()} on key ${chunkId}`, - ); -} - -/** - * Sets the owner of this website. - * Does nothing if the owner already exists. No update possible for the moment. - */ -function setOwnership(): void { - if (!Storage.has(ownerKey)) { - Storage.set(ownerKey, Context.caller().toString()); - } -} - -/** - * Checks if the caller of the contract is the contract owner - * @returns bool - */ -function checkOwnership(): bool { - if (Storage.has(ownerKey)) { - return Storage.get(ownerKey) == Context.caller().toString(); - } - return false; -} - -/** - * Updates the date of the last update field of the website meta data. - */ -function setLastUpdate(): void { - const old = Storage.get(metadataKey); - const current = new Args() - .add(old.nextU64().unwrap()) - .add(Context.timestamp()); - Storage.set(metadataKey, current); + generateEvent(`Contract deployed at address: ${websiteAddr.toString()}`); } diff --git a/smart-contracts/assembly/contracts/website-deployer/websiteStorer.ts b/smart-contracts/assembly/contracts/website-deployer/websiteStorer.ts new file mode 100644 index 0000000..4f20507 --- /dev/null +++ b/smart-contracts/assembly/contracts/website-deployer/websiteStorer.ts @@ -0,0 +1,88 @@ +/** + * Website Storer Smart Contract. + * + * This smart contract is designed to manage the storage and retrieval of website chunks. + * It provides functionalities to append new chunks, and delete the entire website data. + * Only the owner of the contract can perform these operations. + */ + +import { Storage, Context, generateEvent } from '@massalabs/massa-as-sdk'; +import { + Args, + bytesToI32, + i32ToBytes, + stringToBytes, +} from '@massalabs/as-types'; +import { onlyOwner } from '../utils'; +import { _setOwner } from '../utils/ownership-internal'; +import { isDeployingContract } from '@massalabs/massa-as-sdk/assembly/std/context'; + +export const NB_CHUNKS_KEY = stringToBytes('NB_CHUNKS'); + +/** + * Constructor function that initializes the contract owner. + * + * @param _ - Unused parameter. + * + * @throws Will throw an error if the the context is not a SC deployment. + */ +export function constructor(_: StaticArray): void { + assert(isDeployingContract()); + _setOwner(Context.caller().toString()); +} + +/** + * Appends a new chunk to the website data. + * + * @param binaryArgs - Binary arguments containing the chunk number and chunk data. + * + * @throws Will throw an error if the caller is not the owner or if the arguments are invalid. + * + * Emits an event upon successful appending of the chunk. + */ +export function appendBytesToWebsite(binaryArgs: StaticArray): void { + onlyOwner(); + + const args = new Args(binaryArgs); + const chunkNb = args.nextI32().expect('chunk number is missing or invalid'); + const chunkData = args.nextBytes().expect('chunkData is missing or invalid'); + + Storage.set(i32ToBytes(chunkNb), chunkData); + + const totalChunks: i32 = Storage.has(NB_CHUNKS_KEY) + ? bytesToI32(Storage.get(NB_CHUNKS_KEY)) + : 0; + if (chunkNb >= totalChunks) { + Storage.set(NB_CHUNKS_KEY, i32ToBytes(chunkNb + 1)); + } + + generateEvent( + `Website chunk ${chunkNb} deployed to ${Context.callee().toString()}`, + ); +} + +/** + * Deletes all the chunks and metadata related to the website. + * + * @param _ - Unused parameter. + * + * @throws Will throw an error if the caller is not the owner or if the website has not been uploaded yet. + * + * Emits an event upon successful deletion of the website. + */ +export function deleteWebsite(_: StaticArray): void { + onlyOwner(); + + assert(Storage.has(NB_CHUNKS_KEY), 'Website not uploaded yet'); + const nbChunks = bytesToI32(Storage.get(NB_CHUNKS_KEY)); + + for (let i: i32 = 0; i < nbChunks; i++) { + const key = i32ToBytes(i); + if (Storage.has(key)) { + Storage.del(key); + } + } + Storage.del(NB_CHUNKS_KEY); + + generateEvent(`Website ${Context.callee().toString()} deleted successfully`); +} diff --git a/smart-contracts/package-lock.json b/smart-contracts/package-lock.json index 801dc05..2ad3acd 100644 --- a/smart-contracts/package-lock.json +++ b/smart-contracts/package-lock.json @@ -17,7 +17,7 @@ "@massalabs/as-types": "^1.0.1", "@massalabs/eslint-config": "^0.0.9", "@massalabs/massa-as-sdk": "^2.2.1-dev", - "@massalabs/massa-sc-compiler": "^0.0.4-dev", + "@massalabs/massa-sc-compiler": "^0.1.1-dev", "@massalabs/prettier-config-as": "^0.0.2", "@types/node": "^18.11.10", "as-bignum": "^0.2.31", @@ -594,9 +594,9 @@ "dev": true }, "node_modules/@massalabs/massa-sc-compiler": { - "version": "0.0.4-dev.20230227095655", - "resolved": "https://registry.npmjs.org/@massalabs/massa-sc-compiler/-/massa-sc-compiler-0.0.4-dev.20230227095655.tgz", - "integrity": "sha512-IpRsqddwkSPRpjDETa9/cvq+VaRj9H4fvilkqeNKuviTxWV9V9hXtvzZpf1BDOD26FnLoyRCo/ypcRHw1gTjJw==", + "version": "0.1.1-dev.20230904171511", + "resolved": "https://registry.npmjs.org/@massalabs/massa-sc-compiler/-/massa-sc-compiler-0.1.1-dev.20230904171511.tgz", + "integrity": "sha512-2r9MhmiSj6jbF7CA1zOALfru5XVZdFrDR0wO5gnTHtZuUCh1PcGwaRm7W0SxUPXYK2I/EA4HiZsW9BSKmWNg2w==", "dev": true, "dependencies": { "assemblyscript": "^0.25.2", @@ -3924,9 +3924,9 @@ "dev": true }, "@massalabs/massa-sc-compiler": { - "version": "0.0.4-dev.20230227095655", - "resolved": "https://registry.npmjs.org/@massalabs/massa-sc-compiler/-/massa-sc-compiler-0.0.4-dev.20230227095655.tgz", - "integrity": "sha512-IpRsqddwkSPRpjDETa9/cvq+VaRj9H4fvilkqeNKuviTxWV9V9hXtvzZpf1BDOD26FnLoyRCo/ypcRHw1gTjJw==", + "version": "0.1.1-dev.20230904171511", + "resolved": "https://registry.npmjs.org/@massalabs/massa-sc-compiler/-/massa-sc-compiler-0.1.1-dev.20230904171511.tgz", + "integrity": "sha512-2r9MhmiSj6jbF7CA1zOALfru5XVZdFrDR0wO5gnTHtZuUCh1PcGwaRm7W0SxUPXYK2I/EA4HiZsW9BSKmWNg2w==", "dev": true, "requires": { "assemblyscript": "^0.25.2", diff --git a/smart-contracts/package.json b/smart-contracts/package.json index 5ad5c2d..dedc659 100644 --- a/smart-contracts/package.json +++ b/smart-contracts/package.json @@ -22,7 +22,7 @@ "@massalabs/as-types": "^1.0.1", "@massalabs/eslint-config": "^0.0.9", "@massalabs/massa-as-sdk": "^2.2.1-dev", - "@massalabs/massa-sc-compiler": "^0.0.4-dev", + "@massalabs/massa-sc-compiler": "^0.1.1-dev", "@massalabs/prettier-config-as": "^0.0.2", "@types/node": "^18.11.10", "as-bignum": "^0.2.31",