├── smart-contracts
├── index.ts
├── .gitignore
├── assembly
│ ├── contracts
│ │ ├── MRC1155
│ │ │ ├── burnable
│ │ │ │ ├── index.ts
│ │ │ │ └── burn.ts
│ │ │ ├── mintable
│ │ │ │ ├── index.ts
│ │ │ │ └── mint.ts
│ │ │ ├── __tests__
│ │ │ │ ├── as-pect.d.ts
│ │ │ │ ├── metadata.spec.ts
│ │ │ │ ├── mintable.spec.ts
│ │ │ │ └── burnable.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── metadata
│ │ │ │ ├── index.ts
│ │ │ │ ├── metadata.ts
│ │ │ │ └── metadata-internal.ts
│ │ │ ├── README.md
│ │ │ ├── example
│ │ │ │ └── MRC1155.ts
│ │ │ ├── OnMRC1155Received.ts
│ │ │ ├── OnMRC1155ReceivedFail.ts
│ │ │ └── MRC1155.ts
│ │ ├── MRC20
│ │ │ ├── burnable
│ │ │ │ ├── index.ts
│ │ │ │ ├── burn-internal.ts
│ │ │ │ └── burn.ts
│ │ │ ├── mintable
│ │ │ │ ├── index.ts
│ │ │ │ ├── mint.ts
│ │ │ │ └── mint-internal.ts
│ │ │ ├── __tests__
│ │ │ │ ├── as-pect.d.ts
│ │ │ │ ├── wrapper.spec.ts
│ │ │ │ ├── MRC20-mint.spec.ts
│ │ │ │ ├── WMAS.spec.ts
│ │ │ │ ├── MRC20-burn.spec.ts
│ │ │ │ └── MRC20.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── example
│ │ │ │ └── MRC20.ts
│ │ │ ├── IWMAS.ts
│ │ │ ├── MRC20-external.ts
│ │ │ ├── WMAS.ts
│ │ │ ├── MRC20-internals.ts
│ │ │ ├── wrapper.ts
│ │ │ └── MRC20.ts
│ │ ├── utils
│ │ │ ├── index.ts
│ │ │ ├── error.ts
│ │ │ ├── ownership-internal.ts
│ │ │ ├── ownership.ts
│ │ │ ├── accessControl-internal.ts
│ │ │ ├── __tests__
│ │ │ │ ├── ownership.spec.ts
│ │ │ │ └── accessControl.spec.ts
│ │ │ └── accessControl.ts
│ │ ├── dns
│ │ │ ├── __tests__
│ │ │ │ ├── as-pect.d.ts
│ │ │ │ └── dns.spec.ts
│ │ │ └── blacklist.ts
│ │ ├── MRC721
│ │ │ ├── __tests__
│ │ │ │ ├── as-pect.d.ts
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── metadata.spec.ts
│ │ │ │ ├── MRC721-internals.spec.ts
│ │ │ │ ├── MRC721.spec.ts
│ │ │ │ └── MRC721Enumerable-internals.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── metadata
│ │ │ │ ├── index.ts
│ │ │ │ ├── metadata.ts
│ │ │ │ └── metadata-internal.ts
│ │ │ ├── enumerable
│ │ │ │ ├── index.ts
│ │ │ │ ├── MRC721Enumerable.ts
│ │ │ │ └── MRC721Enumerable-internals.ts
│ │ │ ├── examples
│ │ │ │ └── ER721EnumerableMetadata.ts
│ │ │ └── MRC721.ts
│ │ ├── index.ts
│ │ ├── multicall
│ │ │ ├── multicall.ts
│ │ │ ├── serializers
│ │ │ │ └── serializers.ts
│ │ │ └── __tests__
│ │ │ │ └── multicall.spec.ts
│ │ └── deployer
│ │ │ ├── deployer.ts
│ │ │ └── README.md
│ └── tsconfig.json
├── .eslintrc.cjs
├── asconfig.json
├── scripts
│ └── publish-dev.sh
├── tsconfig.json
├── as-pect.asconfig.json
├── as-pect.config.js
├── README.md
└── package.json
├── .github
├── workflows
│ ├── smart-contracts-ci.yml
│ ├── smart-contracts-cd-dev.yml
│ └── smart-contracts-cd.yml
└── ISSUE_TEMPLATE
│ ├── task.md
│ └── bug.md
├── LICENCE
├── CONTRIBUTING.md
├── units.md
├── README.md
└── wallet
└── file-format.md
/smart-contracts/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./assembly/contracts";
--------------------------------------------------------------------------------
/smart-contracts/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | .env
4 | docs
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/burnable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './burn';
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/mintable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mint';
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/burnable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './burn';
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/mintable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mint';
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ownership';
2 | export * from './error';
3 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/dns/__tests__/as-pect.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/__tests__/as-pect.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/__tests__/as-pect.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/index.ts:
--------------------------------------------------------------------------------
1 | export * from './MRC20';
2 | // Internal functions must not be exported
3 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/__tests__/as-pect.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/index.ts:
--------------------------------------------------------------------------------
1 | export * from './MRC721';
2 | // Internal functions must not be exported
3 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/index.ts:
--------------------------------------------------------------------------------
1 | export * from './MRC1155';
2 | // Internal functions must not be exported
3 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/metadata/index.ts:
--------------------------------------------------------------------------------
1 | export * from './metadata';
2 | export * from './metadata-internal';
3 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/metadata/index.ts:
--------------------------------------------------------------------------------
1 | export * from './metadata';
2 | export * from './metadata-internal';
3 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/enumerable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './MRC721Enumerable';
2 | export * from './MRC721Enumerable-internals';
3 |
--------------------------------------------------------------------------------
/smart-contracts/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | '@massalabs',
4 | ],
5 | rules: {
6 | 'new-cap': ['error', { newIsCap: false }],
7 | "camelcase": [2, {"allow": ["_"]}],
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/smart-contracts/asconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "release": {
4 | "sourceMap": true,
5 | "optimizeLevel": 3,
6 | "shrinkLevel": 3,
7 | "converge": true,
8 | "noAssert": false,
9 | "exportRuntime": true,
10 | "bindings": false
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "assemblyscript/std/assembly.json",
3 | "include": [
4 | "**/*.ts"
5 | ],
6 | "typedocOptions": {
7 | "exclude": "assembly/**/__tests__",
8 | "excludePrivate": true,
9 | "excludeProtected": true,
10 | "excludeExternals": true,
11 | "includeVersion": true,
12 | "skipErrorChecking": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/index.ts:
--------------------------------------------------------------------------------
1 | import * as MRC20 from './MRC20';
2 | import * as MRC721 from './MRC721';
3 | import * as MRC721Enumerable from './MRC721/enumerable';
4 | import * as ownership from './utils/ownership';
5 | import * as accessControl from './utils/accessControl';
6 | import * as MRC1155 from './MRC1155';
7 |
8 | export { MRC20, MRC721, MRC721Enumerable, MRC1155, ownership, accessControl };
9 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/error.ts:
--------------------------------------------------------------------------------
1 | import { generateEvent } from '@massalabs/massa-as-sdk';
2 |
3 | const ERROR_PREFIX = 'ERROR';
4 |
5 | /**
6 | * Creates an event prefixing it with an error prefix
7 | * @param reason - the error context
8 | */
9 | export function triggerError(reason: string): void {
10 | generateEvent(`${ERROR_PREFIX} : ${reason}`);
11 | assert(false, reason);
12 | }
13 |
--------------------------------------------------------------------------------
/smart-contracts/scripts/publish-dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | npm version --preid dev --no-git-tag-version --no-commit-hooks prepatch
5 | #Use timestamp as package suffix
6 | TIME=$(date -u +%Y%m%d%H%M%S)
7 | sed -i "/version/s/dev.0/dev.$TIME/g" package.json
8 | PUBLISH_VERSION=$(cat package.json | jq -r '.version')
9 | echo publishing @massalabs/sc-standards@$PUBLISH_VERSION
10 |
11 | npm publish --access public --tag dev
12 |
--------------------------------------------------------------------------------
/smart-contracts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "es2022",
5 | "preserveConstEnums": true,
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "sourceMap": true,
9 | "target": "es2022",
10 | "types": ["node"]
11 | },
12 | "include": ["./*.ts"],
13 | "exclude": ["node_modules"],
14 | "experimentalDecorators": true,
15 | "ts-node": {
16 | "esm": true,
17 | "experimentalSpecifierResolution": "node"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/example/MRC20.ts:
--------------------------------------------------------------------------------
1 | import { u256 } from 'as-bignum/assembly';
2 | import { mrc20Constructor } from '../MRC20';
3 |
4 | export function constructor(): void {
5 | mrc20Constructor('MassaToken', 'MT', 18, u256.fromU64(1010101010));
6 | }
7 |
8 | export {
9 | name,
10 | symbol,
11 | totalSupply,
12 | decimals,
13 | balanceOf,
14 | transfer,
15 | allowance,
16 | increaseAllowance,
17 | decreaseAllowance,
18 | transferFrom,
19 | } from '../MRC20';
20 |
21 | export { setOwner, onlyOwner, isOwner } from '../../utils/ownership';
22 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/dns/blacklist.ts:
--------------------------------------------------------------------------------
1 | import { call, Address } from '@massalabs/massa-as-sdk';
2 | import { Args } from '@massalabs/as-types';
3 |
4 | /**
5 | * Call the DNS to add a website name to blackList Key
6 | * To add a name the caller needs to be the admin
7 | * current admin is : "A1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC"
8 | *
9 | * @param _ -
10 | */
11 | export function main(_: StaticArray): void {
12 | const DNSAddress = new Address(
13 | 'A1rYZSibJj5LuPiHmpgBVcfSBqyZoj8ugYdzV7vvgBzzQSojDCi',
14 | );
15 | call(DNSAddress, 'addWebsiteToBlackList', new Args().add('flappy'), 0);
16 |
17 | return;
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/smart-contracts-ci.yml:
--------------------------------------------------------------------------------
1 | name: smart-contracts CI
2 |
3 | on:
4 | pull_request:
5 | workflow_call:
6 |
7 | defaults:
8 | run:
9 | working-directory: smart-contracts
10 |
11 | jobs:
12 | unit-tests:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 |
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: 18
20 |
21 | - name: Install
22 | run: npm ci
23 |
24 | - name: Code quality
25 | run: npm run fmt:check
26 |
27 | - name: Build
28 | run: npm run build
29 |
30 | - name: Test
31 | run: npm run test
32 |
33 |
34 |
--------------------------------------------------------------------------------
/smart-contracts/as-pect.asconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "targets": {
3 | "coverage": {
4 | "lib": ["./node_modules/@as-covers/assembly/index.ts"],
5 | "transform": ["@as-covers/transform", "@as-pect/transform"]
6 | },
7 | "noCoverage": {
8 | "transform": ["@as-pect/transform"]
9 | }
10 | },
11 | "options": {
12 | "exportMemory": true,
13 | "outFile": "output.wasm",
14 | "textFile": "output.wat",
15 | "bindings": "raw",
16 | "exportStart": "_start",
17 | "exportRuntime": true,
18 | "use": ["RTRACE=1"],
19 | "debug": true,
20 | "exportTable": true
21 | },
22 | "extends": "./asconfig.json",
23 | "entries": ["./node_modules/@as-pect/assembly/assembly/index.ts"]
24 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/task.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Task
3 | about: Create a new task / feature proposal for the team or the community to contribute
4 | title: ''
5 | assignees: ''
6 |
7 | ---
8 |
9 | ## Context
10 |
11 | *Describe / explain why we should do this: motivations, context or other info.
12 |
13 | ## User flow
14 |
15 | *Describe the user flow using user stories so the end result is super clear
16 |
17 | ## How to
18 |
19 | *List the step-by-step to get it do if needed (optional)
20 |
21 | ## Technical details
22 |
23 | *For Dev: give the technical insights so anyone in the team can tackle the tasks
24 |
25 | ## QA testing
26 |
27 | *For Dev: does this task require some QA tests?
28 | *If yes, explain how to validate it*
29 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/IWMAS.ts:
--------------------------------------------------------------------------------
1 | import { Args } from '@massalabs/as-types';
2 | import { Address, call } from '@massalabs/massa-as-sdk';
3 | import { MRC20Wrapper } from './wrapper';
4 | import { u256 } from 'as-bignum/assembly/integer/u256';
5 |
6 | export class IWMAS extends MRC20Wrapper {
7 | init(
8 | name: string = 'Wrapped Massa',
9 | symbol: string = 'WMAS',
10 | decimals: u8 = 9,
11 | supply: u256 = u256.Zero,
12 | ): void {
13 | super.init(name, symbol, decimals, supply);
14 | }
15 |
16 | deposit(value: u64): void {
17 | call(this._origin, 'deposit', new Args(), value);
18 | }
19 |
20 | withdraw(value: u64, to: Address): void {
21 | call(this._origin, 'withdraw', new Args().add(value).add(to), 0);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/metadata/metadata.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This is an extension to the MRC1155 standard.
4 | *
5 | * It allows to store uri for each token id
6 | * It should be used with the internal functions from metadata-internal
7 | *
8 | */
9 |
10 | import { _uri } from './metadata-internal';
11 | import { Args, stringToBytes } from '@massalabs/as-types';
12 |
13 | /**
14 | *
15 | * Get the URI for a token id
16 | *
17 | * @param id - the id of the token
18 | *
19 | * @returns the URI for the token
20 | *
21 | */
22 | export function uri(binaryArgs: StaticArray): StaticArray {
23 | const args = new Args(binaryArgs);
24 | const id = args.nextU256().expect('id argument is missing or invalid');
25 |
26 | return stringToBytes(_uri(id));
27 | }
28 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/mintable/mint.ts:
--------------------------------------------------------------------------------
1 | import { onlyOwner } from '../../utils/ownership';
2 | import { _mint } from './mint-internal';
3 |
4 | /**
5 | * Mintable feature for fungible token.
6 | *
7 | * Re-export this file in your contract entry file to make it available in the contract.
8 | *
9 | * Token mint is restricted to the owner of the contract.
10 | *
11 | */
12 |
13 | /**
14 | * Mint tokens on the recipient address.
15 | * Restricted to the owner of the contract.
16 | *
17 | * @param binaryArgs - `Args` serialized StaticArray containing:
18 | * - the recipient's account (address)
19 | * - the amount of tokens to mint (u256).
20 | */
21 | export function mint(binaryArgs: StaticArray): void {
22 | onlyOwner();
23 | _mint(binaryArgs);
24 | }
25 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/metadata/metadata.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This is an extension to the MRC1155 standard.
4 | *
5 | * It allows to store uri for each token id
6 | * It should be used with the internal functions from metadata-internal
7 | *
8 | */
9 |
10 | import { _uri } from './metadata-internal';
11 | import { Args, stringToBytes } from '@massalabs/as-types';
12 |
13 | /**
14 | *
15 | * Get the URI for a token id
16 | *
17 | * @param id - the id of the token
18 | *
19 | * @returns the URI for the token
20 | *
21 | */
22 | export function uri(binaryArgs: StaticArray): StaticArray {
23 | const args = new Args(binaryArgs);
24 | const id = args.nextU256().expect('id argument is missing or invalid');
25 |
26 | return stringToBytes(_uri(id));
27 | }
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to report a bug and help us to improve this repo
4 | title: ''
5 | labels: 'issue:bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Describe the bug
11 |
12 | *A clear and concise description of what the bug is.*
13 |
14 | ## To Reproduce
15 |
16 | Steps to reproduce the behavior:
17 |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | ## Expected behavior
24 |
25 | *A clear and concise description of what you expected to happen.*
26 |
27 | ## Screenshots
28 |
29 | *If applicable, add screenshots to help explain your problem.*
30 |
31 | ## Version
32 |
33 | *The versions of software, packages, libraries the bug is present in.*
34 |
35 | ## Additional context
36 |
37 | *Add any other context about the problem here.*
38 |
--------------------------------------------------------------------------------
/.github/workflows/smart-contracts-cd-dev.yml:
--------------------------------------------------------------------------------
1 | name: smart-contracts CD dev
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | defaults:
9 | run:
10 | working-directory: smart-contracts
11 |
12 | jobs:
13 | test:
14 | uses: ./.github/workflows/smart-contracts-ci.yml
15 |
16 | publish-dev:
17 | needs: test
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 |
22 | - uses: actions/setup-node@v3
23 | with:
24 | node-version: 18
25 | registry-url: https://registry.npmjs.org
26 |
27 | - name: publish
28 | run: |
29 | npm ci
30 | npm run build
31 | ./scripts/publish-dev.sh
32 | env:
33 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
34 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/MRC20-external.ts:
--------------------------------------------------------------------------------
1 | import { Address, Storage } from '@massalabs/massa-as-sdk';
2 | import { balanceKey } from './MRC20-internals';
3 |
4 | export function getBalanceEntryCost(
5 | tokenAddress: string,
6 | recipient: string,
7 | ): u64 {
8 | let cost = 0;
9 | if (
10 | !Storage.hasOf(
11 | new Address(tokenAddress),
12 | balanceKey(new Address(recipient)),
13 | )
14 | ) {
15 | // baseCost = NEW_LEDGER_ENTRY_COST = STORAGE_BYTE_COST * 4 = 100_000 * 4 = 400_000
16 | cost = 400_000;
17 | // keyCost =
18 | // LEDGER_COST_PER_BYTE * stringToBytes(BALANCE_KEY_PREFIX + receiver).length = 100_000 * (7 + receiver.length)
19 | cost += 100_000 * (7 + recipient.length);
20 | // valCost = LEDGER_COST_PER_BYTE * u256ToBytes(u256.Zero).length = 100_000 * 32 = 3_200_000
21 | cost += 3_200_000;
22 | }
23 |
24 | return cost;
25 | }
26 |
--------------------------------------------------------------------------------
/smart-contracts/as-pect.config.js:
--------------------------------------------------------------------------------
1 | import createMockedABI from '@massalabs/massa-as-sdk/vm-mock';
2 |
3 | export default {
4 | /**
5 | * A set of globs passed to the glob package that qualify typescript files for testing.
6 | */
7 | entries: ['assembly/**/__tests__/**/*.spec.ts'],
8 | /**
9 | * A set of globs passed to the glob package that quality files to be added to each test.
10 | */
11 | include: ['assembly/**/__tests__/**/*.include.ts'],
12 | /**
13 | * A set of regexp that will disclude source files from testing.
14 | */
15 | disclude: [/node_modules/],
16 | /**
17 | * Add your required AssemblyScript imports here.
18 | */
19 | async instantiate(memory, createImports, instantiate, binary) {
20 | return createMockedABI(memory, createImports, instantiate, binary);
21 | },
22 | /** Enable code coverage. */
23 | // coverage: ["assembly/**/*.ts"],
24 | /**
25 | * Specify if the binary wasm file should be written to the file system.
26 | */
27 | outputBinary: false,
28 | };
29 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/__tests__/helpers.ts:
--------------------------------------------------------------------------------
1 | import { bytesToU256 } from '@massalabs/as-types';
2 | import { getKeys } from '@massalabs/massa-as-sdk';
3 | import { u256 } from 'as-bignum/assembly';
4 | import { _getOwnedTokensKeyPrefix } from '../enumerable/MRC721Enumerable-internals';
5 |
6 | const SIZE_OF_U256 = 32;
7 |
8 | /**
9 | * Returns the all the tokens owned by a specific address.
10 | *
11 | * @param owner - The address of the owner.
12 | *
13 | * @returns An array of u256 representing the tokens owned by the address.
14 | *
15 | */
16 | export function getOwnedTokens(owner: string): u256[] {
17 | const tokens: u256[] = [];
18 | const prefix = _getOwnedTokensKeyPrefix(owner);
19 | const keys = getKeys(prefix);
20 |
21 | for (let i = 0; i < keys.length; i++) {
22 | const tokenIdBytesArray = keys[i].slice(keys[i].length - SIZE_OF_U256);
23 | const tokenIdBytes = StaticArray.fromArray(tokenIdBytesArray);
24 | const tokenId = bytesToU256(tokenIdBytes);
25 | tokens.push(tokenId);
26 | }
27 |
28 | return tokens;
29 | }
30 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Massa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/multicall/multicall.ts:
--------------------------------------------------------------------------------
1 | import { Args, i32ToBytes, stringToBytes } from '@massalabs/as-types';
2 | import { getOpData, hasOpKey, env } from '@massalabs/massa-as-sdk';
3 | import { Call, CallResult } from './serializers/serializers';
4 |
5 | export function main(_: StaticArray): StaticArray {
6 | const calls = deserializeCalls();
7 | const results: CallResult[] = [];
8 | for (let i = 0; i < calls.length; i++) {
9 | const call = calls[i];
10 | const res = env.env.call(
11 | call.contract,
12 | call.targetFunc,
13 | call.params,
14 | call.coins,
15 | );
16 | results.push(new CallResult(res));
17 | }
18 | return new Args().addSerializableObjectArray(results).serialize();
19 | }
20 |
21 | const CALL_PREFIX = stringToBytes('C_');
22 |
23 | export function callKey(index: i32): StaticArray {
24 | return CALL_PREFIX.concat(i32ToBytes(index));
25 | }
26 |
27 | function deserializeCalls(): Call[] {
28 | const calls: Call[] = [];
29 |
30 | let i: i32 = 0;
31 | let key = callKey(i);
32 | while (hasOpKey(key)) {
33 | const call = new Call();
34 | call.deserialize(getOpData(key));
35 | calls.push(call);
36 | i++;
37 | key = callKey(i);
38 | }
39 | return calls;
40 | }
41 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/README.md:
--------------------------------------------------------------------------------
1 | # MRC1155
2 |
3 | This repository contains a set of smart contracts to implement the ERC1155 standard on the Massa blockchain.
4 | see [ERC1155 documentation](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC1155)
5 |
6 | On top of that it also includes multiples extensions to the standard to allow for more complex use cases:
7 | - [burnable](./assembly/contracts/burnable.sol)
8 | - [mintable](./assembly/contracts/mintable.sol)
9 | - [metadata](./assembly/contracts/metadata.sol)
10 |
11 | It can be easily merged into massa-standards as this repository contains a set of smart contracts that are fully compatible with the ERC1155 standard with the only common dependencies being ownership contract.
12 |
13 | ## Documentation
14 |
15 | A documentation for each functions internals or externals has been created that can be found just before the functions declarations.
16 |
17 | ## Unit tests
18 |
19 | A big set of unit tests has been written to ensure the correctness of the smart contracts compared to the standard and some security related checks.
20 |
21 | The only missing coverage is for the call of the ERC1155Receiver function which cannot be tested with the current mocked tests environment.
22 |
23 |
--------------------------------------------------------------------------------
/smart-contracts/README.md:
--------------------------------------------------------------------------------
1 | # Massa Smart-contract Standards
2 |
3 | - [fungible token](assembly/contracts/MRC20): implementation of the ERC20 token.
4 | - [non-fungible token](assembly/contracts/MRC721)
5 |
6 | ## Documentation
7 |
8 | Complete documentation of all available functions and objects is here:
9 |
10 | - [`massa-sc-standards documentation`](https://sc-standards.docs.massa.net)
11 |
12 | ## Usage
13 |
14 | ### Install
15 |
16 | ```sh
17 | npm i @massalabs/sc-standards
18 | ```
19 |
20 | ### Example
21 |
22 | ```typescript
23 | import { Args } from '@massalabs/as-types';
24 | import { callerHasWriteAccess } from '@massalabs/massa-as-sdk';
25 | import * as MRC20 from '@massalabs/sc-standards/assembly/contracts/MRC20/index';
26 | export * from '@massalabs/sc-standards/assembly/contracts/MRC20/token';
27 |
28 | /**
29 | * This function is meant to be called only one time: when the contract is deployed.
30 | *
31 | * @param _ - not used
32 | */
33 | export function constructor(_: StaticArray): StaticArray {
34 | // This line is important. It ensures that this function can't be called in the future.
35 | // If you remove this check, someone could call your constructor function and reset your smart contract.
36 | if (!callerHasWriteAccess()) {
37 | return [];
38 | }
39 |
40 | MRC20.constructor(
41 | new Args().add('MY_TOKEN').add('MTK').add(4).add(100000).serialize(),
42 | );
43 |
44 | return [];
45 | }
46 | ```
47 |
--------------------------------------------------------------------------------
/.github/workflows/smart-contracts-cd.yml:
--------------------------------------------------------------------------------
1 | name: smart-contracts CD
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 |
7 | defaults:
8 | run:
9 | working-directory: smart-contracts
10 |
11 | jobs:
12 | test:
13 | uses: ./.github/workflows/smart-contracts-ci.yml
14 |
15 | publish-latest:
16 | needs: test
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v3
20 |
21 | - uses: actions/setup-node@v3
22 | with:
23 | node-version: 18
24 | registry-url: https://registry.npmjs.org
25 |
26 | - name: publish
27 | run: |
28 | npm ci
29 | npm run build
30 | npm publish --access public
31 | env:
32 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
33 |
34 | deploy-typedoc:
35 | needs: test
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v3
39 | - uses: actions/setup-node@v3
40 | with:
41 | node-version: 16
42 | - name: Generate doc
43 | run: |
44 | npm ci
45 | npm run doc
46 | mv docs/documentation/html ../sc-standards
47 | - name: Deploy files
48 | uses: appleboy/scp-action@master
49 | with:
50 | host: ${{ secrets.MASSANET_HOST }}
51 | username: ${{ secrets.MASSANET_USERNAME }}
52 | key: ${{ secrets.MASSANET_SSHKEY }}
53 | source: "./sc-standards"
54 | target: "/var/www/type-doc"
55 | port: 22000
56 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/ownership-internal.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Context,
3 | Storage,
4 | createEvent,
5 | generateEvent,
6 | } from '@massalabs/massa-as-sdk';
7 |
8 | export const OWNER_KEY = 'OWNER';
9 |
10 | export const CHANGE_OWNER_EVENT_NAME = 'CHANGE_OWNER';
11 |
12 | /**
13 | * Sets the contract owner. This function is to be called from a smart contract.
14 | *
15 | * @param newOwner - The address of the new contract owner.
16 | *
17 | * Emits a CHANGE_OWNER event upon successful execution.
18 | */
19 | export function _setOwner(newOwner: string): void {
20 | if (Storage.has(OWNER_KEY)) {
21 | _onlyOwner();
22 | }
23 | Storage.set(OWNER_KEY, newOwner);
24 |
25 | generateEvent(createEvent(CHANGE_OWNER_EVENT_NAME, [newOwner]));
26 | }
27 |
28 | /**
29 | * Checks if the given account is the owner of the contract.
30 | *
31 | * @param account - The address of the account to check.
32 | * @returns true if the account is the owner, false otherwise.
33 | */
34 | export function _isOwner(account: string): bool {
35 | if (!Storage.has(OWNER_KEY)) {
36 | return false;
37 | }
38 | return account === Storage.get(OWNER_KEY);
39 | }
40 |
41 | /**
42 | * Check if the caller is the contract owner.
43 | *
44 | * @throws Will throw an error if the caller is not the owner or if the owner is not set.
45 | */
46 | export function _onlyOwner(): void {
47 | assert(Storage.has(OWNER_KEY), 'Owner is not set');
48 | const owner = Storage.get(OWNER_KEY);
49 | assert(Context.caller().toString() === owner, 'Caller is not the owner');
50 | }
51 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/examples/ER721EnumerableMetadata.ts:
--------------------------------------------------------------------------------
1 | import { Args } from '@massalabs/as-types';
2 | import { _setBaseURI } from '../metadata/metadata-internal';
3 | import { onlyOwner } from '../../utils';
4 | import { isDeployingContract } from '@massalabs/massa-as-sdk';
5 | import { mrc721Constructor } from '../enumerable';
6 |
7 | export function constructor(_binaryArgs: StaticArray): void {
8 | assert(isDeployingContract());
9 | mrc721Constructor('MassaNft', 'MNFT');
10 | _setBaseURI('ipfs://QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd');
11 | }
12 |
13 | /**
14 | * Set the base URI for all token IDs
15 | * @param newBaseUri - the new base URI
16 | *
17 | * @remarks This function is optional and can be removed if you don't need to change the base URI
18 | */
19 | export function setBaseURI(_args: StaticArray): void {
20 | onlyOwner();
21 | const args = new Args(_args);
22 | const newBaseUri = args
23 | .nextString()
24 | .expect('newBaseUri argument is missing or invalid');
25 |
26 | _setBaseURI(newBaseUri);
27 | }
28 |
29 | export {
30 | isApprovedForAll,
31 | setApprovalForAll,
32 | totalSupply,
33 | getApproved,
34 | approve,
35 | transferFrom,
36 | balanceOf,
37 | symbol,
38 | name,
39 | // mint, // Add this line if you want your contract to be able to mint tokens
40 | // burn, // Add this line if you want your contract to be able to burn tokens
41 | } from '../enumerable/MRC721Enumerable';
42 | export { uri } from '../metadata/metadata';
43 | export { setOwner, ownerAddress } from '../../utils/ownership';
44 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/multicall/serializers/serializers.ts:
--------------------------------------------------------------------------------
1 | import { Args, Result, Serializable } from '@massalabs/as-types';
2 |
3 | export class Call implements Serializable {
4 | constructor(
5 | public contract: string = '',
6 | public targetFunc: string = '',
7 | public coins: u64 = 0,
8 | public params: StaticArray = [],
9 | ) {}
10 |
11 | serialize(): StaticArray {
12 | return new Args()
13 | .add(this.contract)
14 | .add(this.targetFunc)
15 | .add(this.coins)
16 | .add(this.params)
17 | .serialize();
18 | }
19 |
20 | deserialize(data: StaticArray, offset: i32 = 0): Result {
21 | const args = new Args(data, offset);
22 |
23 | this.contract = args.nextString().expect("Can't deserialize contract.");
24 | this.targetFunc = args
25 | .nextString()
26 | .expect("Can't deserialize target function.");
27 | this.coins = args.nextU64().expect("Can't deserialize coins.");
28 | this.params = args.nextBytes().expect("Can't deserialize params.");
29 |
30 | return new Result(args.offset);
31 | }
32 | }
33 |
34 | export class CallResult implements Serializable {
35 | constructor(public data: StaticArray = []) {}
36 |
37 | serialize(): StaticArray {
38 | return new Args().add(this.data).serialize();
39 | }
40 |
41 | deserialize(data: StaticArray, offset: i32 = 0): Result {
42 | const args = new Args(data, offset);
43 | this.data = args.nextBytes().expect("Can't deserialize call result.");
44 |
45 | return new Result(args.offset);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/ownership.ts:
--------------------------------------------------------------------------------
1 | import { Storage } from '@massalabs/massa-as-sdk';
2 | import { Args, boolToByte, stringToBytes } from '@massalabs/as-types';
3 | import {
4 | OWNER_KEY,
5 | _isOwner,
6 | _onlyOwner,
7 | _setOwner,
8 | } from './ownership-internal';
9 |
10 | /**
11 | * Set the contract owner
12 | *
13 | * @param binaryArgs - byte string with the following format:
14 | * - the address of the new contract owner (address).
15 | */
16 | export function setOwner(binaryArgs: StaticArray): void {
17 | const args = new Args(binaryArgs);
18 | const newOwner = args
19 | .nextString()
20 | .expect('newOwnerAddress argument is missing or invalid');
21 |
22 | _setOwner(newOwner);
23 | }
24 |
25 | /**
26 | * Returns the contract owner
27 | *
28 | * @returns owner address in bytes
29 | */
30 | export function ownerAddress(_: StaticArray): StaticArray {
31 | if (!Storage.has(OWNER_KEY)) {
32 | return [];
33 | }
34 |
35 | return stringToBytes(Storage.get(OWNER_KEY));
36 | }
37 |
38 | /**
39 | * Returns true if address is the owner of the contract.
40 | *
41 | * @param address -
42 | */
43 | export function isOwner(binaryArgs: StaticArray): StaticArray {
44 | if (!Storage.has(OWNER_KEY)) {
45 | return [0]; // false
46 | }
47 | const address = new Args(binaryArgs)
48 | .nextString()
49 | .expect('address argument is missing or invalid');
50 | return boolToByte(_isOwner(address));
51 | }
52 |
53 | /**
54 | * Throws if the caller is not the owner.
55 | */
56 | export function onlyOwner(): void {
57 | _onlyOwner();
58 | }
59 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/example/MRC1155.ts:
--------------------------------------------------------------------------------
1 | import { Args, stringToBytes } from '@massalabs/as-types';
2 | import { u256 } from 'as-bignum/assembly';
3 | import { mrc1155Constructor } from '../MRC1155';
4 | import * as mint from '../mintable/mint';
5 | import { Context } from '@massalabs/massa-as-sdk';
6 | import { grantRole } from '../../utils/accessControl';
7 |
8 | export function constructor(binaryArgs: StaticArray): void {
9 | const args = new Args(binaryArgs);
10 | const uri = args.nextString().expect('uri argument is missing or invalid');
11 | const ids = args
12 | .nextFixedSizeArray()
13 | .expect('ids argument is missing or invalid');
14 | const amounts = args
15 | .nextFixedSizeArray()
16 | .expect('amounts argument is missing or invalid');
17 |
18 | mrc1155Constructor(uri);
19 |
20 | grantRole(
21 | new Args()
22 | .add(mint.MINTER_ROLE)
23 | .add(Context.caller().toString())
24 | .serialize(),
25 | );
26 | mint.mintBatch(
27 | new Args()
28 | .add(Context.caller().toString())
29 | .add(ids)
30 | .add(amounts)
31 | .add(stringToBytes(''))
32 | .serialize(),
33 | );
34 | }
35 |
36 | export * from '../burnable/burn';
37 | export * from '../mintable/mint';
38 | export * from '../../utils/accessControl';
39 | export * from '../../utils/ownership';
40 | // export everything from the token module except the constructor
41 | export {
42 | uri,
43 | balanceOf,
44 | balanceOfBatch,
45 | setApprovalForAll,
46 | isApprovedForAll,
47 | safeTransferFrom,
48 | safeBatchTransferFrom,
49 | } from '../MRC1155';
50 |
--------------------------------------------------------------------------------
/smart-contracts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@massalabs/sc-standards",
3 | "version": "1.3.0",
4 | "scripts": {
5 | "test": "asp --summary",
6 | "build": "npx massa-as-compile -r",
7 | "as-prettier": "as-prettier --check assembly",
8 | "as-prettier:fix": "as-prettier --write assembly",
9 | "lint": "eslint .",
10 | "lint:fix": "eslint --fix .",
11 | "fmt": "npm run as-prettier:fix && npm run lint:fix",
12 | "fmt:check": "npm run as-prettier && npm run lint",
13 | "doc": "typedoc assembly/contracts/index.ts --name massa-sc-standards --out docs/documentation/html --tsconfig assembly/tsconfig.json"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "ISC",
18 | "devDependencies": {
19 | "@as-pect/cli": "^8.0.1",
20 | "@assemblyscript/loader": "^0.25.2",
21 | "@massalabs/eslint-config": "^0.0.9",
22 | "@massalabs/massa-sc-compiler": "^0.2.0",
23 | "@massalabs/prettier-config-as": "^0.0.2",
24 | "@types/node": "^18.11.10",
25 | "assemblyscript": "^0.27.2",
26 | "dotenv": "^16.0.3",
27 | "prettier": "^2.8.1",
28 | "ts-node": "^10.9.1",
29 | "tslib": "^2.4.0",
30 | "typedoc": "^0.23.24",
31 | "typescript": "^4.8.4"
32 | },
33 | "type": "module",
34 | "prettier": "@massalabs/prettier-config-as",
35 | "files": [
36 | "index.ts",
37 | "assembly"
38 | ],
39 | "dependencies": {
40 | "@massalabs/as-types": "^2.1.0",
41 | "@massalabs/massa-as-sdk": "^3.0.2",
42 | "as-bignum": "^0.3.1"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/burnable/burn-internal.ts:
--------------------------------------------------------------------------------
1 | import { Address, Storage } from '@massalabs/massa-as-sdk';
2 | import { _balance, _setBalance } from '../MRC20-internals';
3 | import { u256 } from 'as-bignum/assembly';
4 | import { bytesToU256, u256ToBytes } from '@massalabs/as-types';
5 | import { TOTAL_SUPPLY_KEY, totalSupply } from '../MRC20';
6 |
7 | /**
8 | * Theses function are internal to the burnable token.
9 | * We define them and export in this file to avoid exporting them in the contract entry file,
10 | * making them callable from the outside world.
11 | *
12 | */
13 |
14 | /**
15 | * Removes amount of token from addressToBurn.
16 | *
17 | * @param addressToBurn -
18 | * @param amount -
19 | * @returns true if tokens has been burned
20 | */
21 | export function _burn(addressToBurn: Address, amount: u256): void {
22 | const oldRecipientBalance = _balance(addressToBurn);
23 | // @ts-ignore
24 | const newRecipientBalance: u256 = oldRecipientBalance - amount;
25 |
26 | // Check underflow
27 | assert(
28 | oldRecipientBalance > newRecipientBalance,
29 | 'Requested burn amount causes an underflow of the recipient balance',
30 | );
31 |
32 | _setBalance(addressToBurn, newRecipientBalance);
33 | }
34 |
35 | /**
36 | * Decreases the total supply of the token.
37 | *
38 | * @param amount -
39 | * @returns true if the total supply has been decreased
40 | */
41 | export function _decreaseTotalSupply(amount: u256): void {
42 | const oldTotalSupply = bytesToU256(totalSupply([]));
43 | // @ts-ignore
44 | const newTotalSupply: u256 = oldTotalSupply - amount;
45 |
46 | // Check underflow
47 | assert(
48 | oldTotalSupply > newTotalSupply,
49 | 'Requested burn amount causes an underflow of the total supply',
50 | );
51 |
52 | Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(newTotalSupply));
53 | }
54 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/burnable/burn.ts:
--------------------------------------------------------------------------------
1 | import { Context, Address, generateEvent } from '@massalabs/massa-as-sdk';
2 | import { Args } from '@massalabs/as-types';
3 | import { _approve, _allowance } from '../MRC20-internals';
4 | import { _burn, _decreaseTotalSupply } from './burn-internal';
5 |
6 | /**
7 | * Burnable feature for fungible token.
8 | *
9 | * Re-export this file in your contract entry file to make it available in the contract.
10 | *
11 | */
12 |
13 | const BURN_EVENT = 'BURN_SUCCESS';
14 |
15 | /**
16 | * Burn tokens from the caller address
17 | *
18 | * @param binaryArgs - byte string with the following format:
19 | * - the amount of tokens to burn obn the caller address (u256).
20 | */
21 | export function burn(binaryArgs: StaticArray): void {
22 | const args = new Args(binaryArgs);
23 | const amount = args
24 | .nextU256()
25 | .expect('amount argument is missing or invalid');
26 |
27 | _decreaseTotalSupply(amount);
28 |
29 | _burn(Context.caller(), amount);
30 |
31 | generateEvent(BURN_EVENT);
32 | }
33 |
34 | /**
35 | * Burn tokens from the caller address
36 | *
37 | * @param binaryArgs - byte string with the following format:
38 | * - the owner of the tokens to be burned (string).
39 | * - the amount of tokens to burn on the caller address (u256).
40 | *
41 | */
42 | export function burnFrom(binaryArgs: StaticArray): void {
43 | const args = new Args(binaryArgs);
44 | const owner = new Address(
45 | args.nextString().expect('owner argument is missing or invalid'),
46 | );
47 | const amount = args
48 | .nextU256()
49 | .expect('amount argument is missing or invalid');
50 |
51 | const spenderAllowance = _allowance(owner, Context.caller());
52 |
53 | assert(spenderAllowance >= amount, 'burnFrom failed: insufficient allowance');
54 |
55 | _decreaseTotalSupply(amount);
56 |
57 | _burn(owner, amount);
58 |
59 | // @ts-ignore
60 | _approve(owner, Context.caller(), spenderAllowance - amount);
61 |
62 | generateEvent(BURN_EVENT);
63 | }
64 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/OnMRC1155Received.ts:
--------------------------------------------------------------------------------
1 | import { Args } from '@massalabs/as-types';
2 | import { createEvent, generateEvent } from '@massalabs/massa-as-sdk';
3 |
4 | import { u256 } from 'as-bignum/assembly';
5 |
6 | export function onMRC1155BatchReceived(
7 | binaryArgs: StaticArray,
8 | ): StaticArray {
9 | const args = new Args(binaryArgs);
10 | const operator = args
11 | .nextString()
12 | .expect('operator argument is missing or invalid');
13 | const from = args.nextString().expect('from argument is missing or invalid');
14 | const ids = args
15 | .nextFixedSizeArray()
16 | .expect('ids argument is missing or invalid');
17 | const values = args
18 | .nextFixedSizeArray()
19 | .expect('values argument is missing or invalid');
20 | const data = args.nextBytes().expect('data argument is missing or invalid');
21 |
22 | generateEvent(
23 | createEvent('MRC1155BatchReceived', [
24 | operator,
25 | from,
26 | ids.map((id: u256) => id.toString()).join(';'),
27 | values.map((id: u256) => id.toString()).join(';'),
28 | data.toString(),
29 | ]),
30 | );
31 | return new Args().add('bc197c81').serialize();
32 | }
33 |
34 | export function OnMRC1155Received(
35 | binaryArgs: StaticArray,
36 | ): StaticArray {
37 | const args = new Args(binaryArgs);
38 | const operator = args
39 | .nextString()
40 | .expect('operator argument is missing or invalid');
41 | const from = args.nextString().expect('from argument is missing or invalid');
42 | const id = args.nextU256().expect('id argument is missing or invalid');
43 | const value = args.nextU256().expect('value argument is missing or invalid');
44 | const data = args.nextBytes().expect('data argument is missing or invalid');
45 |
46 | generateEvent(
47 | createEvent('MRC1155Received', [
48 | operator,
49 | from,
50 | id.toString(),
51 | value.toString(),
52 | data.toString(),
53 | ]),
54 | );
55 | return new Args().add('f23a6e61').serialize();
56 | }
57 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/OnMRC1155ReceivedFail.ts:
--------------------------------------------------------------------------------
1 | import { Args } from '@massalabs/as-types';
2 | import { createEvent, generateEvent } from '@massalabs/massa-as-sdk';
3 |
4 | import { u256 } from 'as-bignum/assembly';
5 |
6 | export function onMRC1155BatchReceived(
7 | binaryArgs: StaticArray,
8 | ): StaticArray {
9 | const args = new Args(binaryArgs);
10 | const operator = args
11 | .nextString()
12 | .expect('operator argument is missing or invalid');
13 | const from = args.nextString().expect('from argument is missing or invalid');
14 | const ids = args
15 | .nextFixedSizeArray()
16 | .expect('ids argument is missing or invalid');
17 | const values = args
18 | .nextFixedSizeArray()
19 | .expect('values argument is missing or invalid');
20 | const data = args.nextBytes().expect('data argument is missing or invalid');
21 |
22 | generateEvent(
23 | createEvent('MRC1155BatchReceived', [
24 | operator,
25 | from,
26 | ids.map((id: u256) => id.toString()).join(';'),
27 | values.map((id: u256) => id.toString()).join(';'),
28 | data.toString(),
29 | ]),
30 | );
31 | return new Args().add('wrong').serialize();
32 | }
33 |
34 | export function OnMRC1155Received(
35 | binaryArgs: StaticArray,
36 | ): StaticArray {
37 | const args = new Args(binaryArgs);
38 | const operator = args
39 | .nextString()
40 | .expect('operator argument is missing or invalid');
41 | const from = args.nextString().expect('from argument is missing or invalid');
42 | const id = args.nextU256().expect('id argument is missing or invalid');
43 | const value = args.nextU256().expect('value argument is missing or invalid');
44 | const data = args.nextBytes().expect('data argument is missing or invalid');
45 |
46 | generateEvent(
47 | createEvent('MRC1155Received', [
48 | operator,
49 | from,
50 | id.toString(),
51 | value.toString(),
52 | data.toString(),
53 | ]),
54 | );
55 | return new Args().add('wrong').serialize();
56 | }
57 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/__tests__/wrapper.spec.ts:
--------------------------------------------------------------------------------
1 | // These tests serve as an example of how to use the MRC20Wrapper class to interact with the MRC20 contract.
2 | import { MRC20Wrapper } from '../wrapper';
3 | import {
4 | Address,
5 | changeCallStack,
6 | mockBalance,
7 | mockScCall,
8 | mockTransferredCoins,
9 | } from '@massalabs/massa-as-sdk';
10 | import { stringToBytes } from '@massalabs/as-types';
11 | import { u256 } from 'as-bignum/assembly';
12 |
13 | const tokenName = 'myToken';
14 |
15 | const userAddr = 'AU1mhPhXCfh8afoNnbW91bXUVAmu8wU7u8v54yNTMvY7E52KBbz3';
16 | const tokenAddress = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
17 |
18 | const tokenContract = new MRC20Wrapper(new Address(tokenAddress));
19 |
20 | function switchUser(user: string): void {
21 | changeCallStack(user + ' , ' + tokenAddress);
22 | }
23 |
24 | describe('Wrapper tests', () => {
25 | beforeAll(() => {
26 | switchUser(userAddr);
27 | });
28 |
29 | test('token name', () => {
30 | const nameBytes = stringToBytes(tokenName);
31 | mockScCall(nameBytes);
32 | expect(tokenContract.name()).toBe(tokenName);
33 | });
34 |
35 | test('version', () => {
36 | const version = '1.2.3';
37 | const versionBytes = stringToBytes(version);
38 | mockScCall(versionBytes);
39 |
40 | expect(tokenContract.version()).toBe(version);
41 | });
42 |
43 | test('transfer', () => {
44 | const recipient = new Address(
45 | 'AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep',
46 | );
47 | const amount = u256.fromU64(100);
48 | mockScCall([]);
49 |
50 | tokenContract.transfer(recipient, amount);
51 | });
52 |
53 | test('transfer with coins', () => {
54 | const coins = 1_000_000;
55 | const recipient = new Address(
56 | 'AU1bfnCAQAhPT2gAcJkL31fCWJixFFtH7RjRHZsvaThVoeNUckep',
57 | );
58 | const amount = u256.fromU64(100);
59 | mockBalance(userAddr, coins);
60 | mockTransferredCoins(coins);
61 | mockScCall([]);
62 |
63 | tokenContract.transfer(recipient, amount, coins);
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/WMAS.ts:
--------------------------------------------------------------------------------
1 | import { Args, u256ToBytes } from '@massalabs/as-types';
2 | import {
3 | Address,
4 | Context,
5 | Storage,
6 | transferCoins,
7 | } from '@massalabs/massa-as-sdk';
8 | import { burn } from './burnable/burn';
9 | import { u256 } from 'as-bignum/assembly/integer/u256';
10 | import { _mint } from './mintable/mint-internal';
11 | import { balanceKey } from './MRC20-internals';
12 |
13 | export * from './MRC20';
14 |
15 | const STORAGE_BYTE_COST = 100_000;
16 | const STORAGE_PREFIX_LENGTH = 4;
17 | const BALANCE_KEY_PREFIX_LENGTH = 7;
18 |
19 | /**
20 | * Wrap wanted value.
21 | *
22 | * @param _ - unused but mandatory.
23 | */
24 | export function deposit(_: StaticArray): void {
25 | const recipient = Context.caller();
26 | const amount = Context.transferredCoins();
27 | const storageCost = computeMintStorageCost(recipient);
28 | assert(
29 | amount > storageCost,
30 | 'Transferred amount is not enough to cover storage cost',
31 | );
32 | _mint(
33 | new Args()
34 | .add(recipient)
35 | .add(u256.fromU64(amount - storageCost))
36 | .serialize(),
37 | );
38 | }
39 |
40 | /**
41 | * Unwrap wanted value.
42 | *
43 | * @param bs - serialized StaticArray containing
44 | * - the amount to withdraw (u64)
45 | * - the recipient's account (String).
46 | */
47 | export function withdraw(bs: StaticArray): void {
48 | const args = new Args(bs);
49 | const amount = args.nextU64().expect('amount is missing');
50 | const recipient = new Address(
51 | args.nextString().expect('recipient is missing'),
52 | );
53 | burn(u256ToBytes(u256.fromU64(amount)));
54 | transferCoins(recipient, amount);
55 | }
56 |
57 | export function computeMintStorageCost(receiver: Address): u64 {
58 | if (Storage.has(balanceKey(receiver))) {
59 | return 0;
60 | }
61 | const baseLength = STORAGE_PREFIX_LENGTH;
62 | const keyLength = BALANCE_KEY_PREFIX_LENGTH + receiver.toString().length;
63 | const valueLength = 4 * sizeof();
64 | return (baseLength + keyLength + valueLength) * STORAGE_BYTE_COST;
65 | }
66 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/mintable/mint.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This is an extension to the MRC1155 standard.
4 | *
5 | * It allows to mint tokens only by the minter role
6 | *
7 | */
8 |
9 | import { _mint, _mintBatch } from '../MRC1155-internal';
10 | import { Args } from '@massalabs/as-types';
11 |
12 | import { u256 } from 'as-bignum/assembly';
13 | import { onlyRole } from '../../utils/accessControl';
14 |
15 | export const MINTER_ROLE = 'minter';
16 |
17 | /**
18 | *
19 | * Mint a specific amount of tokens to an account
20 | *
21 | * Emits a TransferSingle event
22 | *
23 | * @param to - the account to mint the tokens to
24 | * @param id - the id of the token to mint
25 | * @param value - the amount of tokens to mint
26 | * @param data - additional data to pass to the receiver
27 | */
28 | export function mint(binaryArgs: StaticArray): void {
29 | onlyRole(new Args().add(MINTER_ROLE).serialize());
30 | const args = new Args(binaryArgs);
31 | const to = args.nextString().expect('to argument is missing or invalid');
32 | const id = args.nextU256().expect('id argument is missing or invalid');
33 | const value = args.nextU256().expect('value argument is missing or invalid');
34 | const data = args.nextBytes().expect('data argument is missing or invalid');
35 |
36 | _mint(to, id, value, data);
37 | }
38 |
39 | /**
40 | *
41 | * Mint a batch of tokens to an account
42 | *
43 | * Emits a TransferBatch event
44 | *
45 | * @param to - the account to mint the tokens to
46 | * @param ids - the ids of the tokens to mint
47 | * @param values - the amounts of tokens to mint
48 | * @param data - additional data to pass to the receiver
49 | */
50 | export function mintBatch(binaryArgs: StaticArray): void {
51 | onlyRole(new Args().add(MINTER_ROLE).serialize());
52 | const args = new Args(binaryArgs);
53 | const to = args.nextString().expect('to argument is missing or invalid');
54 | const ids = args
55 | .nextFixedSizeArray()
56 | .expect('ids argument is missing or invalid');
57 | const values = args
58 | .nextFixedSizeArray()
59 | .expect('values argument is missing or invalid');
60 | const data = args.nextBytes().expect('data argument is missing or invalid');
61 |
62 | _mintBatch(to, ids, values, data);
63 | }
64 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/accessControl-internal.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Context,
3 | createEvent,
4 | generateEvent,
5 | Storage,
6 | } from '@massalabs/massa-as-sdk';
7 | import { Args, stringToBytes } from '@massalabs/as-types';
8 | import { onlyOwner } from './ownership';
9 | import { _isOwner } from './ownership-internal';
10 |
11 | export const ROLES_KEY = '_ROLES';
12 |
13 | export const ROLE_GRANTED_EVENT = 'ROLE_GRANTED_EVENT';
14 | export const ROLE_REVOKED_EVENT = 'ROLE_REVOKED_EVENT';
15 |
16 | export function _grantRole(role: string, account: string): void {
17 | onlyOwner();
18 |
19 | assert(!_hasRole(role, account), `Account already has ${role} role`);
20 |
21 | const membersList = _members(role);
22 | membersList.push(account);
23 | Storage.set(_roleKey(role), new Args().add(membersList).serialize());
24 |
25 | generateEvent(createEvent(ROLE_GRANTED_EVENT, [role, account]));
26 | }
27 |
28 | export function _revokeRole(role: string, account: string): void {
29 | const caller = Context.caller().toString();
30 |
31 | if (!_isOwner(caller)) {
32 | assert(
33 | Context.caller().toString() === account,
34 | `Caller is not ${account} or admin`,
35 | );
36 | assert(_hasRole(role, account), `Account does not have ${role} role`);
37 | }
38 |
39 | let membersList = _members(role);
40 | if (membersList.length > 1) {
41 | membersList[membersList.indexOf(account)] =
42 | membersList[membersList.length - 1];
43 | membersList.pop();
44 | } else {
45 | membersList = [];
46 | }
47 | Storage.set(_roleKey(role), new Args().add(membersList).serialize());
48 | generateEvent(createEvent(ROLE_REVOKED_EVENT, [role, account]));
49 | }
50 |
51 | export function _roleKey(role: string): StaticArray {
52 | return stringToBytes(ROLES_KEY + role);
53 | }
54 |
55 | export function _members(role: string): string[] {
56 | const key = _roleKey(role);
57 | if (!Storage.has(key)) {
58 | return [];
59 | }
60 | return new Args(Storage.get(key)).nextStringArray().unwrap();
61 | }
62 |
63 | export function _hasRole(role: string, account: string): bool {
64 | return _members(role).includes(account);
65 | }
66 |
67 | export function _onlyRole(role: string): void {
68 | assert(
69 | _hasRole(role, Context.caller().toString()),
70 | `Caller does not have ${role} role`,
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/burnable/burn.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This is an extension to the MRC1155 standard.
4 | *
5 | * It allows to burn tokens in a permissionless way
6 | */
7 |
8 | import { Args } from '@massalabs/as-types';
9 | import {
10 | MRC1155_MISSING_APPROVAL_FOR_ALL_ERROR,
11 | _burn,
12 | _burnBatch,
13 | _isApprovedForAll,
14 | } from '../MRC1155-internal';
15 | import { Context } from '@massalabs/massa-as-sdk';
16 |
17 | import { u256 } from 'as-bignum/assembly';
18 |
19 | /**
20 | *
21 | * Burn a specific amount of tokens with the approval mechanism.
22 | *
23 | * Emits a TransferSingle event.
24 | *
25 | * @param account - the account to burn the tokens from
26 | * @param id - the id of the token to burn
27 | * @param value - the amount of tokens to burn
28 | */
29 | export function burn(binaryArgs: StaticArray): void {
30 | const sender = Context.caller().toString();
31 | const args = new Args(binaryArgs);
32 | const account = args
33 | .nextString()
34 | .expect('account argument is missing or invalid');
35 | const id = args.nextU256().expect('id argument is missing or invalid');
36 | const value = args.nextU256().expect('value argument is missing or invalid');
37 | assert(
38 | account == sender || _isApprovedForAll(account, sender),
39 | MRC1155_MISSING_APPROVAL_FOR_ALL_ERROR,
40 | );
41 |
42 | _burn(account, id, value);
43 | }
44 |
45 | /**
46 | *
47 | * Burn a batch of tokens with the approval mechanism.
48 | *
49 | * Emits a TransferBatch event.
50 | *
51 | * @param account - the account to burn the tokens from
52 | * @param ids - the ids of the tokens to burn
53 | * @param values - the amounts of tokens to burn
54 | */
55 | export function burnBatch(binaryArgs: StaticArray): void {
56 | const sender = Context.caller().toString();
57 | const args = new Args(binaryArgs);
58 | const account = args
59 | .nextString()
60 | .expect('account argument is missing or invalid');
61 | const ids = args
62 | .nextFixedSizeArray()
63 | .expect('ids argument is missing or invalid');
64 | const values = args
65 | .nextFixedSizeArray()
66 | .expect('values argument is missing or invalid');
67 | assert(
68 | account == sender || _isApprovedForAll(account, sender),
69 | MRC1155_MISSING_APPROVAL_FOR_ALL_ERROR,
70 | );
71 |
72 | _burnBatch(account, ids, values);
73 | }
74 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/metadata/metadata-internal.ts:
--------------------------------------------------------------------------------
1 | import { bytesToString, stringToBytes, u256ToBytes } from '@massalabs/as-types';
2 | import { Storage, createEvent, generateEvent } from '@massalabs/massa-as-sdk';
3 | import { u256 } from 'as-bignum/assembly';
4 |
5 | export const BASE_URI_KEY = stringToBytes('BASE_URI');
6 | export const TOKENS_URI_KEY = stringToBytes('TOKENS_URI');
7 | export const URI_EVENT: string = 'URI';
8 |
9 | /**
10 | * @returns the key for the base uri
11 | */
12 | function baseUriKey(): StaticArray {
13 | return BASE_URI_KEY;
14 | }
15 |
16 | /**
17 | * @param id - the id of the token
18 | * @returns the key for the token uri
19 | */
20 | function tokenUrisKey(id: u256): StaticArray {
21 | return TOKENS_URI_KEY.concat(u256ToBytes(id));
22 | }
23 |
24 | /**
25 | * Set the base URI for all token IDs
26 | * @param newBaseUri - the new base URI
27 | */
28 | export function _setBaseURI(newBaseUri: string): void {
29 | const baseURIKey = baseUriKey();
30 | Storage.set(baseURIKey, stringToBytes(newBaseUri));
31 | }
32 |
33 | /**
34 | * @returns the base URI
35 | */
36 | export function _baseURI(): string {
37 | return Storage.has(BASE_URI_KEY)
38 | ? bytesToString(Storage.get(BASE_URI_KEY))
39 | : '';
40 | }
41 |
42 | /**
43 | * @param id - the id of the token
44 | * @returns the URI for the token
45 | */
46 | export function _tokenURI(id: u256): string {
47 | const tokenUriKey = tokenUrisKey(id);
48 | const tokenUri = Storage.has(tokenUriKey)
49 | ? bytesToString(Storage.get(tokenUriKey))
50 | : '';
51 |
52 | return tokenUri;
53 | }
54 |
55 | /**
56 | * Set the URI for a token
57 | * @param id - the id of the token
58 | * @param newUri - the new URI
59 | */
60 | export function _setURI(id: u256, newUri: string): void {
61 | const tokenUriKey = tokenUrisKey(id);
62 |
63 | Storage.set(tokenUriKey, stringToBytes(newUri));
64 | generateEvent(createEvent(URI_EVENT, [newUri, id.toString()]));
65 | }
66 |
67 | /**
68 | * Returns the URI for a given token ID.
69 | *
70 | * It returns the base uri concatenated to the tokenUri if the tokenUri is not empty
71 | * And if it is empty it returns the super uri from token-internal
72 | *
73 | * @param id - The token ID
74 | * @returns the URI for the given token ID
75 | */
76 | export function _uri(id: u256): string {
77 | return _baseURI().concat(_tokenURI(id));
78 | }
79 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/__tests__/ownership.spec.ts:
--------------------------------------------------------------------------------
1 | import { Args, boolToByte, stringToBytes } from '@massalabs/as-types';
2 | import { Storage, changeCallStack } from '@massalabs/massa-as-sdk';
3 | import { isOwner, onlyOwner, ownerAddress, setOwner } from '../ownership';
4 |
5 | import { resetStorage } from '@massalabs/massa-as-sdk';
6 | import { OWNER_KEY } from '../ownership-internal';
7 |
8 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
9 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
10 |
11 | const owner = 'AUDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBOObs';
12 | const ownerArg = new Args().add(owner).serialize();
13 |
14 | const randomUser = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiq';
15 | beforeAll(() => {
16 | log('beforeAll');
17 | resetStorage();
18 | });
19 |
20 | describe('Ownership', () => {
21 | describe('Ownership not set', () => {
22 | test('OWNER_KEY is not set', () =>
23 | expect(Storage.has(OWNER_KEY)).toStrictEqual(false));
24 | test('ownerAddress is empty', () =>
25 | expect(ownerAddress([])).toStrictEqual(stringToBytes('')));
26 | test('isOwner is false', () =>
27 | expect(isOwner(ownerArg)).toStrictEqual(boolToByte(false)));
28 | throws('onlyOwner throw', () => onlyOwner());
29 | test('set owner', () => setOwner(ownerArg));
30 | });
31 |
32 | describe('Ownership is set', () => {
33 | test('OWNER_KEY is set', () =>
34 | expect(Storage.has(OWNER_KEY)).toStrictEqual(true));
35 | test('OWNER_KEY contains owner', () =>
36 | expect(Storage.get(OWNER_KEY)).toStrictEqual(owner));
37 | test('ownerAddress', () =>
38 | expect(ownerAddress([])).toStrictEqual(stringToBytes(owner)));
39 | test('isOwner', () =>
40 | expect(isOwner(ownerArg)).toStrictEqual(boolToByte(true)));
41 |
42 | throws('onlyOwner of random user throws', () => {
43 | changeCallStack(randomUser + ' , ' + contractAddr);
44 | onlyOwner();
45 | });
46 | test('onlyOwner should not throw', () => {
47 | changeCallStack(owner + ' , ' + contractAddr);
48 | onlyOwner();
49 | });
50 | test('set new owner', () =>
51 | setOwner(new Args().add('new owner').serialize()));
52 | throws('forbidden set new owner', () =>
53 | setOwner(new Args().add('another owner').serialize()),
54 | );
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/mintable/mint-internal.ts:
--------------------------------------------------------------------------------
1 | import { Args, bytesToU256, u256ToBytes } from '@massalabs/as-types';
2 | import { _balance, _setBalance } from '../MRC20-internals';
3 | import { Address, Storage, generateEvent } from '@massalabs/massa-as-sdk';
4 | import { TOTAL_SUPPLY_KEY, totalSupply } from '../MRC20';
5 | import { u256 } from 'as-bignum/assembly';
6 |
7 | /**
8 | * Theses function are internal to the mintable token.
9 | * We define them and export in this file to avoid exporting them in the contract entry file,
10 | * making them callable from the outside world.
11 | *
12 | */
13 |
14 | export const MINT_EVENT = 'MINT SUCCESS';
15 |
16 | /**
17 | * Mint tokens on the recipient address
18 | *
19 | * @param binaryArgs - `Args` serialized StaticArray containing:
20 | * - the recipient's account (address)
21 | * - the amount of tokens to mint (u256).
22 | */
23 | export function _mint(binaryArgs: StaticArray): void {
24 | const args = new Args(binaryArgs);
25 | const recipient = new Address(
26 | args.nextString().expect('recipient argument is missing or invalid'),
27 | );
28 | const amount = args
29 | .nextU256()
30 | .expect('amount argument is missing or invalid');
31 |
32 | _increaseTotalSupply(amount);
33 |
34 | _increaseBalance(recipient, amount);
35 |
36 | generateEvent(MINT_EVENT);
37 | }
38 |
39 | /**
40 | * Adds amount of token to recipient.
41 | *
42 | * @param recipient -
43 | * @param amount -
44 | */
45 | export function _increaseBalance(recipient: Address, amount: u256): void {
46 | const oldRecipientBalance = _balance(recipient);
47 | // @ts-ignore
48 | const newRecipientBalance = oldRecipientBalance + amount;
49 |
50 | // Check overflow
51 | assert(
52 | oldRecipientBalance < newRecipientBalance,
53 | 'Requested mint amount causes an overflow',
54 | );
55 |
56 | _setBalance(recipient, newRecipientBalance);
57 | }
58 |
59 | /**
60 | * Increases the total supply of the token.
61 | *
62 | * @param amount - how much you want to increase the total supply
63 | */
64 | export function _increaseTotalSupply(amount: u256): void {
65 | const oldTotalSupply = bytesToU256(totalSupply([]));
66 | // @ts-ignore
67 | const newTotalSupply = oldTotalSupply + amount;
68 |
69 | // Check overflow
70 | assert(
71 | oldTotalSupply < newTotalSupply,
72 | 'Requested mint amount causes an overflow',
73 | );
74 |
75 | Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(newTotalSupply));
76 | }
77 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/__tests__/metadata.spec.ts:
--------------------------------------------------------------------------------
1 | import { resetStorage, setDeployContext } from '@massalabs/massa-as-sdk';
2 | import { Args, stringToBytes } from '@massalabs/as-types';
3 | import { mrc721Constructor } from '../MRC721';
4 | import { u256 } from 'as-bignum/assembly';
5 | import {
6 | _baseURI,
7 | _setBaseURI,
8 | _setURI,
9 | _tokenURI,
10 | _uri,
11 | uri,
12 | } from '../metadata';
13 |
14 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
15 |
16 | beforeEach(() => {
17 | resetStorage();
18 | setDeployContext(user1Address);
19 | mrc721Constructor('MassaNft', 'MNFT');
20 | });
21 |
22 | describe('_setBaseURI', () => {
23 | test('should set base URI', () => {
24 | const newBaseUri = 'ipfs://QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
25 | _setBaseURI(newBaseUri);
26 | expect(_baseURI()).toStrictEqual(newBaseUri);
27 | });
28 | });
29 |
30 | describe('_setURI', () => {
31 | test('should set URI', () => {
32 | const id = u256.One;
33 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
34 | _setURI(id, newUri);
35 | expect(_tokenURI(id)).toStrictEqual(newUri);
36 | });
37 | });
38 |
39 | describe('_uri', () => {
40 | test('should return token URI without base', () => {
41 | const id = u256.One;
42 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
43 | _setURI(id, newUri);
44 | expect(_uri(id)).toStrictEqual(newUri);
45 | });
46 |
47 | test('should return token URI with base', () => {
48 | const id = u256.One;
49 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
50 | _setURI(id, newUri);
51 | _setBaseURI('ipfs://');
52 | expect(_uri(id)).toStrictEqual('ipfs://' + newUri);
53 | });
54 | });
55 |
56 | describe('uri', () => {
57 | test('should return token URI without base', () => {
58 | const id = u256.One;
59 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
60 |
61 | _setURI(id, newUri);
62 | expect(uri(new Args().add(id).serialize())).toStrictEqual(
63 | stringToBytes(newUri),
64 | );
65 | });
66 |
67 | test('should return token URI with base', () => {
68 | const id = u256.One;
69 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
70 |
71 | _setURI(id, newUri);
72 | _setBaseURI('ipfs://');
73 | expect(uri(new Args().add(id).serialize())).toStrictEqual(
74 | stringToBytes('ipfs://' + newUri),
75 | );
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/metadata/metadata-internal.ts:
--------------------------------------------------------------------------------
1 | import { bytesToString, stringToBytes, u256ToBytes } from '@massalabs/as-types';
2 | import { Storage, createEvent, generateEvent } from '@massalabs/massa-as-sdk';
3 | import { u256 } from 'as-bignum/assembly';
4 | import { URI_EVENT, _uri as superUri } from '../MRC1155-internal';
5 |
6 | export const BASE_URI_KEY = stringToBytes('BASE_URI');
7 | export const TOKENS_URI_KEY = stringToBytes('TOKENS_URI');
8 |
9 | /**
10 | * @returns the key for the base uri
11 | */
12 | function baseUriKey(): StaticArray {
13 | return BASE_URI_KEY;
14 | }
15 |
16 | /**
17 | * @param id - the id of the token
18 | * @returns the key for the token uri
19 | */
20 | function tokenUrisKey(id: u256): StaticArray {
21 | return TOKENS_URI_KEY.concat(u256ToBytes(id));
22 | }
23 |
24 | /**
25 | * Set the base URI for all token IDs
26 | * @param newBaseUri - the new base URI
27 | */
28 | export function _setBaseURI(newBaseUri: string): void {
29 | const baseURIKey = baseUriKey();
30 | Storage.set(baseURIKey, stringToBytes(newBaseUri));
31 | }
32 |
33 | /**
34 | * @returns the base URI
35 | */
36 | export function _baseURI(): string {
37 | return Storage.has(BASE_URI_KEY)
38 | ? bytesToString(Storage.get(BASE_URI_KEY))
39 | : '';
40 | }
41 |
42 | /**
43 | * @param id - the id of the token
44 | * @returns the URI for the token
45 | */
46 | export function _tokenURI(id: u256): string {
47 | const tokenUriKey = tokenUrisKey(id);
48 | const tokenUri = Storage.has(tokenUriKey)
49 | ? bytesToString(Storage.get(tokenUriKey))
50 | : '';
51 |
52 | return tokenUri;
53 | }
54 |
55 | /**
56 | * Set the URI for a token
57 | * @param id - the id of the token
58 | * @param newUri - the new URI
59 | */
60 | export function _setURI(id: u256, newUri: string): void {
61 | const tokenUriKey = tokenUrisKey(id);
62 |
63 | Storage.set(tokenUriKey, stringToBytes(newUri));
64 | generateEvent(createEvent(URI_EVENT, [newUri, id.toString()]));
65 | }
66 |
67 | /**
68 | * Returns the URI for a given token ID.
69 | *
70 | * It returns the base uri concatenated to the tokenUri if the tokenUri is not empty
71 | * And if it is empty it returns the super uri from token-internal
72 | *
73 | * @param id - The token ID
74 | * @returns the URI for the given token ID
75 | */
76 | export function _uri(id: u256): string {
77 | const tokenUri = _tokenURI(id);
78 | const baseUri = _baseURI();
79 |
80 | return tokenUri != '' ? baseUri.concat(tokenUri) : superUri(id);
81 | }
82 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/multicall/__tests__/multicall.spec.ts:
--------------------------------------------------------------------------------
1 | import { Args, stringToBytes } from '@massalabs/as-types';
2 | import { callKey, main } from '../multicall';
3 | import { Call, CallResult } from '../serializers/serializers';
4 | import {
5 | setDeployContext,
6 | setOpData,
7 | mockScCall,
8 | resetStorage,
9 | } from '@massalabs/massa-as-sdk';
10 |
11 | describe('Multicall Contract Tests', () => {
12 | beforeAll(() => {
13 | setDeployContext();
14 | });
15 |
16 | beforeEach(() => {
17 | resetStorage();
18 | });
19 |
20 | test('should execute simple call', () => {
21 | // Create test calls
22 | const call = new Call(
23 | 'contract1',
24 | 'function1',
25 | 100,
26 | stringToBytes('param1'),
27 | );
28 | // Serialize call and set it in storage
29 | setOpData(callKey(0), call.serialize());
30 |
31 | // Mock the call results
32 | const call1Result: StaticArray = [3, 3, 3];
33 | mockScCall(call1Result);
34 |
35 | // Execute multicall
36 | const res = main([]);
37 |
38 | const resArray = new Args(res)
39 | .nextSerializableObjectArray()
40 | .unwrap();
41 | expect(resArray.length).toBe(1);
42 | expect(resArray[0]).toStrictEqual(new CallResult(call1Result));
43 | });
44 |
45 | test('should execute multiple calls', () => {
46 | // Create test calls
47 | const calls: Call[] = [
48 | new Call('contract1', 'function1', 100, stringToBytes('param1')),
49 | new Call('contract2', 'function2', 200, stringToBytes('param2')),
50 | ];
51 |
52 | // Serialize calls and set them in storage
53 | for (let i = 0; i < calls.length; i++) {
54 | const call = calls[i];
55 | setOpData(callKey(i), call.serialize());
56 | }
57 |
58 | // Mock the call results
59 | const call1Result: StaticArray = [1, 2, 3];
60 | const call2Result: StaticArray = [4, 5, 6];
61 | mockScCall(call1Result);
62 | mockScCall(call2Result);
63 |
64 | // Execute multicall
65 | const res = main([]);
66 |
67 | const resArray = new Args(res)
68 | .nextSerializableObjectArray()
69 | .unwrap();
70 | expect(resArray.length).toBe(2);
71 | expect(resArray[0]).toStrictEqual(new CallResult(call1Result));
72 | expect(resArray[1]).toStrictEqual(new CallResult(call2Result));
73 | });
74 |
75 | test('should handle empty calls array', () => {
76 | const res = main(new StaticArray(0));
77 | expect(res).toStrictEqual([0, 0, 0, 0]);
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/MRC20-internals.ts:
--------------------------------------------------------------------------------
1 | import { stringToBytes } from '@massalabs/as-types';
2 | import { bytesToU256, u256ToBytes } from '@massalabs/as-types';
3 | import { Address, Storage } from '@massalabs/massa-as-sdk';
4 | import { u256 } from 'as-bignum/assembly';
5 |
6 | export const BALANCE_KEY_PREFIX = 'BALANCE';
7 | export const ALLOWANCE_KEY_PREFIX = 'ALLOWANCE';
8 |
9 | /**
10 | * Theses function are intended to be used in different token types (mintable, burnable...).
11 | * We define them and export in this file to avoid exporting them in the contract entry file,
12 | * making them callable from the outside world
13 | *
14 | */
15 |
16 | /**
17 | * Returns the balance of a given address.
18 | *
19 | * @param address - address to get the balance for
20 | */
21 | export function _balance(address: Address): u256 {
22 | const key = balanceKey(address);
23 | if (Storage.has(key)) {
24 | return bytesToU256(Storage.get(key));
25 | }
26 | return u256.Zero;
27 | }
28 |
29 | /**
30 | * Sets the balance of a given address.
31 | *
32 | * @param address - address to set the balance for
33 | * @param balance -
34 | */
35 | export function _setBalance(address: Address, balance: u256): void {
36 | Storage.set(balanceKey(address), u256ToBytes(balance));
37 | }
38 |
39 | /**
40 | * @param address -
41 | * @returns the key of the balance in the storage for the given address
42 | */
43 | export function balanceKey(address: Address): StaticArray {
44 | return stringToBytes(BALANCE_KEY_PREFIX + address.toString());
45 | }
46 |
47 | /**
48 | * Sets the allowance of the spender on the owner's account.
49 | *
50 | * @param owner - owner address
51 | * @param spender - spender address
52 | * @param amount - amount to set an allowance for
53 | */
54 | export function _approve(owner: Address, spender: Address, amount: u256): void {
55 | const key = allowanceKey(owner, spender);
56 | Storage.set(key, u256ToBytes(amount));
57 | }
58 |
59 | /**
60 | * Returns the allowance set on the owner's account for the spender.
61 | *
62 | * @param owner - owner's id
63 | * @param spender - spender's id
64 | *
65 | * @returns the allowance
66 | */
67 | export function _allowance(owner: Address, spender: Address): u256 {
68 | const key = allowanceKey(owner, spender);
69 | return Storage.has(key) ? bytesToU256(Storage.get(key)) : u256.Zero;
70 | }
71 |
72 | /**
73 | * @param owner - address of the token owner
74 | * @param spender - address of the token spender
75 | * @returns the key of the allowance in the storage for the given addresses
76 | */
77 | function allowanceKey(owner: Address, spender: Address): StaticArray {
78 | return stringToBytes(
79 | ALLOWANCE_KEY_PREFIX + owner.toString().concat(spender.toString()),
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/accessControl.ts:
--------------------------------------------------------------------------------
1 | import { Args, boolToByte } from '@massalabs/as-types';
2 | import {
3 | _grantRole,
4 | _hasRole,
5 | _members,
6 | _onlyRole,
7 | _revokeRole,
8 | } from './accessControl-internal';
9 | import { balance, transferRemaining } from '@massalabs/massa-as-sdk';
10 |
11 | /**
12 | * Set the role for account
13 | *
14 | * @param role - role name string
15 | * @param account - account address string
16 | */
17 | export function grantRole(binaryArgs: StaticArray): void {
18 | const initBal = balance();
19 |
20 | const args = new Args(binaryArgs);
21 | const role = args.nextString().expect('role argument is missing or invalid');
22 | const account = args
23 | .nextString()
24 | .expect('account argument is missing or invalid');
25 |
26 | _grantRole(role, account);
27 |
28 | transferRemaining(initBal);
29 | }
30 |
31 | /**
32 | * get the members for a role
33 | *
34 | * @param role - role name string
35 | */
36 | export function members(binaryArgs: StaticArray): StaticArray {
37 | const args = new Args(binaryArgs);
38 | const role = args.nextString().expect('role argument is missing or invalid');
39 | return new Args().add(_members(role)).serialize();
40 | }
41 |
42 | /**
43 | * Returns true if the account has the role.
44 | *
45 | * @param role - role name string
46 | * @param account - account address string
47 | * @returns boolean
48 | */
49 | export function hasRole(binaryArgs: StaticArray): StaticArray {
50 | const args = new Args(binaryArgs);
51 | const role = args.nextString().expect('role argument is missing or invalid');
52 |
53 | const account = args
54 | .nextString()
55 | .expect('account argument is missing or invalid');
56 |
57 | return boolToByte(_hasRole(role, account));
58 | }
59 |
60 | /**
61 | * Revoke role for account. Must be called by the role owner or the contract admin.
62 | *
63 | * @param role - role name string
64 | * @param account - account address string
65 | */
66 | export function revokeRole(binaryArgs: StaticArray): void {
67 | const initBal = balance();
68 |
69 | const args = new Args(binaryArgs);
70 | const role = args.nextString().expect('role argument is missing or invalid');
71 | const account = args
72 | .nextString()
73 | .expect('account argument is missing or invalid');
74 |
75 | _revokeRole(role, account);
76 | transferRemaining(initBal);
77 | }
78 |
79 | /**
80 | * Assert that caller has the role.
81 | *
82 | * @param role - role name string
83 | * @returns boolean
84 | */
85 | export function onlyRole(binaryArgs: StaticArray): void {
86 | const role = new Args(binaryArgs)
87 | .nextString()
88 | .expect('role argument is missing or invalid');
89 | _onlyRole(role);
90 | }
91 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Massa Standard Definition
2 |
3 | Thank you for your interest in contributing to the Massa Standard Definition project!
4 |
5 | This project is open to contributions from anyone in the Massa community, and we welcome your input.
6 |
7 | Thank you for considering contributing to massa-standards!
8 |
9 | ## Reporting Bugs
10 |
11 | If you discover a bug, please create a [new issue](https://github.com/massalabs/massa-standards/issues/new?assignees=&labels=issue%3Abug&projects=&template=bug.md&title=) on our GitHub repository.
12 |
13 | In your issue, please include a clear and concise description of the bug, any relevant code snippets, error messages, and steps to reproduce the issue.
14 |
15 | ## Contributing Code
16 |
17 | To get started with contributing to the project, please follow these steps:
18 |
19 | 1. Fork the repository on GitHub.
20 | 2. Clone the forked repository to your local machine.
21 | 3. Create a new branch for your changes.
22 | 4. Make your changes and commit them to your branch.
23 | 5. Push your branch to your forked repository on GitHub.
24 | 6. Create a pull request to merge your changes into the main repository.
25 |
26 | ## Guidelines
27 |
28 | When contributing to the project, please follow these guidelines:
29 |
30 | - Use clear and concise language in your code and documentation.
31 | - Ensure that your code is well-structured and easy to read.
32 | - Be respectful and professional in your interactions with other contributors.
33 |
34 | ## Code style
35 | Please ensure that your code follows the existing code style used in the project.
36 |
37 | For smart contract, ensure that your code follows the existing code style used in the project. We use the [MassaLabs Prettier configuration](https://github.com/massalabs/prettier-config-as) and [MassaLabs ESLint configuration](https://github.com/massalabs/eslint-config) for formatting and linting.
38 |
39 | You can run the following command to format your code before committing:
40 |
41 | ```sh
42 | npm run fmt
43 | ```
44 |
45 | ## Tests
46 |
47 | Please ensure that your changes include any necessary tests.
48 |
49 | You can run the following command to run the smart-contracts tests:
50 |
51 | ```sh
52 | npm run build
53 | ```
54 |
55 | and then
56 |
57 | ```sh
58 | npm run test
59 | ```
60 | ## Template
61 |
62 | Please use the following template when starting a new standard document (see [example](wallet/dapps-communication.md)):
63 |
64 | ```markdown
65 | # Name
66 |
67 | **Authors:**
68 |
69 | **Status:** Draft, Effective or Deprecated
70 |
71 | **Version:** 0.1
72 |
73 | ## Abstract
74 |
75 | ## Targeted Audience
76 |
77 | ## Specification
78 |
79 | ## Implementation
80 |
81 | ```
82 |
83 | ## License
84 |
85 | By contributing to massa standard, you agree that your contributions will be licensed under the MIT License.
86 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/__tests__/metadata.spec.ts:
--------------------------------------------------------------------------------
1 | import { resetStorage, setDeployContext } from '@massalabs/massa-as-sdk';
2 | import { Args, stringToBytes } from '@massalabs/as-types';
3 |
4 | import { u256 } from 'as-bignum/assembly';
5 | import {
6 | _baseURI,
7 | _setBaseURI,
8 | _setURI,
9 | _tokenURI,
10 | _uri,
11 | uri,
12 | } from '../metadata';
13 | import { mrc1155Constructor } from '../MRC1155';
14 |
15 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
16 |
17 | const TOKEN_URI = 'ipfs://QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
18 |
19 | beforeEach(() => {
20 | resetStorage();
21 | setDeployContext(user1Address);
22 | mrc1155Constructor(TOKEN_URI);
23 | });
24 |
25 | describe('_setBaseURI', () => {
26 | test('should set base URI', () => {
27 | const newBaseUri = 'ipfs://QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
28 | _setBaseURI(newBaseUri);
29 | expect(_baseURI()).toStrictEqual(newBaseUri);
30 | });
31 | });
32 |
33 | describe('_setURI', () => {
34 | test('should set URI', () => {
35 | const id = u256.One;
36 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
37 | _setURI(id, newUri);
38 | expect(_tokenURI(id)).toStrictEqual(newUri);
39 | });
40 | });
41 |
42 | describe('_uri', () => {
43 | test('should return token URI without base', () => {
44 | const id = u256.One;
45 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
46 | _setURI(id, newUri);
47 | expect(_uri(id)).toStrictEqual(newUri);
48 | });
49 |
50 | test('should return token URI with base', () => {
51 | const id = u256.One;
52 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
53 | _setURI(id, newUri);
54 | _setBaseURI('ipfs://');
55 | expect(_uri(id)).toStrictEqual('ipfs://' + newUri);
56 | });
57 |
58 | test('should return super URI', () => {
59 | const id = u256.One;
60 | expect(_uri(id)).toStrictEqual(TOKEN_URI);
61 | });
62 | });
63 |
64 | describe('uri', () => {
65 | test('should return token URI without base', () => {
66 | const id = u256.One;
67 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
68 |
69 | _setURI(id, newUri);
70 | expect(uri(new Args().add(id).serialize())).toStrictEqual(
71 | stringToBytes(newUri),
72 | );
73 | });
74 |
75 | test('should return token URI with base', () => {
76 | const id = u256.One;
77 | const newUri = 'QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
78 |
79 | _setURI(id, newUri);
80 | _setBaseURI('ipfs://');
81 | expect(uri(new Args().add(id).serialize())).toStrictEqual(
82 | stringToBytes('ipfs://' + newUri),
83 | );
84 | });
85 |
86 | test('should return super URI', () => {
87 | const id = u256.One;
88 | expect(uri(new Args().add(id).serialize())).toStrictEqual(
89 | stringToBytes(TOKEN_URI),
90 | );
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/__tests__/MRC20-mint.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCallStack,
3 | resetStorage,
4 | setDeployContext,
5 | } from '@massalabs/massa-as-sdk';
6 | import {
7 | Args,
8 | bytesToString,
9 | stringToBytes,
10 | u8toByte,
11 | u256ToBytes,
12 | } from '@massalabs/as-types';
13 | import { mint } from '../mintable/mint';
14 | import {
15 | VERSION,
16 | balanceOf,
17 | mrc20Constructor,
18 | decimals,
19 | name,
20 | symbol,
21 | totalSupply,
22 | version,
23 | } from '../MRC20';
24 | import { ownerAddress } from '../../utils/ownership';
25 | import { u256 } from 'as-bignum/assembly';
26 |
27 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
28 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
29 |
30 | const user1Address = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
31 |
32 | const user2Address = 'A12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8';
33 |
34 | function switchUser(user: string): void {
35 | changeCallStack(user + ' , ' + contractAddr);
36 | }
37 |
38 | const TOKEN_NAME = 'MINTABLE_TOKEN';
39 | const TOKEN_SYMBOL = 'MTKN';
40 | const DECIMALS: u8 = 2;
41 | const TOTAL_SUPPLY = new u256(10000, 1000, 100, 10);
42 |
43 | beforeAll(() => {
44 | resetStorage();
45 | setDeployContext(user1Address);
46 | mrc20Constructor(TOKEN_NAME, TOKEN_SYMBOL, DECIMALS, TOTAL_SUPPLY);
47 | });
48 |
49 | describe('ERC20 MINT - Initialization', () => {
50 | test('total supply is properly initialized', () => {
51 | expect(totalSupply([])).toStrictEqual(u256ToBytes(TOTAL_SUPPLY));
52 | });
53 |
54 | test('token name is properly initialized', () => {
55 | expect(name([])).toStrictEqual(stringToBytes(TOKEN_NAME));
56 | });
57 |
58 | test('symbol is properly initialized', () => {
59 | expect(symbol([])).toStrictEqual(stringToBytes(TOKEN_SYMBOL));
60 | });
61 |
62 | test('decimals is properly initialized', () => {
63 | expect(decimals([])).toStrictEqual(u8toByte(DECIMALS));
64 | });
65 |
66 | test('version is properly initialized', () => {
67 | expect(version([])).toStrictEqual(VERSION);
68 | });
69 |
70 | test('owner is properly initialized', () => {
71 | expect(bytesToString(ownerAddress([]))).toStrictEqual(user1Address);
72 | });
73 | });
74 |
75 | const mintAmount = new u256(5000, 33);
76 |
77 | describe('Mint ERC20 to U2', () => {
78 | test('Should mint ERC20', () => {
79 | mint(new Args().add(user2Address).add(mintAmount).serialize());
80 | // check balance of U2
81 | expect(balanceOf(new Args().add(user2Address).serialize())).toStrictEqual(
82 | u256ToBytes(mintAmount),
83 | );
84 |
85 | // check totalSupply update
86 | expect(totalSupply([])).toStrictEqual(
87 | // @ts-ignore
88 | u256ToBytes(mintAmount + TOTAL_SUPPLY),
89 | );
90 | });
91 | });
92 |
93 | describe('Fails mint ERC20', () => {
94 | throws('Should overflow ERC20', () =>
95 | mint(new Args().add(user2Address).add(U64.MAX_VALUE).serialize()),
96 | );
97 |
98 | switchUser(user2Address);
99 |
100 | throws('Should fail because the owner is not the tx emitter', () =>
101 | mint(new Args().add(user1Address).add(u64(5000)).serialize()),
102 | );
103 |
104 | test("Should check totalSupply didn't change", () => {
105 | expect(totalSupply([])).toStrictEqual(
106 | // @ts-ignore
107 | u256ToBytes(mintAmount + TOTAL_SUPPLY),
108 | );
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/__tests__/mintable.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCallStack,
3 | resetStorage,
4 | setDeployContext,
5 | } from '@massalabs/massa-as-sdk';
6 | import { Args, stringToBytes, u256ToBytes } from '@massalabs/as-types';
7 | import { balanceOf, mrc1155Constructor } from '../MRC1155';
8 | import { u256 } from 'as-bignum/assembly';
9 | import { _balanceOfBatch } from '../MRC1155-internal';
10 | import { MINTER_ROLE, mint, mintBatch } from '../mintable';
11 | import { grantRole } from '../../utils/accessControl';
12 |
13 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
14 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
15 |
16 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
17 |
18 | const user2Address = 'AU12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8';
19 |
20 | const TOKEN_URI = 'ipfs://QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
21 |
22 | function switchUser(user: string): void {
23 | changeCallStack(user + ' , ' + contractAddr);
24 | }
25 |
26 | beforeEach(() => {
27 | switchUser(user1Address);
28 | resetStorage();
29 | setDeployContext(user1Address);
30 | mrc1155Constructor(TOKEN_URI);
31 | });
32 |
33 | describe('mint', () => {
34 | test('should mint tokens', () => {
35 | const id = u256.One;
36 | const value = u256.from(10);
37 | const data = stringToBytes('mint data');
38 | grantRole(new Args().add(MINTER_ROLE).add(user1Address).serialize());
39 | mint(
40 | new Args()
41 | .add(stringToBytes(user1Address))
42 | .add(id)
43 | .add(value)
44 | .add(data)
45 | .serialize(),
46 | );
47 | expect(
48 | balanceOf(
49 | new Args().add(stringToBytes(user1Address)).add(id).serialize(),
50 | ),
51 | ).toStrictEqual(u256ToBytes(value));
52 | });
53 |
54 | throws('Caller does not have minter role', () => {
55 | const id = u256.One;
56 | const value = u256.from(10);
57 | const data = stringToBytes('mint data');
58 | switchUser(user2Address);
59 | mint(
60 | new Args()
61 | .add(stringToBytes(user1Address))
62 | .add(id)
63 | .add(value)
64 | .add(data)
65 | .serialize(),
66 | );
67 | });
68 | });
69 |
70 | describe('mintBatch', () => {
71 | test('should mint tokens', () => {
72 | const owners = [user1Address, user1Address];
73 | const ids = [u256.One, u256.from(2)];
74 | const values = [u256.from(10), u256.from(20)];
75 | const data = stringToBytes('mint data');
76 | grantRole(new Args().add(MINTER_ROLE).add(user1Address).serialize());
77 | mintBatch(
78 | new Args().add(user1Address).add(ids).add(values).add(data).serialize(),
79 | );
80 | expect(_balanceOfBatch(owners, ids)).toStrictEqual(values);
81 | });
82 |
83 | throws('Caller does not have minter role', () => {
84 | const ids = [u256.One, u256.from(2)];
85 | const values = [u256.from(10), u256.from(20)];
86 | const data = stringToBytes('mint data');
87 | switchUser(user2Address);
88 | mintBatch(
89 | new Args().add(user1Address).add(ids).add(values).add(data).serialize(),
90 | );
91 | });
92 |
93 | throws('MRC1155InvalidArrayLength', () => {
94 | const ids = [u256.One];
95 | const values = [u256.from(10), u256.from(20)];
96 | const data = stringToBytes('mint data');
97 | grantRole(new Args().add(MINTER_ROLE).add(user1Address).serialize());
98 | mintBatch(
99 | new Args().add(user1Address).add(ids).add(values).add(data).serialize(),
100 | );
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/utils/__tests__/accessControl.spec.ts:
--------------------------------------------------------------------------------
1 | import { Args, boolToByte } from '@massalabs/as-types';
2 | import { changeCallStack } from '@massalabs/massa-as-sdk';
3 | import {
4 | grantRole,
5 | hasRole,
6 | members,
7 | onlyRole,
8 | revokeRole,
9 | } from '../accessControl';
10 |
11 | import { resetStorage } from '@massalabs/massa-as-sdk';
12 | import { setOwner } from '../ownership';
13 |
14 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
15 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
16 |
17 | const owner = 'AUDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBOObs';
18 | const randomUser = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiq';
19 | const minterUser = 'AU12Ny7y5CndDDFgMJcq5kQiUJQTLutaVqUBnqTHDQALpocVBnkP';
20 | const minterRole = 'minter';
21 | const roleArg1 = new Args().add(minterRole).add(owner).serialize();
22 | const roleArg2 = new Args().add(minterRole).add(minterUser).serialize();
23 |
24 | beforeAll(() => {
25 | resetStorage();
26 | });
27 |
28 | describe('Roles', () => {
29 | test('has no role yet', () => {
30 | expect(hasRole(roleArg1)).toStrictEqual(boolToByte(false));
31 | expect(members(new Args().add(minterRole).serialize())).toStrictEqual([
32 | 0, 0, 0, 0,
33 | ]);
34 | });
35 | throws('cannot perform action without role', () => {
36 | onlyRole(new Args().add(minterRole).serialize());
37 | });
38 |
39 | test('can grant a role', () => {
40 | setOwner(new Args().add(owner).serialize());
41 | switchUser(owner);
42 | grantRole(roleArg1);
43 | onlyRole(new Args().add(minterRole).serialize());
44 | expect(hasRole(roleArg1)).toBeTruthy();
45 | expect(members(new Args().add(minterRole).serialize())).toStrictEqual(
46 | new Args().add([owner]).serialize(),
47 | );
48 | });
49 |
50 | test('self revoke a role', () => {
51 | revokeRole(roleArg1);
52 | expect(hasRole(roleArg1)).toStrictEqual(boolToByte(false));
53 | expect(members(new Args().add(minterRole).serialize())).toStrictEqual([
54 | 0, 0, 0, 0,
55 | ]);
56 | });
57 | throws('cannot perform action without role', () => {
58 | onlyRole(new Args().add(minterRole).serialize());
59 | });
60 |
61 | test('can get members of some role', () => {
62 | grantRole(roleArg1);
63 | grantRole(roleArg2);
64 | const minters = new Args(members(new Args().add(minterRole).serialize()))
65 | .nextStringArray()
66 | .unwrap();
67 | expect(minters).toStrictEqual([owner, minterUser]);
68 | });
69 |
70 | throws('cannot perform action without role', () => {
71 | switchUser(randomUser);
72 | onlyRole(new Args().add(minterRole).serialize());
73 | });
74 |
75 | test('create and grant a new role', () => {
76 | const role = 'SINGER_ROLE';
77 | const userRoleArg = new Args().add(role).add(randomUser).serialize();
78 | switchUser(owner);
79 | grantRole(userRoleArg);
80 | expect(hasRole(userRoleArg)).toBeTruthy();
81 | expect(members(new Args().add(role).serialize())).toStrictEqual(
82 | new Args().add([randomUser]).serialize(),
83 | );
84 |
85 | switchUser(randomUser);
86 | onlyRole(new Args().add(role).serialize());
87 | });
88 |
89 | test('admin revoke a role', () => {
90 | const role = 'SINGER_ROLE';
91 | const userRoleArg = new Args().add(role).add(randomUser).serialize();
92 | switchUser(owner);
93 | revokeRole(userRoleArg);
94 | expect(hasRole(userRoleArg)).toStrictEqual(boolToByte(false));
95 | expect(members(new Args().add(role).serialize())).toStrictEqual([
96 | 0, 0, 0, 0,
97 | ]);
98 | });
99 | });
100 |
101 | function switchUser(user: string): void {
102 | changeCallStack(user + ' , ' + contractAddr);
103 | }
104 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/deployer/deployer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateEvent,
3 | createSC,
4 | getOpData,
5 | call,
6 | functionExists,
7 | hasOpKey,
8 | generateRawEvent,
9 | Address,
10 | } from '@massalabs/massa-as-sdk';
11 | import { Args } from '@massalabs/as-types';
12 |
13 | const CONSTRUCTOR = 'constructor';
14 |
15 | /**
16 | * This function deploys and calls the constructor function of the deployed smart contract.
17 | *
18 | * The data structure of the operation datastore must be like describe in
19 | * packages/sc-deployer/src/index.ts
20 | *
21 | * @param _ - not used
22 | */
23 | export function main(_: StaticArray): void {
24 | let nbSC = getNbSC();
25 |
26 | const deployedSC: Address[] = [];
27 |
28 | for (let i: u64 = 0; i < nbSC; i++) {
29 | const contractAddr = createSC(getScByteCode(i));
30 |
31 | if (functionExists(contractAddr, CONSTRUCTOR)) {
32 | call(contractAddr, CONSTRUCTOR, getConstructorArgs(i), getCoins(i));
33 | }
34 |
35 | generateEvent(`Contract deployed at address: ${contractAddr.toString()}`);
36 | deployedSC.push(contractAddr);
37 | }
38 |
39 | generateRawEvent(
40 | new Args().addSerializableObjectArray(deployedSC).serialize(),
41 | );
42 | }
43 |
44 | /**
45 | * Get the number of smart contract to deploy.
46 | * @returns The number of smart contract to deploy.
47 | * @throws if the number of smart contract is not defined.
48 | */
49 | function getNbSC(): u64 {
50 | const key: StaticArray = [0];
51 |
52 | assert(
53 | hasOpKey(key),
54 | 'The number of smart contracts to deploy is undefined.',
55 | );
56 | const raw = getOpData(key);
57 | return new Args(raw).mustNext('nbSC');
58 | }
59 |
60 | /**
61 | * Get the bytecode of the smart contract to deploy.
62 | * @param i - The index of the smart contract.
63 | * @returns The bytecode of the smart contract.
64 | * @throws if the bytecode of the smart contract is not defined.
65 | */
66 | function getScByteCode(i: u64): StaticArray {
67 | const key = new Args().add(i + 1).serialize();
68 | assert(hasOpKey(key), `No bytecode found for contract number: ${i + 1}`);
69 | return getOpData(key);
70 | }
71 |
72 | /**
73 | * Get the arguments key of the constructor function of the smart contract to deploy.
74 | * @param i - The index of the smart contract.
75 | * @returns The arguments key of the constructor function.
76 | */
77 | function argsKey(i: u64): StaticArray {
78 | const argsSubKey: StaticArray = [0];
79 | return new Args()
80 | .add(i + 1)
81 | .add(argsSubKey)
82 | .serialize();
83 | }
84 |
85 | /**
86 | * Get the arguments of the constructor function of the smart contract to deploy.
87 | * @param i - The index of the smart contract.
88 | * @returns The arguments of the constructor function.
89 | */
90 | function getConstructorArgs(i: u64): Args {
91 | const keyArgs = argsKey(i);
92 | return hasOpKey(keyArgs) ? new Args(getOpData(argsKey(i))) : new Args();
93 | }
94 |
95 | /**
96 | * Get the coins key of the constructor function of the smart contract to deploy.
97 | * @param i - The index of the smart contract.
98 | * @returns The coins key of the constructor function.
99 | */
100 | function coinsKey(i: u64): StaticArray {
101 | let coinsSubKey: StaticArray = [1];
102 |
103 | return new Args()
104 | .add(i + 1)
105 | .add(coinsSubKey)
106 | .serialize();
107 | }
108 |
109 | /**
110 | * Get the coins of the constructor function of the smart contract to deploy.
111 | * @param i - The index of the smart contract.
112 | * @returns The coins of the constructor function.
113 | */
114 | function getCoins(i: u64): u64 {
115 | let keyCoins = coinsKey(i);
116 |
117 | return hasOpKey(keyCoins)
118 | ? new Args(getOpData(keyCoins)).next().unwrapOrDefault()
119 | : 0;
120 | }
121 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/__tests__/WMAS.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Address,
3 | changeCallStack,
4 | resetStorage,
5 | setDeployContext,
6 | mockBalance,
7 | mockTransferredCoins,
8 | } from '@massalabs/massa-as-sdk';
9 | import { Args, u256ToBytes } from '@massalabs/as-types';
10 | import {
11 | balanceOf,
12 | mrc20Constructor,
13 | deposit,
14 | withdraw,
15 | computeMintStorageCost,
16 | } from '../WMAS';
17 | import { u256 } from 'as-bignum/assembly';
18 |
19 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
20 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
21 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
22 | const user2Address = 'AU12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8';
23 | const user3Address = 'AUDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBOObs';
24 |
25 | const amount = 1_000_000_000_000;
26 | const storageCost = computeMintStorageCost(new Address(user2Address));
27 | const amountMinusStorageCost = amount - storageCost;
28 |
29 | function switchUser(user: string): void {
30 | changeCallStack(user + ' , ' + contractAddr);
31 | }
32 |
33 | beforeEach(() => {
34 | resetStorage();
35 | setDeployContext(user1Address);
36 | mrc20Constructor('Wrapped MAS', 'WMAS', 9, u256.Zero);
37 | });
38 |
39 | describe('deposit', () => {
40 | it('should deposit MAS', () => {
41 | switchUser(user2Address);
42 | mockBalance(user2Address, amount);
43 | mockTransferredCoins(amount);
44 | deposit([]);
45 | const balance = balanceOf(new Args().add(user2Address).serialize());
46 | expect(balance).toStrictEqual(
47 | u256ToBytes(u256.fromU64(amountMinusStorageCost)),
48 | );
49 | });
50 | it('should not charge for storage for later deposits', () => {
51 | switchUser(user2Address);
52 | mockBalance(user2Address, amount);
53 | mockTransferredCoins(amount);
54 | deposit([]);
55 | mockBalance(user2Address, amount);
56 | mockTransferredCoins(amount);
57 | deposit([]);
58 | expect(balanceOf(new Args().add(user2Address).serialize())).toStrictEqual(
59 | u256ToBytes(u256.fromU64(amount + amountMinusStorageCost)),
60 | );
61 | });
62 | it('should reject operation not covering storage cost', () => {
63 | switchUser(user3Address);
64 | mockBalance(user3Address, storageCost);
65 | mockTransferredCoins(storageCost);
66 | expect(() => {
67 | deposit([]);
68 | }).toThrow('Transferred amount is not enough to cover storage cost');
69 | });
70 | it('should deposit minimal amount', () => {
71 | switchUser(user3Address);
72 | mockBalance(user3Address, storageCost + 1);
73 | mockTransferredCoins(storageCost + 1);
74 | deposit([]);
75 | expect(balanceOf(new Args().add(user3Address).serialize())).toStrictEqual(
76 | u256ToBytes(u256.One),
77 | );
78 | });
79 | });
80 |
81 | describe('withdraw', () => {
82 | beforeEach(() => {
83 | switchUser(user2Address);
84 | mockBalance(user2Address, amount);
85 | mockTransferredCoins(amount);
86 | deposit([]);
87 | mockBalance(contractAddr, amount);
88 | });
89 |
90 | it('should withdraw MAS', () => {
91 | withdraw(
92 | new Args().add(amountMinusStorageCost).add(user2Address).serialize(),
93 | );
94 | expect(balanceOf(new Args().add(user2Address).serialize())).toStrictEqual(
95 | u256ToBytes(u256.Zero),
96 | );
97 | });
98 | it('should throw if amount is missing', () => {
99 | expect(() => {
100 | withdraw(new Args().add(user2Address).serialize());
101 | }).toThrow('amount is missing');
102 | });
103 | it('should throw if recipient is missing', () => {
104 | expect(() => {
105 | withdraw(new Args().add(amount).serialize());
106 | }).toThrow('recipient is missing');
107 | });
108 | it('should throw if amount is greater than balance', () => {
109 | expect(() => {
110 | withdraw(
111 | new Args()
112 | .add(2 * amount)
113 | .add(user2Address)
114 | .serialize(),
115 | );
116 | }).toThrow('Requested burn amount causes an underflow');
117 | });
118 | it('should reject non-depositor', () => {
119 | switchUser(user1Address);
120 | expect(() => {
121 | withdraw(new Args().add(amount).add(user1Address).serialize());
122 | }).toThrow('Requested burn amount causes an underflow');
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/units.md:
--------------------------------------------------------------------------------
1 | # Massa Units
2 |
3 | **Authors:** G. Libert
4 |
5 | **Status:** Effective
6 |
7 | **Version:** 1.0
8 |
9 | **History:**
10 |
11 | | Date | Comment |
12 | | ---- | ------- |
13 | | 03/2023 | The standard is effective |
14 |
15 | ## Abstract
16 |
17 | This specification defines the units used to measure the different objects of the Massa blockchain. It aims to provide a standardized format for the different units to offer the simplest and most readable user experience possible while being consistent from one application to another within our ecosystem.
18 |
19 | > _NOTE:_ The manipulation of very large or very small numbers will be simplified by the use of [metric prefixes](https://en.wikipedia.org/wiki/Metric_prefix) of the [SI](https://en.wikipedia.org/wiki/International_System_of_Units).
20 |
21 | ## Targeted Audience
22 |
23 | This specification is intended for developers who wish to interact with the Massa blockchain network using various tools and applications such as node API, smart contract tooling, dApps tooling, Thyra, and Thyra plugins.
24 |
25 | ## Units
26 |
27 | ### Massa coin
28 |
29 | Massa coin is the name of the native coin on the Massa blockchain.
30 | Massa coin is expressed as an unsigned 64-bit integer.
31 |
32 | 1 Massa is equivalent to 1,000,000,000 nanoMassa, the smallest unit of Massa coin.
33 |
34 | To simplify usage, the following standard units implemenation are recommended:
35 |
36 | - milliMassa (mMASSA): 1 milliMassa represents 1,000,000 nanoMassa and also 1/1000th of a Massa.
37 | - microMassa (µMASSA): 1 microMassa represents 1,000 nanoMassa and also 1/1,000,000th of a Massa.
38 | - nanoMassa (nMASSA): 1 nanoMassa represents the smallest unit of Massa coin and also 1/1,000,000,000th of a Massa.
39 |
40 | ### Gas
41 |
42 | Gas is a virtual unit used to measure the computational complexity of executing transactions on the Massa blockchain. Each operation requires a certain amount of Gas, and the amount of Gas consumed is proportional to the computational complexity of the operation.
43 | Gas is expressed as an unsigned 64-bit integer.
44 |
45 | 1 gas unit (GAS) is the smallest unit of gas.
46 |
47 | To simplify usage, the following standard units implemenation are recommended:
48 |
49 | - kiloGas (kGAS): 1 kiloGas represents 1,000 gas units.
50 | - megaGas (MGAS): 1 megaGas represents 1,000 kGAS or 1,000,000 gas units.
51 | - gigaGas (GGAS): 1 gigaGas represents 1,000 MGAS or 1,000,000 kGAS or 1,000,000,000 gas units.
52 |
53 | ### Roll
54 |
55 | Roll is a specific token used for staking on the Massa blockchain. A Roll token is required to create and endorse a block on the blockchain. The probability of being drawn to create or endorse a block is correlated to the number of rolls possessed.
56 | Roll is expressed as an unsigned 64-bit integer.
57 |
58 | To simplify usage, the following standard units are recommended:
59 |
60 | - kiloRoll (kROLL): 1 kiloRoll represents 1,000 rolls.
61 | - megaRoll (MROLL): 1 megaRoll represents 1,000 kiloRolls or 1,000,000 rolls.
62 |
63 | ### Transaction Fee
64 |
65 | A transaction fee is a small amount of Massa coin paid by the sender of a transaction to compensate the nodes in the Massa network for validating and executing the transaction.
66 |
67 | Transaction fees are denominated in Massa coin and are paid by the sender in addition to the amount being transacted. The fee amount is determined by the sender and is included in the transaction data.
68 |
69 | Note: Developers should use the same units implementation as for Massa coin values when handling transaction fee values.
70 |
71 | ## Usage
72 |
73 | When interacting with the Massa blockchain and its objects, it is recommended to use the native types provided by the programming language when available. For instance, when working with the Massa coin, an unsigned 64-bit integer can be used to represent the coin's value.
74 |
75 | To make code more readable and avoid errors, it is recommended to use useful constants such as mMASSA or kROLL to express values. For instance, to represent 1 milliMassa, one can write:
76 |
77 | ```text
78 | const amount = 1 * mMASSA;
79 | ```
80 |
81 | In JavaScript, which does not natively support unsigned 64-bit integers, the BigInt type should be used to represent Massa objects such as the Massa coin or Roll tokens. For example, to represent 1 Massa coin, one can write:
82 |
83 | ```javascript
84 | const amount = 1n * mMASSA;
85 | ```
86 |
87 | In programming languages that allow underscores to be used in numbers for better readability, such as TypeScript and Rust, it is recommended to use them when expressing Massa values. For example, to represent 12,500 milliMassa, one can write:
88 |
89 | ```typescript
90 | const amount = 12_500n * mMASSA;
91 | ```
92 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/__tests__/MRC721-internals.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCallStack,
3 | resetStorage,
4 | setDeployContext,
5 | } from '@massalabs/massa-as-sdk';
6 |
7 | import { u256 } from 'as-bignum/assembly';
8 | import {
9 | _approve,
10 | _balanceOf,
11 | _constructor,
12 | _getApproved,
13 | _isApproved,
14 | _isApprovedForAll,
15 | _name,
16 | _ownerOf,
17 | _setApprovalForAll,
18 | _symbol,
19 | _transferFrom,
20 | _update,
21 | } from '../MRC721-internals';
22 |
23 | const tokenAddress = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
24 | const caller = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
25 | const from = 'AU12CzoKEASaeBHnxGLnHDG2u73dLzWWfgvW6bc4L1UfMA5Uc5Fg7';
26 | const to = 'AU178qZCfaNXkz9tQiXJcVfAEnYGJ27UoNtFFJh3BiT8jTfY8P2D';
27 | const approved = 'AU1sF3HSa7fcBoE12bE1Eq2ohKqcRPBHuNRmdqAMfw8WEkHCU3aF';
28 | const newOwner = 'AU12F7y3PWpw72XcwhSksJztRiTSqAvLxaLacP2qDYhNUEfEXuG4T';
29 | const zeroAddress = '';
30 | const tokenId = u256.One;
31 |
32 | const NFTName = 'MASSA_NFT';
33 | const NFTSymbol = 'NFT';
34 |
35 | beforeEach(() => {
36 | resetStorage();
37 | setDeployContext(caller);
38 | _constructor(NFTName, NFTSymbol);
39 | });
40 |
41 | function switchUser(user: string): void {
42 | changeCallStack(user + ' , ' + tokenAddress);
43 | }
44 |
45 | describe('Initialization', () => {
46 | test('get name', () => {
47 | expect(_name()).toBe(NFTName);
48 | });
49 | test('get symbol', () => {
50 | expect(_symbol()).toBe(NFTSymbol);
51 | });
52 | });
53 |
54 | describe('update', () => {
55 | test('mint an nft', () => {
56 | _update(to, tokenId, zeroAddress);
57 | expect(_balanceOf(to)).toBe(tokenId);
58 | expect(_ownerOf(tokenId)).toBe(to);
59 | });
60 | throws('Minting to zero address should fail', () => {
61 | _update(zeroAddress, tokenId, zeroAddress);
62 | });
63 | throws('Minting an already existing tokenId should fail', () => {
64 | _update(to, tokenId, zeroAddress);
65 | _update(to, tokenId, zeroAddress);
66 | });
67 | });
68 |
69 | describe('Approval', () => {
70 | test('approve an address', () => {
71 | _update(caller, tokenId, zeroAddress);
72 | _approve(approved, tokenId);
73 | const _approved = _getApproved(tokenId);
74 | expect(_approved).toBe(approved);
75 | });
76 |
77 | test('check if address is approved', () => {
78 | _update(caller, tokenId, zeroAddress);
79 | _approve(approved, tokenId);
80 | const isApproved = _isApproved(approved, tokenId);
81 | expect(isApproved).toBe(true);
82 | });
83 |
84 | test('Approving zero address should revoke approval', () => {
85 | _update(caller, tokenId, zeroAddress);
86 | _approve(zeroAddress, tokenId);
87 | const _approved = _getApproved(tokenId);
88 | expect(_approved).toBe('');
89 | });
90 |
91 | throws('Approving a token one does not own should fail', () => {
92 | _update(to, tokenId, zeroAddress);
93 | switchUser(from);
94 | _approve(approved, tokenId);
95 | });
96 | });
97 |
98 | describe('Operator Approval', () => {
99 | test('should not be approved for all', () => {
100 | const isApprovedForAll = _isApprovedForAll(caller, to);
101 | expect(isApprovedForAll).toBe(false);
102 | });
103 | test('set approval for all', () => {
104 | _setApprovalForAll(to, true);
105 | const isApprovedForAll = _isApprovedForAll(caller, to);
106 | expect(isApprovedForAll).toBe(true);
107 | });
108 |
109 | test('revoke approval for all', () => {
110 | _setApprovalForAll(to, true);
111 | _setApprovalForAll(to, false);
112 | const isApprovedForAll = _isApprovedForAll(caller, to);
113 | expect(isApprovedForAll).toBe(false);
114 | });
115 | });
116 |
117 | describe('Transferring NFTs', () => {
118 | test('transferFrom with approval succeeds', () => {
119 | _update(caller, tokenId, zeroAddress);
120 | _approve(from, tokenId);
121 | switchUser(from);
122 | _transferFrom(caller, newOwner, tokenId);
123 |
124 | const ownerOfToken = _ownerOf(tokenId);
125 | expect(ownerOfToken).toBe(newOwner);
126 |
127 | const balanceOfNewOwner = _balanceOf(newOwner);
128 | expect(balanceOfNewOwner).toBe(u256.One);
129 |
130 | const balanceOfOldOwner = _balanceOf(from);
131 | expect(balanceOfOldOwner).toBe(u256.Zero);
132 | });
133 | throws('Transferring a non-existent token should fail', () => {
134 | _transferFrom(from, to, tokenId);
135 | });
136 |
137 | throws('Transferring from incorrect owner should fail', () => {
138 | _update(to, tokenId, zeroAddress);
139 | _transferFrom(from, newOwner, tokenId);
140 | });
141 |
142 | throws('Transferring without approval should fail', () => {
143 | _update(caller, tokenId, zeroAddress);
144 | switchUser(from);
145 | _transferFrom(caller, newOwner, tokenId);
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/__tests__/MRC20-burn.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCallStack,
3 | resetStorage,
4 | setDeployContext,
5 | } from '@massalabs/massa-as-sdk';
6 | import {
7 | Args,
8 | bytesToString,
9 | stringToBytes,
10 | u8toByte,
11 | bytesToU256,
12 | u256ToBytes,
13 | } from '@massalabs/as-types';
14 | import {
15 | balanceOf,
16 | totalSupply,
17 | name,
18 | symbol,
19 | decimals,
20 | version,
21 | mrc20Constructor,
22 | increaseAllowance,
23 | transfer,
24 | allowance,
25 | VERSION,
26 | } from '../MRC20';
27 | import { burn, burnFrom } from '../burnable/burn';
28 | import { ownerAddress } from '../../utils/ownership';
29 | import { u256 } from 'as-bignum/assembly';
30 |
31 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
32 |
33 | const user2Address = 'AU12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8';
34 |
35 | const user3Address = 'AUDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBOObs';
36 |
37 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
38 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
39 | function switchUser(user: string): void {
40 | changeCallStack(user + ' , ' + contractAddr);
41 | }
42 |
43 | beforeAll(() => {
44 | resetStorage();
45 | setDeployContext(user1Address);
46 | });
47 |
48 | const TOKEN_NAME = 'BURNABLE_TOKEN';
49 | const TOKEN_SYMBOL = 'BTKN';
50 | const DECIMALS: u8 = 2;
51 | const TOTAL_SUPPLY = new u256(10000, 0, 0, 100);
52 |
53 | describe('ERC20 BURN - Initialization', () => {
54 | mrc20Constructor(TOKEN_NAME, TOKEN_SYMBOL, DECIMALS, TOTAL_SUPPLY);
55 |
56 | test('total supply is properly initialized', () => {
57 | expect(totalSupply([])).toStrictEqual(u256ToBytes(TOTAL_SUPPLY));
58 | });
59 |
60 | test('token name is properly initialized', () => {
61 | expect(name([])).toStrictEqual(stringToBytes(TOKEN_NAME));
62 | });
63 |
64 | test('symbol is properly initialized', () => {
65 | expect(symbol([])).toStrictEqual(stringToBytes(TOKEN_SYMBOL));
66 | });
67 |
68 | test('decimals is properly initialized', () => {
69 | expect(decimals([])).toStrictEqual(u8toByte(DECIMALS));
70 | });
71 |
72 | test('version is properly initialized', () => {
73 | expect(version([])).toStrictEqual(VERSION);
74 | });
75 |
76 | test('owner is properly initialized', () => {
77 | expect(bytesToString(ownerAddress([]))).toStrictEqual(user1Address);
78 | });
79 | });
80 |
81 | const burnAmount = new u256(5000, 0, 1);
82 |
83 | describe('Burn ERC20 to U1', () => {
84 | test('Should burn ERC20', () => {
85 | burn(new Args().add(burnAmount).serialize());
86 |
87 | // check balance of U1
88 | expect(
89 | bytesToU256(balanceOf(new Args().add(user1Address).serialize())),
90 | // @ts-ignore
91 | ).toBe(TOTAL_SUPPLY - burnAmount);
92 |
93 | // check totalSupply update
94 | expect(totalSupply([])).toStrictEqual(
95 | // @ts-ignore
96 | u256ToBytes(TOTAL_SUPPLY - burnAmount),
97 | );
98 | });
99 | });
100 |
101 | describe('Fails burn ERC20', () => {
102 | throws('Fails to burn because of underflow ', () =>
103 | burn(new Args().add(u256.Max).serialize()),
104 | );
105 | });
106 |
107 | const allowAmount = new u256(1, 1, 1, 1);
108 | describe('burnFrom', () => {
109 | beforeAll(() => {
110 | switchUser(user3Address);
111 |
112 | // Increase allowance for U1 to spend U3 tokens
113 | increaseAllowance(
114 | new Args().add(user1Address).add(allowAmount).serialize(),
115 | );
116 | switchUser(user1Address);
117 | });
118 |
119 | throws('on insufficient allowance ', () => {
120 | burnFrom(
121 | new Args()
122 | .add(user3Address)
123 | // @ts-ignore
124 | .add(allowAmount + u256.One)
125 | .serialize(),
126 | );
127 | });
128 |
129 | throws('on insufficient balance', () =>
130 | burnFrom(new Args().add(user2Address).add(allowAmount).serialize()),
131 | );
132 |
133 | test('should burn tokens from an other address', () => {
134 | const u1balanceBefore = balanceOf(new Args().add(user1Address).serialize());
135 | const u3balanceBefore = balanceOf(new Args().add(user3Address).serialize());
136 |
137 | transfer(new Args().add(user3Address).add(allowAmount).serialize());
138 |
139 | burnFrom(new Args().add(user3Address).add(allowAmount).serialize());
140 |
141 | // Check balance changes
142 | expect(balanceOf(new Args().add(user1Address).serialize())).toStrictEqual(
143 | // @ts-ignore
144 | u256ToBytes(bytesToU256(u1balanceBefore) - allowAmount),
145 | );
146 |
147 | expect(balanceOf(new Args().add(user3Address).serialize())).toStrictEqual(
148 | u3balanceBefore,
149 | );
150 |
151 | // Verify allowances after transferFrom
152 | expect(
153 | allowance(new Args().add(user1Address).add(user3Address).serialize()),
154 | ).toStrictEqual(u256ToBytes(u256.Zero));
155 | });
156 | });
157 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/deployer/README.md:
--------------------------------------------------------------------------------
1 | # Smart Contract Deployer Documentation
2 |
3 | ## Overview
4 |
5 | The `deployer.ts` smart contract is designed to facilitate the deployment of multiple smart contracts in a single operation. It automates the process of deploying smart contracts, calling their constructor functions (if available), and passing the necessary arguments and coins. This is achieved by leveraging the operation datastore to store and retrieve the required information for each contract.
6 | This smart contract is aimed to be run in an ExecuteSC operation.
7 | The deployed contract address(es) are retrived from execution events.
8 |
9 | ## How It Works
10 |
11 | The deployer smart contract reads the information about the contracts to be deployed from the operation datastore. For each contract, it performs the following steps:
12 |
13 | 1. Retrieves the bytecode of the smart contract.
14 | 2. Deploys the smart contract.
15 | 3. Checks if the smart contract has a `constructor` function.
16 | 4. If a `constructor` function exists, it calls the function with the provided arguments and coins.
17 | 5. Generates an event indicating the address of the deployed contract.
18 | 6. Collects the addresses of all deployed contracts and generates a raw event containing this list.
19 |
20 | ## Operation Datastore Structure
21 |
22 | The operation datastore is used to store the information required for deploying the smart contracts. The structure is as follows:
23 |
24 | - **Key `[0]`**: Contains the number of smart contracts to deploy.
25 | - **Key `[x, x, x, x]`**: Contains the bytecode of each smart contract. The 4 bytes of the key represent the index of the contract in the list.
26 | - **Key `[x, x, x, x, 0, 0, 0, 0]`**: (Optional) Contains the arguments for the constructor function of each contract.
27 | - **Key `[x, x, x, x, 0, 0, 0, 1]`**: (Optional) Contains the coins to be sent to the constructor function of each contract.
28 |
29 | ## Functions
30 |
31 | ### `main(_: StaticArray): void`
32 |
33 | The entry point of the deployer smart contract. It deploys all the smart contracts and calls their constructors if available.
34 |
35 | - **Parameters**: `_` (not used)
36 | - **Behavior**:
37 | - Retrieves the number of contracts to deploy.
38 | - Iterates through each contract, deploying it and calling its constructor if applicable.
39 | - Generates events for each deployed contract and a raw event with the list of all deployed contract addresses.
40 |
41 | ---
42 |
43 | ### `getNbSC(): u64`
44 |
45 | Retrieves the number of smart contracts to deploy.
46 |
47 | - **Returns**: The number of smart contracts to deploy.
48 | - **Throws**: If the number of smart contracts is not defined in the datastore.
49 |
50 | ---
51 |
52 | ### `getScByteCode(i: u64): StaticArray`
53 |
54 | Retrieves the bytecode of the smart contract at the specified index.
55 |
56 | - **Parameters**: `i` - The index of the smart contract.
57 | - **Returns**: The bytecode of the smart contract.
58 | - **Throws**: If the bytecode is not defined in the datastore.
59 |
60 | ---
61 |
62 | ### `argsKey(i: u64): StaticArray`
63 |
64 | Generates the key for retrieving the constructor arguments of the smart contract at the specified index.
65 |
66 | - **Parameters**: `i` - The index of the smart contract.
67 | - **Returns**: The key for the constructor arguments.
68 |
69 | ---
70 |
71 | ### `getConstructorArgs(i: u64): Args`
72 |
73 | Retrieves the arguments for the constructor function of the smart contract at the specified index.
74 |
75 | - **Parameters**: `i` - The index of the smart contract.
76 | - **Returns**: The arguments for the constructor function.
77 |
78 | ---
79 |
80 | ### `coinsKey(i: u64): StaticArray`
81 |
82 | Generates the key for retrieving the coins to be sent to the constructor function of the smart contract at the specified index.
83 |
84 | - **Parameters**: `i` - The index of the smart contract.
85 | - **Returns**: The key for the coins.
86 |
87 | ---
88 |
89 | ### `getCoins(i: u64): u64`
90 |
91 | Retrieves the amount of coins to be sent to the constructor function of the smart contract at the specified index.
92 |
93 | - **Parameters**: `i` - The index of the smart contract.
94 | - **Returns**: The amount of coins to send.
95 |
96 | ## Events
97 |
98 | - **Contract Deployment Event**: For each deployed contract, an event is generated with the address of the deployed contract.
99 | - **Raw Event**: A raw event is generated containing the list of all deployed contract addresses.
100 |
101 | ## Usage
102 |
103 | To use the deployer smart contract, you need to:
104 |
105 | 1. Populate the operation datastore with the required information (number of contracts, bytecode, constructor arguments, and coins).
106 | 2. Execute the deployer smart contract using an `ExecuteSC` operation.
107 | 3. Monitor the events to retrieve the addresses of the deployed contracts.
108 |
109 | ## Massa-web3
110 | This contract is fully integrated in [@massalabs/massa-web3](https://github.com/massalabs/massa-web3) typescript library, and can is currently used to deploy Smart contracts on Massa
111 |
112 | ## Remarks
113 |
114 | - The deployer smart contract ensures that all contracts are deployed and initialized in a single transaction.
115 | - If any required information is missing in the datastore, the deployment will fail with an appropriate error message.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Massa Standard Definition
2 |
3 | Welcome to the Massa Standard Definition project!
4 |
5 | This project aims to establish a common set of standards for the Massa blockchain ecosystem. The standards defined here will help to promote interoperability and ease of use for Massa-based applications and services.
6 |
7 | If you're interested in learning more about Massa and its capabilities, check out the following resources:
8 |
9 | - [Massa website](https://massa.net): This is the official website for the Massa blockchain. Here, you can learn more about Massa's features and use cases, as well as explore the Massa ecosystem and community.
10 | - [Massa documentation](https://docs.massa.net/): This is the official documentation for Massa. Here, you can find detailed guides and tutorials for developing on the Massa blockchain, as well as API reference documentation for the Massa SDK and other tools.
11 |
12 | ## Fungible Token
13 |
14 | The [Fungible Token standard implementation](smart-contracts/assembly/contracts/MRC20) defines a common set of rules for creating and managing Massa-based tokens that are fungible (i.e. interchangeable).
15 |
16 | This is MassaLabs implementation of [the ERC20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/).
17 |
18 | ## Non-Fungible Token
19 |
20 | The [Non-Fungible Token standard implementation](smart-contracts/assembly/contracts/MRC721) defines a common set of rules for creating and managing Massa-based tokens that are non-fungible (i.e. unique).
21 |
22 | This is MassaLabs implementation of [the ERC721](https://ethereum.org/en/developers/docs/standards/tokens/erc-721/).
23 |
24 | ## Massa Domain Name Service
25 |
26 | The [Massa Domain Name Service standard](smart-contracts/assembly/contracts/dns/dns.ts) defines a common set of rules for creating and managing Massa-based domain names.
27 |
28 | This is MassaLabs implementation of [the ENS](https://docs.ens.domains/).
29 |
30 | ## MRC1155 Token
31 |
32 | The [MRC1155](smart-contracts/assembly/contracts/MRC1155/) standard defines a common set of rules for creating and managing Massa-based tokens that can represent multiple types of assets, both fungible and non-fungible, within a single contract.
33 |
34 | This is MassaLabs implementation of [the ERC1155](https://eips.ethereum.org/EIPS/eip-1155).
35 |
36 | ## Contract utils
37 |
38 | ### Deployer
39 |
40 | The [Deployer smart contract](smart-contracts/assembly/contracts/deployer/deployer.ts) automates the deployment of multiple smart contracts in a single operation. It retrieves the necessary information (bytecode, constructor arguments, and coins) from the operation datastore, deploys the contracts, and calls their constructor functions if available.
41 |
42 | This utility simplifies the process of deploying and initializing multiple smart contracts in a single transaction and allow to call a constructor function in the deployed contract(s).
43 |
44 | For more details, refer to the [Deployer Documentation](smart-contracts/assembly/contracts/deployer/README.md).
45 |
46 | ### Ownable
47 |
48 | The [Ownable standard implementation](smart-contracts/assembly/contracts/utils/ownership.ts) defines a common set of rules for ownership management of Massa-based contracts. It provides basic authorization control functions, simplifying the implementation of user permissions.
49 |
50 | This is MassaLabs implementation of [the Ownable pattern](https://docs.openzeppelin.com/contracts/4.x/access-control#ownership-and-ownable).
51 |
52 | ### Role-Based Access Control
53 |
54 | The [Role-Based Access Control standard implementation](smart-contracts/assembly/contracts/utils/accessControl.ts) defines a common set of rules for managing permissions using roles. It allows for the assignment of specific permissions to different roles, which can then be granted to users or other entities.
55 |
56 | This is MassaLabs implementation of [the RBAC pattern](https://docs.openzeppelin.com/contracts/4.x/access-control#role-based-access-control).
57 |
58 | ### Multicall
59 |
60 | The [Multicall contract](smart-contracts/assembly/contracts/multicall) enables batching multiple smart contract calls into a single operation. It reads a list of calls from the operation datastore, executes them sequentially, and returns the results as a serialized array.
61 | It can be used to batch readonly calls, write calls or both.
62 |
63 | **Important:**
64 | The multicall contract is designed to be used in an `ExecuteSC` context. It is not intended to be deployed as a persistent contract on-chain. Instead, it should be executed directly via an `ExecuteSC` operation, where the calls and their parameters are provided through the operation datastore.
65 |
66 | This utility is useful for reducing the number of transactions and for atomic execution of multiple contract calls within a single operation.
67 |
68 | **Important:**
69 | Due to current node limitation, multicall read cannot be achieved because the returnet data of main function is ignored
70 | see: https://github.com/massalabs/massa/issues/4913
71 |
72 | ## Massa Units
73 |
74 | The [Massa Units standard](units.md) defines a set of common units of measurement for use on the Massa blockchain.
75 |
76 | These units include:
77 |
78 | - Massa coin
79 | - Gas
80 | - Rolls
81 |
82 | ## DApps <> Wallet Provider Communication
83 |
84 | The [DApps <> Wallet Provider Communication standard](wallet/dapps-communication.md) defines a common interface for communication between Massa-based decentralized applications (DApps) and wallet providers.
85 |
86 | This standard aims to simplify the process of integrating Massa-based DApps with various wallet providers, making it easier for end-users to access and use these applications.
87 |
88 | ## Contributing
89 |
90 | To contribute to the Massa Standard Definition project, please refer to the document [contributing](CONTRIBUTING.md).
91 |
92 | ## License
93 |
94 | This project is licensed under the MIT license. For more information, please refer to the [LICENSE file](LICENCE).
95 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/enumerable/MRC721Enumerable.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is an example of an MRC721 contract that uses the `MRC721Enumerable-internals`
3 | * helper functions to implement enumeration functionality similar to the ERC-721 Enumerable extension.
4 | *
5 | * @remarks
6 | *
7 | * **Note:** We have diverged from the ERC-721 Enumerable standard in this implementation.
8 | * On the Massa blockchain, indices are not necessary for token enumeration because we can directly access
9 | * the storage and structure data in a way that allows developers to easily retrieve the needed values.
10 | *
11 | * Instead of maintaining explicit arrays or mappings of token IDs and owner tokens by index,
12 | * we utilize key prefix querying to retrieve all token IDs and tokens owned by specific addresses.
13 | * This approach leverages Massa's storage capabilities for efficient data access and simplifies the contract logic.
14 | *
15 | * **Benefits of This Approach:**
16 | * - **Reduced Storage Costs:** Eliminates the need for additional storage structures to maintain indices.
17 | * - **Simplified Logic:** Streamlines the process of token enumeration, making the contract easier to maintain.
18 | *
19 | * **Underlying Storage Structure:**
20 | *
21 | * We store important information in the following formats:
22 | *
23 | * - **Total Supply:**
24 | *
25 | * [TOTAL_SUPPLY_KEY] = totalSupply
26 | * - `TOTAL_SUPPLY_KEY`: A constant key for the total supply of tokens.
27 | * - `totalSupply`: A `u256` value representing the total number of tokens in existence.
28 | *
29 | * - **Owned Tokens:**
30 | *
31 | * [OWNED_TOKENS_KEY][owner][tokenId] = tokenId
32 | * - `OWNED_TOKENS_KEY`: A constant prefix for all owned tokens.
33 | * - `owner`: The owner's address.
34 | * - `tokenId`: The token ID.
35 | * - The value `tokenId` is stored to facilitate easy retrieval.
36 | *
37 | * **Retrieving Data Using Key Prefixes:**
38 | *
39 | * We utilize the `getKeys` function from the `massa-as-sdk`, which allows us to retrieve all keys that start with a
40 | * specific prefix. This enables us to:
41 | * - Retrieve all tokens owned by a specific address by querying keys with the prefix `[OWNED_TOKENS_KEY][owner]`.
42 | * - Retrieve all existing token IDs by querying keys with the appropriate prefix if needed.
43 | *
44 | * **Key Points:**
45 | * - The `getKeys` function from the `massa-as-sdk` allows us to filter storage keys by a given prefix,
46 | * enabling efficient data retrieval.
47 | *
48 | * **This file does two things:**
49 | * 1. It wraps the `MRC721Enumerable-internals` functions, manages the deserialization/serialization of the arguments
50 | * and return values, and exposes them to the outside world.
51 | * 2. It implements some custom features that are not part of the ERC-721 standard,
52 | * such as `mint`, `burn`, or ownership management.
53 | *
54 | * **Important:** The `MRC721Enumerable-internals` functions are not supposed to be re-exported by this file.
55 | */
56 |
57 | import { Args, u256ToBytes } from '@massalabs/as-types';
58 | import {
59 | _constructor,
60 | _update,
61 | _transferFrom,
62 | _totalSupply,
63 | } from './MRC721Enumerable-internals';
64 | import { onlyOwner } from '../../utils/ownership';
65 | import { _setOwner } from '../../utils/ownership-internal';
66 | import { Context, isDeployingContract } from '@massalabs/massa-as-sdk';
67 |
68 | /**
69 | * @param name - the name of the NFT
70 | * @param symbol - the symbol of the NFT
71 | * @remarks You must call this function in your contract's constructor or re-write it to fit your needs !
72 | */
73 | export function mrc721Constructor(name: string, symbol: string): void {
74 | assert(isDeployingContract());
75 | _constructor(name, symbol);
76 | _setOwner(Context.caller().toString());
77 | }
78 |
79 | /**
80 | *
81 | * @param binaryArgs - serialized arguments representing the address of the sender,
82 | * the address of the recipient, and the tokenId to transfer.
83 | *
84 | * @remarks This function is only callable by the owner of the tokenId or an approved operator.
85 | */
86 | export function transferFrom(binaryArgs: StaticArray): void {
87 | const args = new Args(binaryArgs);
88 | const from = args.nextString().expect('from argument is missing or invalid');
89 | const to = args.nextString().expect('to argument is missing or invalid');
90 | const tokenId = args
91 | .nextU256()
92 | .expect('tokenId argument is missing or invalid');
93 | _transferFrom(from, to, tokenId);
94 | }
95 |
96 | /**
97 | *
98 | * @param binaryArgs - serialized arguments representing the address of the recipient and the tokenId to mint
99 | *
100 | * @remarks This function is only callable by the owner of the contract.
101 | */
102 | export function mint(binaryArgs: StaticArray): void {
103 | onlyOwner();
104 | const args = new Args(binaryArgs);
105 | const to = args.nextString().expect('to argument is missing or invalid');
106 | const tokenId = args
107 | .nextU256()
108 | .expect('tokenId argument is missing or invalid');
109 | _update(to, tokenId, '');
110 | }
111 |
112 | /**
113 | *
114 | * @param binaryArgs - serialized u256 representing the tokenId to burn
115 | *
116 | * @remarks This function is not part of the ERC721 standard.
117 | * It serves as an example of how to use the NFT-enumerable-internals functions to implement custom features.
118 | */
119 | export function burn(binaryArgs: StaticArray): void {
120 | const args = new Args(binaryArgs);
121 | const tokenId = args
122 | .nextU256()
123 | .expect('tokenId argument is missing or invalid');
124 | _update('', tokenId, '');
125 | }
126 |
127 | /**
128 | * Returns the total number of tokens.
129 | * @returns a serialized u256 representing the total supply
130 | */
131 | export function totalSupply(_: StaticArray): StaticArray {
132 | return u256ToBytes(_totalSupply());
133 | }
134 |
135 | /**
136 | * Expose the ownerAddress function to allow checking the owner of the contract.
137 | */
138 | export { ownerAddress } from '../../utils/ownership';
139 | /**
140 | * Expose non-modified functions from MRC721.
141 | */
142 | export {
143 | name,
144 | symbol,
145 | balanceOf,
146 | ownerOf,
147 | getApproved,
148 | isApprovedForAll,
149 | approve,
150 | setApprovalForAll,
151 | } from '../MRC721';
152 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/__tests__/burnable.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCallStack,
3 | resetStorage,
4 | setDeployContext,
5 | } from '@massalabs/massa-as-sdk';
6 | import {
7 | Args,
8 | stringToBytes,
9 | u256ToBytes,
10 | fixedSizeArrayToBytes,
11 | boolToByte,
12 | } from '@massalabs/as-types';
13 | import {
14 | balanceOf,
15 | balanceOfBatch,
16 | mrc1155Constructor,
17 | setApprovalForAll,
18 | } from '../MRC1155';
19 | import { u256 } from 'as-bignum/assembly';
20 | import { _mint, _mintBatch } from '../MRC1155-internal';
21 | import { burn, burnBatch } from '../burnable';
22 |
23 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
24 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
25 |
26 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
27 |
28 | const user2Address = 'AU12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8';
29 |
30 | const TOKEN_URI = 'ipfs://QmW77ZQQ7Jm9q8WuLbH8YZg2K7T9Qnjbzm7jYVQQrJY5Yd';
31 |
32 | function switchUser(user: string): void {
33 | changeCallStack(user + ' , ' + contractAddr);
34 | }
35 |
36 | beforeEach(() => {
37 | switchUser(user1Address);
38 | resetStorage();
39 | setDeployContext(user1Address);
40 | mrc1155Constructor(TOKEN_URI);
41 | });
42 |
43 | describe('burn', () => {
44 | test('should burn tokens', () => {
45 | const id = u256.One;
46 | const value = u256.from(10);
47 | const data = stringToBytes('burn data');
48 | _mint(user1Address, id, value, data);
49 |
50 | expect(
51 | balanceOf(
52 | new Args().add(stringToBytes(user1Address)).add(id).serialize(),
53 | ),
54 | ).toStrictEqual(u256ToBytes(value));
55 |
56 | burn(
57 | new Args()
58 | .add(stringToBytes(user1Address))
59 | .add(id)
60 | .add(value)
61 | .serialize(),
62 | );
63 | expect(
64 | balanceOf(
65 | new Args().add(stringToBytes(user1Address)).add(id).serialize(),
66 | ),
67 | ).toStrictEqual(u256ToBytes(u256.Zero));
68 | });
69 |
70 | test('should burn tokens with approval', () => {
71 | const id = u256.One;
72 | const value = u256.from(10);
73 | const data = stringToBytes('burn data');
74 | _mint(user1Address, id, value, data);
75 | expect(
76 | balanceOf(
77 | new Args().add(stringToBytes(user1Address)).add(id).serialize(),
78 | ),
79 | ).toStrictEqual(u256ToBytes(value));
80 |
81 | setApprovalForAll(
82 | new Args()
83 | .add(stringToBytes(user2Address))
84 | .add(boolToByte(true))
85 | .serialize(),
86 | );
87 |
88 | switchUser(user2Address);
89 | burn(
90 | new Args()
91 | .add(stringToBytes(user1Address))
92 | .add(id)
93 | .add(value)
94 | .serialize(),
95 | );
96 | expect(
97 | balanceOf(
98 | new Args().add(stringToBytes(user1Address)).add(id).serialize(),
99 | ),
100 | ).toStrictEqual(u256ToBytes(u256.Zero));
101 | });
102 |
103 | throws('MRC1155MissingApprovalForAll', () => {
104 | const id = u256.One;
105 | const value = u256.from(10);
106 | const data = stringToBytes('burn data');
107 | _mint(user1Address, id, value, data);
108 | expect(
109 | balanceOf(
110 | new Args().add(stringToBytes(user1Address)).add(id).serialize(),
111 | ),
112 | ).toStrictEqual(u256ToBytes(value));
113 |
114 | switchUser(user2Address);
115 | burn(
116 | new Args()
117 | .add(stringToBytes(user1Address))
118 | .add(id)
119 | .add(value)
120 | .serialize(),
121 | );
122 | });
123 | });
124 |
125 | describe('burnBatch', () => {
126 | test('should burn batch of tokens', () => {
127 | const ids = [u256.One, u256.from(2), u256.from(3)];
128 | const values = [u256.from(10), u256.from(20), u256.from(30)];
129 | const data = stringToBytes('burn data');
130 | _mintBatch(user1Address, ids, values, data);
131 | expect(
132 | balanceOfBatch(
133 | new Args()
134 | .add([user1Address, user1Address, user1Address])
135 | .add(ids)
136 | .serialize(),
137 | ),
138 | ).toStrictEqual(fixedSizeArrayToBytes(values));
139 |
140 | burnBatch(new Args().add(user1Address).add(ids).add(values).serialize());
141 | expect(
142 | balanceOfBatch(
143 | new Args()
144 | .add([user1Address, user1Address, user1Address])
145 | .add(ids)
146 | .serialize(),
147 | ),
148 | ).toStrictEqual(
149 | fixedSizeArrayToBytes([u256.Zero, u256.Zero, u256.Zero]),
150 | );
151 | });
152 |
153 | test('should burn batch of tokens with approval', () => {
154 | const ids = [u256.One, u256.from(2), u256.from(3)];
155 | const values = [u256.from(10), u256.from(20), u256.from(30)];
156 | const data = stringToBytes('burn data');
157 | _mintBatch(user1Address, ids, values, data);
158 | expect(
159 | balanceOfBatch(
160 | new Args()
161 | .add([user1Address, user1Address, user1Address])
162 | .add(ids)
163 | .serialize(),
164 | ),
165 | ).toStrictEqual(fixedSizeArrayToBytes(values));
166 |
167 | setApprovalForAll(
168 | new Args()
169 | .add(stringToBytes(user2Address))
170 | .add(boolToByte(true))
171 | .serialize(),
172 | );
173 |
174 | switchUser(user2Address);
175 | burnBatch(new Args().add(user1Address).add(ids).add(values).serialize());
176 | expect(
177 | balanceOfBatch(
178 | new Args()
179 | .add([user1Address, user1Address, user1Address])
180 | .add(ids)
181 | .serialize(),
182 | ),
183 | ).toStrictEqual(
184 | fixedSizeArrayToBytes([u256.Zero, u256.Zero, u256.Zero]),
185 | );
186 | });
187 |
188 | throws('MRC1155MissingApprovalForAll', () => {
189 | const ids = [u256.One, u256.from(2), u256.from(3)];
190 | const values = [u256.from(10), u256.from(20), u256.from(30)];
191 | const data = stringToBytes('burn data');
192 | _mintBatch(user1Address, ids, values, data);
193 | expect(
194 | balanceOfBatch(
195 | new Args()
196 | .add([user1Address, user1Address, user1Address])
197 | .add(ids)
198 | .serialize(),
199 | ),
200 | ).toStrictEqual(fixedSizeArrayToBytes(values));
201 |
202 | switchUser(user2Address);
203 | burnBatch(new Args().add(user1Address).add(ids).add(values).serialize());
204 | });
205 | });
206 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/enumerable/MRC721Enumerable-internals.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provides internal functions to support token enumeration functionality for the MRC721 contract on Massa.
3 | * It utilizes key prefix querying to retrieve all token IDs and tokens owned by specific addresses
4 | * in the datastore without maintaining explicit indices.
5 | */
6 |
7 | import { Context, Storage } from '@massalabs/massa-as-sdk';
8 | import { u256 } from 'as-bignum/assembly';
9 | import { bytesToU256, stringToBytes, u256ToBytes } from '@massalabs/as-types';
10 | import {
11 | _isAuthorized,
12 | _ownerOf,
13 | ownerKey,
14 | _update as _updateBase,
15 | _constructor as _constructorBase,
16 | } from '../MRC721-internals';
17 | export const TOTAL_SUPPLY_KEY: StaticArray = stringToBytes('totalSupply');
18 | export const OWNED_TOKENS_KEY: StaticArray = stringToBytes('ownedTokens');
19 |
20 | /**
21 | * Constructs a new MRC721 contract.
22 | * @param binaryArgs - the binary arguments name and symbol
23 | *
24 | * @remarks This function shouldn't be directly exported by the implementation contract.
25 | * It is meant to be called by the constructor of the implementation contract.
26 | * Please check the MRC721Enumerable.ts file for an example of how to use this function.
27 | */
28 | export function _constructor(name: string, symbol: string): void {
29 | _constructorBase(name, symbol);
30 | Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(u256.Zero));
31 | }
32 |
33 | /* -------------------------------------------------------------------------- */
34 | /* TOTAL SUPPLY */
35 | /* -------------------------------------------------------------------------- */
36 |
37 | /**
38 | * Returns the total number of tokens in existence.
39 | */
40 | export function _totalSupply(): u256 {
41 | return bytesToU256(Storage.get(TOTAL_SUPPLY_KEY));
42 | }
43 |
44 | /**
45 | * Increases the total supply by the given delta.
46 | * @param delta - The amount to increase the total supply by.
47 | *
48 | * @throws Will throw an error if the addition of delta to currentSupply exceeds u256.Max.
49 | */
50 | export function _increaseTotalSupply(delta: u256): void {
51 | const currentSupply = _totalSupply();
52 | // @ts-ignore
53 | const maxAllowedDelta = u256.Max - currentSupply;
54 | // @ts-ignore
55 | assert(u256.le(delta, maxAllowedDelta), 'Total supply overflow');
56 | // @ts-ignore
57 | const newSupply = currentSupply + delta;
58 | Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(newSupply));
59 | }
60 |
61 | /**
62 | * Decreases the total supply by the given delta.
63 | * @param delta - The amount to decrease the total supply by.
64 | *
65 | * @throws Will throw an error if `delta` exceeds the current total supply, causing an underflow.
66 | */
67 | export function _decreaseTotalSupply(delta: u256): void {
68 | const currentSupply = _totalSupply();
69 | assert(u256.le(delta, currentSupply), 'Total supply underflow');
70 | // @ts-ignore
71 | const newSupply = currentSupply - delta;
72 | // @ts-ignore
73 | Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(newSupply));
74 | }
75 |
76 | /* -------------------------------------------------------------------------- */
77 | /* OWNED TOKENS */
78 | /* -------------------------------------------------------------------------- */
79 |
80 | /**
81 | * Returns the key prefix for the owned tokens of an owner.
82 | * @param owner - The owner's address.
83 | */
84 | export function _getOwnedTokensKeyPrefix(owner: string): StaticArray {
85 | return OWNED_TOKENS_KEY.concat(stringToBytes(owner));
86 | }
87 |
88 | /**
89 | * Adds a token to the owner's list of tokens.
90 | * @param owner - The owner's address.
91 | * @param tokenId - The token ID to add.
92 | */
93 | function _addTokenToOwnerEnumeration(owner: string, tokenId: u256): void {
94 | const key = _getOwnedTokensKeyPrefix(owner).concat(u256ToBytes(tokenId));
95 | Storage.set(key, []);
96 | }
97 |
98 | /**
99 | * Removes a token from the owner's list of tokens.
100 | * @param owner - The owner's address.
101 | * @param tokenId - The token ID to remove.
102 | */
103 | function _removeTokenFromOwnerEnumeration(owner: string, tokenId: u256): void {
104 | const key = _getOwnedTokensKeyPrefix(owner).concat(u256ToBytes(tokenId));
105 | Storage.del(key);
106 | }
107 |
108 | /* -------------------------------------------------------------------------- */
109 | /* UPDATE */
110 | /* -------------------------------------------------------------------------- */
111 |
112 | /**
113 | * Updates the token ownership and enumerations.
114 | * @param to - The address to transfer the token to.
115 | * @param tokenId - The token ID.
116 | * @param auth - The address authorized to perform the update.
117 | */
118 | export function _update(to: string, tokenId: u256, auth: string): void {
119 | const previousOwner = _updateBase(to, tokenId, auth);
120 |
121 | // Mint
122 | if (previousOwner == '') {
123 | _addTokenToOwnerEnumeration(to, tokenId);
124 | _increaseTotalSupply(u256.One);
125 | } else {
126 | // Transfer
127 | if (to != '' && to != previousOwner) {
128 | _removeTokenFromOwnerEnumeration(previousOwner, tokenId);
129 | _addTokenToOwnerEnumeration(to, tokenId);
130 | }
131 | // Burn
132 | else if (to == '') {
133 | _removeTokenFromOwnerEnumeration(previousOwner, tokenId);
134 | _decreaseTotalSupply(u256.One);
135 | }
136 | }
137 | }
138 |
139 | /* -------------------------------------------------------------------------- */
140 | /* EXPORT NECESSARY FUNCTIONS */
141 | /* -------------------------------------------------------------------------- */
142 |
143 | /**
144 | * Transfers a token from one address to another.
145 | * @param from - The current owner's address.
146 | * @param to - The new owner's address.
147 | * @param tokenId - The token ID to transfer.
148 | */
149 | export function _transferFrom(from: string, to: string, tokenId: u256): void {
150 | assert(
151 | _isAuthorized(Context.caller().toString(), tokenId),
152 | 'Unauthorized caller',
153 | );
154 | assert(from == _ownerOf(tokenId), 'Unauthorized from');
155 | assert(to != '', 'Unauthorized to');
156 | assert(Storage.has(ownerKey(tokenId)), 'Nonexistent token');
157 | _update(to, tokenId, from);
158 | }
159 |
160 | export {
161 | _approve,
162 | _balanceOf,
163 | _getApproved,
164 | _isApprovedForAll,
165 | _name,
166 | _ownerOf,
167 | _setApprovalForAll,
168 | _symbol,
169 | } from '../MRC721-internals';
170 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/wrapper.ts:
--------------------------------------------------------------------------------
1 | import { Address, call } from '@massalabs/massa-as-sdk';
2 | import {
3 | Args,
4 | NoArg,
5 | bytesToU256,
6 | bytesToString,
7 | byteToU8,
8 | } from '@massalabs/as-types';
9 | import { u256 } from 'as-bignum/assembly';
10 |
11 | /**
12 | * The Massa's standard token implementation wrapper.
13 | *
14 | * This class can be used to wrap a smart contract implementing
15 | * Massa standard token.
16 | * All the serialization/deserialization will handled here.
17 | *
18 | * ```typescript
19 | * const coin = new MRC20Wrapper(scAddress);
20 | * const coinName = coin.name();
21 | * const bal = coin.balanceOf(myAddress);
22 | * console.log(`balance: ${bal.value.toString()} of token: ${coinName}`);
23 | * ```
24 | */
25 | export class MRC20Wrapper {
26 | _origin: Address;
27 |
28 | /**
29 | * Wraps a smart contract exposing standard token FFI.
30 | *
31 | * @param at - Address of the smart contract. -
32 | */
33 | constructor(at: Address) {
34 | this._origin = at;
35 | }
36 |
37 | /**
38 | * Initializes the smart contract.
39 | *
40 | * @param name - Name of the token.
41 | * @param symbol - Symbol of the token.
42 | * @param decimals - Number of decimals of the token.
43 | * @param supply - Initial supply of the token.
44 | * @param coins - Number of coins to send to the smart contract.
45 | */
46 | init(
47 | name: string,
48 | symbol: string,
49 | decimals: u8,
50 | supply: u256,
51 | coins: u64 = 0,
52 | ): void {
53 | const args = new Args().add(name).add(symbol).add(decimals).add(supply);
54 | call(this._origin, 'constructor', args, coins);
55 | }
56 |
57 | /**
58 | * Returns the version of the smart contract.
59 | * This versioning is following the best practices defined in https://semver.org/.
60 | *
61 | * @returns
62 | */
63 | version(): string {
64 | return bytesToString(call(this._origin, 'version', NoArg, 0));
65 | }
66 |
67 | /**
68 | * Returns the name of the token.
69 | *
70 | * @returns name of the token.
71 | */
72 | name(): string {
73 | return bytesToString(call(this._origin, 'name', NoArg, 0));
74 | }
75 |
76 | /** Returns the symbol of the token.
77 | *
78 | * @returns token symbol.
79 | */
80 | symbol(): string {
81 | return bytesToString(call(this._origin, 'symbol', NoArg, 0));
82 | }
83 |
84 | /**
85 | * Returns the number of decimals of the token.
86 | *
87 | * @returns number of decimals.
88 | */
89 | decimals(): u8 {
90 | const res = call(this._origin, 'decimals', NoArg, 0);
91 | return byteToU8(res);
92 | }
93 |
94 | /**
95 | * Returns the total token supply.
96 | *
97 | * The number of tokens that were initially minted.
98 | *
99 | * @returns number of minted tokens.
100 | */
101 | totalSupply(): u256 {
102 | return bytesToU256(call(this._origin, 'totalSupply', NoArg, 0));
103 | }
104 |
105 | /**
106 | * Returns the balance of an account.
107 | *
108 | * @param account -
109 | */
110 | balanceOf(account: Address): u256 {
111 | return bytesToU256(
112 | call(this._origin, 'balanceOf', new Args().add(account), 0),
113 | );
114 | }
115 |
116 | /**
117 | * Transfers tokens from the caller's account to the recipient's account.
118 | *
119 | * @param toAccount -
120 | * @param nbTokens -
121 | * @param coins -
122 | */
123 | transfer(toAccount: Address, nbTokens: u256, coins: u64 = 0): void {
124 | call(
125 | this._origin,
126 | 'transfer',
127 | new Args().add(toAccount).add(nbTokens),
128 | coins,
129 | );
130 | }
131 |
132 | /**
133 | * Returns the allowance set on the owner's account for the spender.
134 | *
135 | * @param ownerAccount -
136 | * @param spenderAccount -
137 | */
138 | allowance(ownerAccount: Address, spenderAccount: Address): u256 {
139 | return bytesToU256(
140 | call(
141 | this._origin,
142 | 'allowance',
143 | new Args().add(ownerAccount).add(spenderAccount),
144 | 0,
145 | ),
146 | );
147 | }
148 |
149 | /**
150 | * Increases the allowance of the spender on the owner's account
151 | * by the given amount.
152 | *
153 | * This function can only be called by the owner.
154 | *
155 | * @param spenderAccount -
156 | * @param nbTokens -
157 | * @param coins -
158 | */
159 | increaseAllowance(
160 | spenderAccount: Address,
161 | nbTokens: u256,
162 | coins: u64 = 0,
163 | ): void {
164 | call(
165 | this._origin,
166 | 'increaseAllowance',
167 | new Args().add(spenderAccount).add(nbTokens),
168 | coins,
169 | );
170 | }
171 |
172 | /**
173 | * Decreases the allowance of the spender on the owner's account
174 | * by the given amount.
175 | *
176 | * This function can only be called by the owner.
177 | * Coins is left to zero as this function does not need storage entry creation.
178 | *
179 | * @param spenderAccount -
180 | * @param nbTokens -
181 | */
182 | decreaseAllowance(spenderAccount: Address, nbTokens: u256): void {
183 | call(
184 | this._origin,
185 | 'decreaseAllowance',
186 | new Args().add(spenderAccount).add(nbTokens),
187 | 0,
188 | );
189 | }
190 |
191 | /**
192 | * Transfers token ownership from the owner's account to
193 | * the recipient's account using the spender's allowance.
194 | *
195 | * This function can only be called by the spender.
196 | * This function is atomic:
197 | * - both allowance and transfer are executed if possible;
198 | * - or if allowance or transfer is not possible, both are discarded.
199 | *
200 | * @param ownerAccount -
201 | * @param recipientAccount -
202 | * @param nbTokens -
203 | */
204 | transferFrom(
205 | ownerAccount: Address,
206 | recipientAccount: Address,
207 | nbTokens: u256,
208 | coins: u64 = 0,
209 | ): void {
210 | call(
211 | this._origin,
212 | 'transferFrom',
213 | new Args().add(ownerAccount).add(recipientAccount).add(nbTokens),
214 | coins,
215 | );
216 | }
217 |
218 | /**
219 | * Mint an amount of nbTokens tokens from to the toAccount address .
220 | *
221 | * @param toAccount -
222 | * @param nbTokens -
223 | * @param coins -
224 | */
225 | mint(toAccount: Address, nbTokens: u256, coins: u64 = 0): void {
226 | call(this._origin, 'mint', new Args().add(toAccount).add(nbTokens), coins);
227 | }
228 |
229 | /**
230 | * Burn nbTokens on the caller address
231 | *
232 | * Coins is left to zero as this function does not need storage entry creation.
233 | *
234 | * @param nbTokens -
235 | */
236 | burn(nbTokens: u256): void {
237 | call(this._origin, 'burn', new Args().add(nbTokens), 0);
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/wallet/file-format.md:
--------------------------------------------------------------------------------
1 | # Wallet file format standard
2 |
3 | Initial meta issue:
4 |
5 | **Authors:** N. Seva, G. Libert, V. Deshpande
6 |
7 | **Status:** Review
8 |
9 | **Version:** 1.0
10 |
11 | ## Introduction
12 |
13 | The Wallet file format standard provides a specification for protection and serialization of accounts within a wallet. This standard doesn't specify the address object nor the cryptography used by the Massa blockchain to sign operation.
14 |
15 | ### Targeted Audience
16 |
17 | This specification is intended for software developers, security professionals, and organizations who are developing or implementing wallet software.
18 |
19 | ### Vocabulary
20 |
21 | **Account:** A collection of information that represents a user's identity.
22 |
23 | **Wallet:** An application handling multiple accounts and offering multiple services such as signing transactions or access to the blockchain.
24 |
25 | **Salt:** A random sequence of bytes used as an additional input to a key derivation function.
26 |
27 | **Nonce:** A random sequence of bytes used as an initialization vector for symmetric encryption.
28 |
29 | **Private key:** A key used to sign transactions and authenticate the account holder.
30 |
31 | **Public key:** A key used to identify the account holder and verify digital signatures generated using a private key.
32 |
33 | **Address:** A unique identifier that represents the account.
34 |
35 | **YAML:** A human-readable data serialization format.
36 |
37 | ## Specification
38 |
39 | ### Cryptography
40 |
41 | #### PBKDF2
42 |
43 | To protect the private key, a symmetric key is derived from the user password using the PBKDF2 algorithm, as defined in IETF [RFC 2898](https://www.ietf.org/rfc/rfc2898.txt).
44 |
45 | > _NOTE:_ The user input password is first converted to bytes using utf-8 encoding before passing it to PBKDF2.
46 |
47 | Specifically, the PBKDF2 arguments defined in section 5.2 of the aforementioned standard must follow the followings:
48 |
49 | - 16-byte salt,
50 | - 600,000 iterations, and a
51 | - derived key length of 32 bytes.
52 |
53 | The hash function utilized in this process is SHA-256, as specified in the NIST [FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) document.
54 |
55 | These values align with the recommendations set forth in the NIST [Special Publication 800-132](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf)
56 | and [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2).
57 |
58 | #### AES-GCM
59 |
60 | After deriving the symmetric key (the derived key from PBKDF2), the account's private key is prepended by its version and encrypted using AES-256 with Galois/Counter Mode (GCM), as defined by the NIST in the [Special Publication 800-38D](https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38d.pdf). The AES-256 algorithm is specified in the NIST [FIPS 197](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.197.pdf) document.
61 |
62 | ##### Nonce
63 |
64 | The nonce, used as initialization vector for AES-GCM, must have a size of 96 bits (12 bytes) as recommended in Special Publication 800-38D.
65 |
66 | ##### Authentication tag
67 |
68 | The authentication tag is appended to the end of the ciphered private key.
69 |
70 | The size of the authentication tag is 16 bytes (128 bits).
71 |
72 | ### Serialization
73 |
74 | An account, in a serialized form, consists of various components, including the account's private and public keys, a salt, a nonce, a version, a nickname, and optionally, an address to aid in wallet recognition.
75 | The account should be serialized using YAML to enable human readability of the file's contents, particularly the address and nickname.
76 |
77 | The following table summarize the format:
78 |
79 | | Field | Presence | Format | Comment | Example |
80 | | ----- | -------- | ------ | ------- | ------- |
81 | | Version | Mandatory | Integer | Entire part of this specification version | 1 |
82 | | Nickname | Mandatory | String || "Savings" |
83 | | Address | Optional | String || "AU12..." |
84 | | Salt | Mandatory | Byte array | Salt for PBKDF2 (16 Bytes) | [57, 125, 102, 235, 118, 62, 21, 145, 126, 197, 242, 54, 145, 50, 178, 98] |
85 | | Nonce | Mandatory | Byte array | Initialization Vector (12 Bytes) for AES-GCM | [119, 196, 31, 33, 211, 243, 26, 58, 102, 180, 47, 57] |
86 | | CipheredData | Mandatory | Byte array | Ciphered Private Key Bytes (using AES-GCM) followed by Authentication Tag (16 Bytes) | [17, 42 ...] |
87 | | PublicKey | Mandatory | Byte array | Public key prepended by its version | [0, 21, 126 ...] |
88 |
89 | #### Example
90 |
91 | Here is an example of YAML serialization:
92 |
93 | ```yaml
94 | ---
95 | Version: 1
96 | Nickname: Savings
97 | Address: AU12...
98 | Salt: [57, 125, 102, 235, 118, 62, 21, 145, 126, 197, 242, 54, 145, 50, 178, 98]
99 | Nonce: [119, 196, 31, 33, 211, 243, 26, 58, 102, 180, 47, 57]
100 | CipheredData: [17, 42, ...]
101 | PublicKey: [0, 21, 126, ...]
102 | ```
103 |
104 | #### Decryption of the Private Key
105 |
106 | In order to decrypt the private key, following steps are followed:
107 |
108 | 1. User inputs a password.
109 | 2. Password is converted to bytes (utf-8 encoding).
110 | 3. Symmetric key is derived using PBKDF2 with this password (bytes) and the salt (bytes) as input.
111 | 4. This derived symmetric key is then used as AES-GCM Key along with the nonce (IV) to decrypt the ciphered private key.
112 | 5. The authentication tag at the end of ciphered data checks if the derived symmetric key used in point 4. is right.
113 |
114 | ## Security Concerns
115 |
116 | The wallet file format specification aims to ensure the security and integrity of user accounts. Then, several security concerns must be taken into account:
117 |
118 | ### Encryption and Decryption
119 |
120 | The encryption and decryption of the private key must be performed correctly to ensure the confidentiality and integrity of the account. If the encryption algorithm or key derivation function is weak, the private key may be vulnerable to attacks. It is therefore crucial to use strong and proven encryption algorithms and key derivation functions.
121 |
122 | ### Salt Generation
123 |
124 | The generation of random and unpredictable values for the salt is critical in protecting the private key. If the values are not truly random or can be easily guessed, the encryption of the private key may be compromised. It is therefore essential to use a robust and reliable method.
125 |
126 | ### Password Strength
127 |
128 | The strength of the user's password is crucial to the security of the wallet. If the password is weak or easily guessable, the private key's confidentiality may be compromised. Therefore, it is essential to educate users on creating strong passwords and implementing password policies that enforce minimum complexity requirements.
129 |
130 | ## Implementation
131 |
132 | This section will be updated with links to reference implementations developed by MassaLabs.
133 | These implementations will follow the wallet file format specification to ensure compatibility across different platforms.
134 |
135 | By providing these references, we aim to facilitate adoption and promote security and interoperability.
136 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/__tests__/MRC721.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | changeCallStack,
3 | resetStorage,
4 | setDeployContext,
5 | } from '@massalabs/massa-as-sdk';
6 | import {
7 | name,
8 | symbol,
9 | mrc721Constructor,
10 | mint,
11 | ownerOf,
12 | balanceOf,
13 | approve,
14 | getApproved,
15 | setApprovalForAll,
16 | isApprovedForAll,
17 | transferFrom,
18 | burn,
19 | ownerAddress,
20 | } from '../MRC721';
21 | import {
22 | Args,
23 | byteToBool,
24 | bytesToString,
25 | bytesToU256,
26 | } from '@massalabs/as-types';
27 | import { u256 } from 'as-bignum/assembly';
28 |
29 | const NFTName = 'MASSA_NFT';
30 | const NFTSymbol = 'NFT';
31 | const contractOwner = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
32 | const tokenAddress = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
33 | const from = 'AU12CzoKEASaeBHnxGLnHDG2u73dLzWWfgvW6bc4L1UfMA5Uc5Fg7';
34 | const to = 'AU178qZCfaNXkz9tQiXJcVfAEnYGJ27UoNtFFJh3BiT8jTfY8P2D';
35 | const approved = 'AU1sF3HSa7fcBoE12bE1Eq2ohKqcRPBHuNRmdqAMfw8WEkHCU3aF';
36 | const newOwner = 'AU12F7y3PWpw72XcwhSksJztRiTSqAvLxaLacP2qDYhNUEfEXuG4T';
37 | const zeroAddress = '';
38 | const tokenId = u256.One;
39 |
40 | function switchUser(user: string): void {
41 | changeCallStack(user + ' , ' + tokenAddress);
42 | }
43 |
44 | beforeEach(() => {
45 | resetStorage();
46 | switchUser(contractOwner);
47 | setDeployContext(contractOwner);
48 | mrc721Constructor(NFTName, NFTSymbol);
49 | });
50 |
51 | describe('Initialization', () => {
52 | test('get name', () => {
53 | expect(bytesToString(name())).toBe(NFTName);
54 | });
55 | test('get symbol', () => {
56 | expect(bytesToString(symbol())).toBe(NFTSymbol);
57 | });
58 | test('get owner', () => {
59 | expect(bytesToString(ownerAddress([]))).toBe(contractOwner);
60 | });
61 | });
62 |
63 | describe('Minting', () => {
64 | test('Mint token to an address', () => {
65 | switchUser(contractOwner);
66 | mint(new Args().add(to).add(tokenId).serialize());
67 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
68 | to,
69 | );
70 | expect(
71 | bytesToU256(balanceOf(new Args().add(to).serialize())),
72 | ).toStrictEqual(u256.One);
73 | });
74 | throws('Minting from not owner should fail', () => {
75 | switchUser(from);
76 | mint(new Args().add(to).add(tokenId).serialize());
77 | });
78 | throws('Minting to zero address should fail', () => {
79 | mint(new Args().add(zeroAddress).add(tokenId).serialize());
80 | });
81 | throws('Minting an already existing tokenId should fail', () => {
82 | mint(new Args().add(to).add(tokenId).serialize());
83 | mint(new Args().add(to).add(tokenId).serialize());
84 | });
85 | test('Mint multiple tokens to an address', () => {
86 | mint(new Args().add(to).add(tokenId).serialize());
87 | mint(new Args().add(to).add(new u256(2)).serialize());
88 | expect(
89 | bytesToU256(balanceOf(new Args().add(to).serialize())),
90 | ).toStrictEqual(new u256(2));
91 | });
92 | test('Mint multiple tokens to different addresses', () => {
93 | mint(new Args().add(to).add(tokenId).serialize());
94 | mint(new Args().add(from).add(new u256(2)).serialize());
95 | expect(
96 | bytesToU256(balanceOf(new Args().add(to).serialize())),
97 | ).toStrictEqual(u256.One);
98 | expect(
99 | bytesToString(ownerOf(new Args().add(new u256(2)).serialize())),
100 | ).toBe(from);
101 | expect(
102 | bytesToU256(balanceOf(new Args().add(from).serialize())),
103 | ).toStrictEqual(u256.One);
104 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
105 | to,
106 | );
107 | });
108 | });
109 |
110 | describe('Approval', () => {
111 | test('Approve token for an address', () => {
112 | mint(new Args().add(from).add(tokenId).serialize());
113 | switchUser(from);
114 | approve(new Args().add(approved).add(tokenId).serialize());
115 | expect(
116 | bytesToString(getApproved(new Args().add(tokenId).serialize())),
117 | ).toBe(approved);
118 | });
119 | test('ApproveForAll for operator and owner', () => {
120 | mint(new Args().add(from).add(tokenId).serialize());
121 | switchUser(from);
122 | setApprovalForAll(new Args().add(approved).add(true).serialize());
123 | expect(
124 | byteToBool(
125 | isApprovedForAll(new Args().add(from).add(approved).serialize()),
126 | ),
127 | ).toBe(true);
128 | });
129 | throws('Approve token of not owned token should fail', () => {
130 | mint(new Args().add(from).add(tokenId).serialize());
131 | switchUser(approved);
132 | approve(new Args().add(approved).add(tokenId).serialize());
133 | });
134 | });
135 |
136 | describe('Transfers', () => {
137 | test('Transfer token from owner', () => {
138 | mint(new Args().add(from).add(tokenId).serialize());
139 | switchUser(from);
140 | transferFrom(new Args().add(from).add(to).add(tokenId).serialize());
141 | });
142 | throws('Transfer not owned or approved token should fail', () => {
143 | mint(new Args().add(from).add(tokenId).serialize());
144 | switchUser(from);
145 | transferFrom(new Args().add(to).add(newOwner).add(tokenId).serialize());
146 | });
147 | test('Transfer approved token', () => {
148 | mint(new Args().add(from).add(tokenId).serialize());
149 | switchUser(from);
150 | approve(new Args().add(approved).add(tokenId).serialize());
151 | switchUser(approved);
152 | transferFrom(new Args().add(from).add(to).add(tokenId).serialize());
153 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
154 | to,
155 | );
156 | });
157 | test('Transfer approvedForAll token', () => {
158 | mint(new Args().add(from).add(tokenId).serialize());
159 | switchUser(from);
160 | setApprovalForAll(new Args().add(approved).add(true).serialize());
161 | switchUser(approved);
162 | transferFrom(new Args().add(from).add(to).add(tokenId).serialize());
163 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
164 | to,
165 | );
166 | });
167 | });
168 |
169 | describe('burn', () => {
170 | test('burn token', () => {
171 | mint(new Args().add(from).add(tokenId).serialize());
172 | switchUser(from);
173 | burn(new Args().add(tokenId).serialize());
174 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
175 | '',
176 | );
177 | });
178 | test('burn token with approval', () => {
179 | mint(new Args().add(from).add(tokenId).serialize());
180 | switchUser(from);
181 | approve(new Args().add(approved).add(tokenId).serialize());
182 | switchUser(approved);
183 | burn(new Args().add(tokenId).serialize());
184 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
185 | '',
186 | );
187 | });
188 | test('burn token with approvalForAll', () => {
189 | mint(new Args().add(from).add(tokenId).serialize());
190 | switchUser(from);
191 | setApprovalForAll(new Args().add(approved).add(true).serialize());
192 | switchUser(approved);
193 | burn(new Args().add(tokenId).serialize());
194 | expect(bytesToString(ownerOf(new Args().add(tokenId).serialize()))).toBe(
195 | '',
196 | );
197 | });
198 | throws('burn not owned or approved token should fail', () => {
199 | mint(new Args().add(from).add(tokenId).serialize());
200 | switchUser(to);
201 | burn(new Args().add(tokenId).serialize());
202 | });
203 | });
204 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC1155/MRC1155.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This file contains the implementation of the MRC1155 token standard.
4 | *
5 | * It can be extended with the following extensions:
6 | * - mintable
7 | * - burnable
8 | * - metadata
9 | *
10 | * The extensions are implemented in separate files and can be imported as separate modules.
11 | *
12 | * The contract is meant to be used as a base contract for a specific Multi-NFT contract.
13 | */
14 |
15 | import {
16 | Args,
17 | boolToByte,
18 | fixedSizeArrayToBytes,
19 | stringToBytes,
20 | u256ToBytes,
21 | } from '@massalabs/as-types';
22 | import {
23 | _balanceOf,
24 | _constructor,
25 | _balanceOfBatch,
26 | _uri,
27 | _setApprovalForAll,
28 | _isApprovedForAll,
29 | _safeTransferFrom,
30 | _safeBatchTransferFrom,
31 | MRC1155_MISSING_APPROVAL_FOR_ALL_ERROR,
32 | MRC1155_INVALID_ARRAY_LENGTH_ERROR,
33 | } from './MRC1155-internal';
34 |
35 | import { Context, isDeployingContract } from '@massalabs/massa-as-sdk';
36 |
37 | import { u256 } from 'as-bignum/assembly';
38 | import { _setOwner } from '../utils/ownership-internal';
39 |
40 | /**
41 | * Constructs a new Multi-NFT contract.
42 | *
43 | * @remarks You must call this function in your contract's constructor or re-write it to fit your needs !
44 | *
45 | * @param uri - the URI for the NFT contract
46 | */
47 | export function mrc1155Constructor(uri: string): void {
48 | assert(isDeployingContract());
49 | _setOwner(Context.caller().toString());
50 | _constructor(uri);
51 | }
52 |
53 | /**
54 | *
55 | * Get the URI for a token id
56 | *
57 | * @param id - the id of the token
58 | *
59 | * @returns the URI for the token
60 | *
61 | */
62 | export function uri(binaryArgs: StaticArray): StaticArray {
63 | const args = new Args(binaryArgs);
64 | const id = args.nextU256().expect('id argument is missing or invalid');
65 |
66 | return stringToBytes(_uri(id));
67 | }
68 |
69 | /**
70 | *
71 | * Get the balance of a specific token for an address
72 | *
73 | * @param owner - the address to get the balance for
74 | * @param id - the id of the token to get the balance for
75 | *
76 | * @returns the balance of the token for the address
77 | */
78 | export function balanceOf(binaryArgs: StaticArray): StaticArray {
79 | const args = new Args(binaryArgs);
80 | const owner = args
81 | .nextString()
82 | .expect('owner argument is missing or invalid');
83 | const id = args.nextU256().expect('id argument is missing or invalid');
84 |
85 | return u256ToBytes(_balanceOf(owner, id));
86 | }
87 |
88 | /**
89 | *
90 | * Get the balance of multiple tokens for multiples addresses
91 | *
92 | * @param owners - the addresses to get the balance for
93 | * @param ids - the ids of the tokens to get the balance for
94 | *
95 | * @returns the balances of the tokens for the addresses
96 | */
97 | export function balanceOfBatch(binaryArgs: StaticArray): StaticArray {
98 | const args = new Args(binaryArgs);
99 | const owners = args
100 | .nextStringArray()
101 | .expect('owners argument is missing or invalid');
102 | const ids = args
103 | .nextFixedSizeArray()
104 | .expect('ids argument is missing or invalid');
105 | assert(owners.length == ids.length, MRC1155_INVALID_ARRAY_LENGTH_ERROR);
106 |
107 | const balances = _balanceOfBatch(owners, ids);
108 | return fixedSizeArrayToBytes(balances);
109 | }
110 |
111 | /**
112 | *
113 | * Set the approval status of an operator for a specific token
114 | *
115 | * Emits an ApprovalForAll event
116 | *
117 | * @param operator - the operator to set the approval for
118 | * @param approved - the new approval status
119 | */
120 | export function setApprovalForAll(binaryArgs: StaticArray): void {
121 | const sender = Context.caller().toString();
122 | const args = new Args(binaryArgs);
123 | const operator = args
124 | .nextString()
125 | .expect('operator argument is missing or invalid');
126 | const approved = args
127 | .nextBool()
128 | .expect('approved argument is missing or invalid');
129 |
130 | _setApprovalForAll(sender, operator, approved);
131 | }
132 |
133 | /**
134 | *
135 | * Check if an operator is approved for all tokens of an owner
136 | *
137 | * @param owner - the owner of the tokens
138 | * @param operator - the operator to check
139 | *
140 | * @returns true if the operator is approved for all tokens of the owner
141 | */
142 | export function isApprovedForAll(binaryArgs: StaticArray): StaticArray {
143 | const args = new Args(binaryArgs);
144 | const owner = args
145 | .nextString()
146 | .expect('owner argument is missing or invalid');
147 | const operator = args
148 | .nextString()
149 | .expect('operator argument is missing or invalid');
150 |
151 | return boolToByte(_isApprovedForAll(owner, operator));
152 | }
153 |
154 | /**
155 | *
156 | * Safe transfer of a specific amount of tokens to an address.
157 | * The receiving address can implement the OnMRC1155Received interface to be called once a transfer happens.
158 | *
159 | * Emits a TransferSingle event.
160 | *
161 | * @param from - the account to transfer the tokens from
162 | * @param to - the account to transfer the tokens to
163 | * @param id - the id of the token to transfer
164 | * @param value - the amount of tokens to transfer
165 | * @param data - additional data to pass to the receiver
166 | */
167 | export function safeTransferFrom(binaryArgs: StaticArray): void {
168 | const sender = Context.caller().toString();
169 | const args = new Args(binaryArgs);
170 | const from = args.nextString().expect('from argument is missing or invalid');
171 | const to = args.nextString().expect('to argument is missing or invalid');
172 | const id = args.nextU256().expect('id argument is missing or invalid');
173 | const value = args.nextU256().expect('value argument is missing or invalid');
174 | const data = args.nextBytes().expect('data argument is missing or invalid');
175 | assert(
176 | from == sender || _isApprovedForAll(from, sender),
177 | MRC1155_MISSING_APPROVAL_FOR_ALL_ERROR,
178 | );
179 |
180 | _safeTransferFrom(from, to, id, value, data);
181 | }
182 |
183 | /**
184 | *
185 | * Safe transfer of a batch of tokens to an address.
186 | * The receiving address can implement the onMRC1155BatchReceived interface to be called once a transfer happens.
187 | *
188 | * Emits a TransferBatch event.
189 | *
190 | * @param from - the account to transfer the tokens from
191 | * @param to - the account to transfer the tokens to
192 | * @param ids - the ids of the tokens to transfer
193 | * @param values - the amounts of tokens to transfer
194 | * @param data - additional data to pass to the receiver
195 | */
196 | export function safeBatchTransferFrom(binaryArgs: StaticArray): void {
197 | const sender = Context.caller().toString();
198 | const args = new Args(binaryArgs);
199 | const from = args.nextString().expect('from argument is missing or invalid');
200 | const to = args.nextString().expect('to argument is missing or invalid');
201 | const ids = args
202 | .nextFixedSizeArray()
203 | .expect('ids argument is missing or invalid');
204 | const values = args
205 | .nextFixedSizeArray()
206 | .expect('values argument is missing or invalid');
207 | const data = args.nextBytes().expect('data argument is missing or invalid');
208 | assert(
209 | from == sender || _isApprovedForAll(from, sender),
210 | MRC1155_MISSING_APPROVAL_FOR_ALL_ERROR,
211 | );
212 |
213 | _safeBatchTransferFrom(from, to, ids, values, data);
214 | }
215 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/__tests__/MRC20.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Address,
3 | changeCallStack,
4 | resetStorage,
5 | setDeployContext,
6 | } from '@massalabs/massa-as-sdk';
7 | import {
8 | Args,
9 | stringToBytes,
10 | u8toByte,
11 | bytesToU256,
12 | u256ToBytes,
13 | } from '@massalabs/as-types';
14 | import {
15 | transfer,
16 | balanceOf,
17 | totalSupply,
18 | name,
19 | symbol,
20 | decimals,
21 | version,
22 | transferFrom,
23 | allowance,
24 | increaseAllowance,
25 | decreaseAllowance,
26 | mrc20Constructor,
27 | VERSION,
28 | } from '../MRC20';
29 | import { u256 } from 'as-bignum/assembly';
30 |
31 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
32 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
33 |
34 | const user1Address = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
35 |
36 | const user2Address = 'AU12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1e8';
37 |
38 | const user3Address = 'AUDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBOObs';
39 |
40 | const TOKEN_NAME = 'TOKEN_NAME';
41 | const TOKEN_SYMBOL = 'TKN';
42 | const DECIMALS: u8 = 8;
43 | const TOTAL_SUPPLY = new u256(100, 100, 100, 100);
44 |
45 | function switchUser(user: string): void {
46 | changeCallStack(user + ' , ' + contractAddr);
47 | }
48 |
49 | beforeAll(() => {
50 | resetStorage();
51 | setDeployContext(user1Address);
52 | mrc20Constructor(TOKEN_NAME, TOKEN_SYMBOL, DECIMALS, TOTAL_SUPPLY);
53 | });
54 |
55 | describe('Initialization', () => {
56 | test('total supply is properly initialized', () =>
57 | expect(totalSupply([])).toStrictEqual(u256ToBytes(TOTAL_SUPPLY)));
58 |
59 | test('token name is properly initialized', () =>
60 | expect(name([])).toStrictEqual(stringToBytes(TOKEN_NAME)));
61 |
62 | test('symbol is properly initialized', () =>
63 | expect(symbol([])).toStrictEqual(stringToBytes(TOKEN_SYMBOL)));
64 |
65 | test('decimals is properly initialized', () =>
66 | expect(decimals([])).toStrictEqual(u8toByte(DECIMALS)));
67 |
68 | test('version is properly initialized', () =>
69 | expect(version([])).toStrictEqual(VERSION));
70 | });
71 |
72 | describe('BalanceOf', () => {
73 | test('Check an empty balance', () =>
74 | expect(balanceOf(new Args().add(contractAddr).serialize())).toStrictEqual(
75 | u256ToBytes(u256.Zero),
76 | ));
77 |
78 | test('Check a non empty balance', () =>
79 | expect(
80 | bytesToU256(balanceOf(new Args().add(user1Address).serialize())),
81 | ).toBe(TOTAL_SUPPLY));
82 |
83 | test('Check balance of invalid address', () => {
84 | const invalidAddress = new Address('A12AZDefef');
85 | expect(
86 | balanceOf(new Args().add(invalidAddress.toString()).serialize()),
87 | ).toStrictEqual(u256ToBytes(u256.Zero));
88 | });
89 | });
90 |
91 | describe('Transfer', () => {
92 | test('Transfer from U1 => U2', () => {
93 | const transferAmount = new u256(10, 10);
94 |
95 | transfer(new Args().add(user2Address).add(transferAmount).serialize());
96 |
97 | // Check user1 balance
98 | expect(
99 | balanceOf(new Args().add(user1Address).serialize()),
100 | // @ts-ignore
101 | ).toStrictEqual(u256ToBytes(TOTAL_SUPPLY - transferAmount));
102 |
103 | // Check user2 balance
104 | expect(balanceOf(new Args().add(user2Address).serialize())).toStrictEqual(
105 | u256ToBytes(transferAmount),
106 | );
107 | });
108 |
109 | throws('Insuficient balance to transfer from U1 => U2', () => {
110 | // @ts-ignore
111 | const invalidAmount = TOTAL_SUPPLY + u256.One;
112 | transfer(new Args().add(user2Address).add(invalidAmount).serialize());
113 | });
114 |
115 | throws('Overflow', () =>
116 | transfer(new Args().add(user2Address).add(u256.Max).serialize()),
117 | );
118 |
119 | throws('Self transfer', () =>
120 | transfer(new Args().add(user1Address).serialize()),
121 | );
122 | });
123 |
124 | let u1u2AllowAmount = new u256(20, 20);
125 |
126 | describe('Allowance', () => {
127 | test('Increase user1 allowance for user2 to spend', () => {
128 | increaseAllowance(
129 | new Args().add(user2Address).add(u1u2AllowAmount).serialize(),
130 | );
131 |
132 | // check new allowance
133 | expect(
134 | allowance(new Args().add(user1Address).add(user2Address).serialize()),
135 | ).toStrictEqual(u256ToBytes(u1u2AllowAmount));
136 | });
137 |
138 | test('Increase user1 allowance to max amount for user2 to spend', () => {
139 | increaseAllowance(new Args().add(user2Address).add(u256.Max).serialize());
140 |
141 | // check new allowance
142 | expect(
143 | allowance(new Args().add(user1Address).add(user2Address).serialize()),
144 | ).toStrictEqual(u256ToBytes(u256.Max));
145 | });
146 |
147 | test('Decreases allowance U1 => U2', () => {
148 | const decreaseAmount = u256.fromU64(666);
149 | decreaseAllowance(
150 | new Args().add(user2Address).add(decreaseAmount).serialize(),
151 | );
152 |
153 | // check new allowance
154 | expect(
155 | allowance(new Args().add(user1Address).add(user2Address).serialize()),
156 | // @ts-ignore
157 | ).toStrictEqual(u256ToBytes(u256.Max - decreaseAmount));
158 | });
159 |
160 | test('Decrease user1 allowance to 0 for user2', () =>
161 | decreaseAllowance(new Args().add(user2Address).add(u256.Max).serialize()));
162 |
163 | test('check allowance is set to 0', () =>
164 | expect(
165 | allowance(new Args().add(user1Address).add(user2Address).serialize()),
166 | ).toStrictEqual(u256ToBytes(u256.Zero)));
167 | });
168 |
169 | const allowAmount = new u256(6000);
170 |
171 | describe('transferFrom', () => {
172 | beforeAll(() => {
173 | switchUser(user3Address);
174 |
175 | // Increase allowance for U1 to spend U3 tokens
176 | increaseAllowance(
177 | new Args().add(user1Address).add(allowAmount).serialize(),
178 | );
179 |
180 | switchUser(user1Address);
181 | });
182 |
183 | throws('Fails because not enough allowance U3 => U1 ', () => {
184 | transferFrom(
185 | new Args()
186 | .add(user3Address)
187 | .add(user2Address)
188 | // @ts-ignore
189 | .add(allowAmount + u256.One)
190 | .serialize(),
191 | );
192 | });
193 |
194 | throws('Fails because not enough token on U3', () =>
195 | transferFrom(
196 | new Args()
197 | .add(user3Address)
198 | .add(user2Address)
199 | .add(allowAmount)
200 | .serialize(),
201 | ),
202 | );
203 |
204 | test('u1 send tokens to u3 then transfer tokens from u3 to u2 ', () => {
205 | const u1balanceBefore = balanceOf(new Args().add(user1Address).serialize());
206 | const u2balanceBefore = balanceOf(new Args().add(user2Address).serialize());
207 | const u3balanceBefore = balanceOf(new Args().add(user3Address).serialize());
208 |
209 | transfer(new Args().add(user3Address).add(allowAmount).serialize());
210 |
211 | transferFrom(
212 | new Args()
213 | .add(user3Address)
214 | .add(user2Address)
215 | .add(allowAmount)
216 | .serialize(),
217 | );
218 |
219 | // Check balance changes
220 | expect(balanceOf(new Args().add(user1Address).serialize())).toStrictEqual(
221 | // @ts-ignore
222 | u256ToBytes(bytesToU256(u1balanceBefore) - allowAmount),
223 | );
224 |
225 | expect(balanceOf(new Args().add(user2Address).serialize())).toStrictEqual(
226 | // @ts-ignore
227 |
228 | u256ToBytes(bytesToU256(u2balanceBefore) + allowAmount),
229 | );
230 | expect(balanceOf(new Args().add(user3Address).serialize())).toStrictEqual(
231 | u3balanceBefore,
232 | );
233 |
234 | // Verify allowances after transferFrom
235 | expect(
236 | allowance(new Args().add(user1Address).add(user3Address).serialize()),
237 | ).toStrictEqual(u256ToBytes(u256.Zero));
238 | });
239 | });
240 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/MRC721.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This is an example of a MRC721 contract that uses the MRC721-internals
4 | * helper functions to implement the ERC721 standard.
5 | *
6 | * This files does basically two things:
7 | * 1. It wraps the MRC721-internals functions, manages the deserialize/serialize of the arguments and return values,
8 | * and exposes them to the outside world.
9 | * 2. It implements some custom features that are not part of the ERC721 standard, like mint, burn or ownership.
10 | *
11 | * The MRC721-internals functions are not supposed to be re-exported by this file.
12 | */
13 |
14 | import {
15 | Args,
16 | boolToByte,
17 | stringToBytes,
18 | u256ToBytes,
19 | } from '@massalabs/as-types';
20 | import {
21 | _approve,
22 | _balanceOf,
23 | _constructor,
24 | _getApproved,
25 | _isApprovedForAll,
26 | _name,
27 | _ownerOf,
28 | _setApprovalForAll,
29 | _symbol,
30 | _update,
31 | _transferFrom,
32 | } from './MRC721-internals';
33 | import { onlyOwner } from '../utils/ownership';
34 |
35 | import { Context, isDeployingContract } from '@massalabs/massa-as-sdk';
36 | import { _setOwner } from '../utils/ownership-internal';
37 |
38 | /**
39 | * @param name - the name of the NFT
40 | * @param symbol - the symbol of the NFT
41 | * @remarks You must call this function in your contract's constructor or re-write it to fit your needs !
42 | */
43 | export function mrc721Constructor(name: string, symbol: string): void {
44 | assert(isDeployingContract());
45 |
46 | _constructor(name, symbol);
47 | _setOwner(Context.caller().toString());
48 | }
49 |
50 | export function name(): StaticArray {
51 | return stringToBytes(_name());
52 | }
53 |
54 | export function symbol(): StaticArray {
55 | return stringToBytes(_symbol());
56 | }
57 |
58 | /**
59 | *
60 | * @param binaryArgs - serialized string representing the address whose balance we want to check
61 | * @returns a serialized u256 representing the balance of the address
62 | * @remarks As we can see, instead of checking the storage directly,
63 | * we call the _balanceOf function from the MRC721-internals.
64 | */
65 | export function balanceOf(binaryArgs: StaticArray): StaticArray {
66 | const args = new Args(binaryArgs);
67 | const address = args
68 | .nextString()
69 | .expect('address argument is missing or invalid');
70 | return u256ToBytes(_balanceOf(address));
71 | }
72 |
73 | /**
74 | *
75 | * @param binaryArgs - serialized u256 representing the tokenId whose owner we want to check
76 | * @returns a serialized string representing the address of owner of the tokenId
77 | */
78 | export function ownerOf(binaryArgs: StaticArray): StaticArray {
79 | const args = new Args(binaryArgs);
80 | const tokenId = args
81 | .nextU256()
82 | .expect('tokenId argument is missing or invalid');
83 | return stringToBytes(_ownerOf(tokenId));
84 | }
85 |
86 | /**
87 | *
88 | * @param binaryArgs - serialized u256 representing the tokenId whose approved address we want to check
89 | * @returns a serialized string representing the address of the approved address of the tokenId
90 | */
91 | export function getApproved(binaryArgs: StaticArray): StaticArray {
92 | const args = new Args(binaryArgs);
93 | const tokenId = args
94 | .nextU256()
95 | .expect('tokenId argument is missing or invalid');
96 | return stringToBytes(_getApproved(tokenId));
97 | }
98 |
99 | /**
100 | *
101 | * @param binaryArgs - serialized strings representing the address of an owner and an operator
102 | * @returns a serialized u8 representing a boolean value indicating if
103 | * the operator is approved for all the owner's tokens
104 | */
105 | export function isApprovedForAll(binaryArgs: StaticArray): StaticArray {
106 | const args = new Args(binaryArgs);
107 | const owner = args
108 | .nextString()
109 | .expect('owner argument is missing or invalid');
110 | const operator = args
111 | .nextString()
112 | .expect('operator argument is missing or invalid');
113 | return boolToByte(_isApprovedForAll(owner, operator));
114 | }
115 |
116 | /**
117 | *
118 | * @param binaryArgs - serialized strings representing the address of the recipient and the tokenId to approve
119 | * @remarks This function is only callable by the owner of the tokenId or an approved operator.
120 | * Indeed, this will be checked by the _approve function of the MRC721-internals.
121 | *
122 | */
123 | export function approve(binaryArgs: StaticArray): void {
124 | const args = new Args(binaryArgs);
125 | const to = args.nextString().expect('to argument is missing or invalid');
126 | const tokenId = args
127 | .nextU256()
128 | .expect('tokenId argument is missing or invalid');
129 | _approve(to, tokenId);
130 | }
131 |
132 | /**
133 | *
134 | * @param binaryArgs - serialized arguments representing the address of the operator and a boolean value indicating
135 | * if the operator should be approved for all the caller's tokens
136 | *
137 | */
138 | export function setApprovalForAll(binaryArgs: StaticArray): void {
139 | const args = new Args(binaryArgs);
140 | const to = args.nextString().expect('to argument is missing or invalid');
141 | const approved = args
142 | .nextBool()
143 | .expect('approved argument is missing or invalid');
144 | _setApprovalForAll(to, approved);
145 | }
146 |
147 | /**
148 | *
149 | * @param binaryArgs - serialized arguments representing the address of the sender,
150 | * the address of the recipient and the tokenId to transfer.
151 | *
152 | * @remarks This function is only callable by the owner of the tokenId or an approved operator.
153 | *
154 | */
155 | export function transferFrom(binaryArgs: StaticArray): void {
156 | const args = new Args(binaryArgs);
157 | const from = args.nextString().expect('from argument is missing or invalid');
158 | const to = args.nextString().expect('to argument is missing or invalid');
159 | const tokenId = args
160 | .nextU256()
161 | .expect('tokenId argument is missing or invalid');
162 | _transferFrom(from, to, tokenId);
163 | }
164 |
165 | /**
166 | *
167 | * @param binaryArgs - serialized arguments representing the address of the recipient and the tokenId to mint
168 | *
169 | * @remarks This function is only callable by the owner of the contract.
170 | *
171 | * This function is not part of the ERC721 standard.
172 | * It serves as an example of how to use the MRC721-internals functions to implement custom features.
173 | * Here we make use of the _update function from the MRC721-internals to mint a new token.
174 | * Indeed, by calling _update with a non-existing tokenId, we are creating a new token.
175 | *
176 | * We also make sure that the mint feature is only callable by the owner of the contract
177 | * by using the onlyOwner modifier.
178 | *
179 | */
180 | export function mint(binaryArgs: StaticArray): void {
181 | onlyOwner();
182 | const args = new Args(binaryArgs);
183 | const to = args.nextString().expect('to argument is missing or invalid');
184 | const tokenId = args
185 | .nextU256()
186 | .expect('tokenId argument is missing or invalid');
187 | _update(to, tokenId, '');
188 | }
189 |
190 | /**
191 | *
192 | * @param binaryArgs - serialized u256 representing the tokenId to burn
193 | *
194 | * @remarks This function is not part of the ERC721 standard.
195 | * It serves as an example of how to use the MRC721-internals functions to implement custom features.
196 | * Here we make use of the _update function from the MRC721-internals to burn a token.
197 | * Indeed, by calling _update with the zero address as a recipient, we are burning the token.
198 | *
199 | * We also made sure that the burn feature is only callable by the owner of the token or an approved operator.
200 | * Indeed, the _update function will check if the caller is the owner of the token or an approved operator.
201 | *
202 | */
203 | export function burn(binaryArgs: StaticArray): void {
204 | const args = new Args(binaryArgs);
205 | const tokenId = args
206 | .nextU256()
207 | .expect('tokenId argument is missing or invalid');
208 | _update('', tokenId, '');
209 | }
210 |
211 | /**
212 | * Here we re-export the ownerAddress function from the ownership file.
213 | * This will allow the outside world to check the owner of the contract.
214 | * However we do not re-export any function from the MRC721-internals file.
215 | * This is because the MRC721-internals functions are not supposed to be called directly by the outside world.
216 | */
217 | export { ownerAddress } from '../utils/ownership';
218 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC20/MRC20.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Address,
3 | Context,
4 | generateEvent,
5 | Storage,
6 | isDeployingContract,
7 | } from '@massalabs/massa-as-sdk';
8 | import { Args, stringToBytes, u256ToBytes } from '@massalabs/as-types';
9 | import { _balance, _setBalance, _approve, _allowance } from './MRC20-internals';
10 | import { setOwner } from '../utils/ownership';
11 | import { u256 } from 'as-bignum/assembly';
12 |
13 | export const VERSION = stringToBytes('0.0.1');
14 |
15 | const TRANSFER_EVENT_NAME = 'TRANSFER SUCCESS';
16 | const APPROVAL_EVENT_NAME = 'APPROVAL SUCCESS';
17 |
18 | export const NAME_KEY = stringToBytes('NAME');
19 | export const SYMBOL_KEY = stringToBytes('SYMBOL');
20 | export const TOTAL_SUPPLY_KEY = stringToBytes('TOTAL_SUPPLY');
21 | export const DECIMALS_KEY = stringToBytes('DECIMALS');
22 |
23 | /**
24 | * Initialize the ERC20 contract
25 | *
26 | * @remarks You must call this function in your contract's constructor or re-write it to fit your needs !
27 | *
28 | * @param name - the name of the token
29 | * @param symbol - the symbol of the token
30 | * @param decimals - the number of decimals
31 | * @param totalSupply - the total supply of the token
32 | */
33 | export function mrc20Constructor(
34 | name: string,
35 | symbol: string,
36 | decimals: u8,
37 | totalSupply: u256,
38 | ): void {
39 | assert(isDeployingContract());
40 |
41 | Storage.set(NAME_KEY, stringToBytes(name));
42 | Storage.set(SYMBOL_KEY, stringToBytes(symbol));
43 | Storage.set(DECIMALS_KEY, [decimals]);
44 | Storage.set(TOTAL_SUPPLY_KEY, u256ToBytes(totalSupply));
45 |
46 | setOwner(new Args().add(Context.caller().toString()).serialize());
47 |
48 | _setBalance(Context.caller(), totalSupply);
49 | }
50 |
51 | /**
52 | * Returns the version of this smart contract.
53 | * This versioning is following the best practices defined in https://semver.org/.
54 | *
55 | * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18
56 | * @returns token version
57 | */
58 | export function version(_: StaticArray): StaticArray {
59 | return VERSION;
60 | }
61 |
62 | // ======================================================== //
63 | // ==== TOKEN ATTRIBUTES ==== //
64 | // ======================================================== //
65 |
66 | /**
67 | * Returns the name of the token.
68 | *
69 | * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18
70 | * @returns token name.
71 | */
72 | export function name(_: StaticArray): StaticArray {
73 | return Storage.get(NAME_KEY);
74 | }
75 |
76 | /** Returns the symbol of the token.
77 | *
78 | * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18
79 | * @returns token symbol.
80 | */
81 | export function symbol(_: StaticArray): StaticArray {
82 | return Storage.get(SYMBOL_KEY);
83 | }
84 |
85 | /**
86 | * Returns the total token supply.
87 | *
88 | * The number of tokens that were initially minted.
89 | *
90 | * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18
91 | * @returns u256
92 | */
93 | export function totalSupply(_: StaticArray): StaticArray {
94 | return Storage.get(TOTAL_SUPPLY_KEY);
95 | }
96 |
97 | /**
98 | * Returns the maximum number of digits being part of the fractional part
99 | * of the token when using a decimal representation.
100 | *
101 | * @param _ - unused see https://github.com/massalabs/massa-sc-std/issues/18
102 | * @returns
103 | */
104 | export function decimals(_: StaticArray): StaticArray {
105 | return Storage.get(DECIMALS_KEY);
106 | }
107 |
108 | // ==================================================== //
109 | // ==== BALANCE ==== //
110 | // ==================================================== //
111 |
112 | /**
113 | * Returns the balance of an account.
114 | *
115 | * @param binaryArgs - Args object serialized as a string containing an owner's account (Address).
116 | */
117 | export function balanceOf(binaryArgs: StaticArray): StaticArray {
118 | const args = new Args(binaryArgs);
119 |
120 | const addr = new Address(
121 | args.nextString().expect('Address argument is missing or invalid'),
122 | );
123 |
124 | return u256ToBytes(_balance(addr));
125 | }
126 |
127 | // ==================================================== //
128 | // ==== TRANSFER ==== //
129 | // ==================================================== //
130 |
131 | /**
132 | * Transfers tokens from the caller's account to the recipient's account.
133 | *
134 | * @param binaryArgs - Args object serialized as a string containing:
135 | * - the recipient's account (address)
136 | * - the number of tokens (u256).
137 | */
138 | export function transfer(binaryArgs: StaticArray): void {
139 | const owner = Context.caller();
140 |
141 | const args = new Args(binaryArgs);
142 | const toAddress = new Address(
143 | args.nextString().expect('receiverAddress argument is missing or invalid'),
144 | );
145 | const amount = args
146 | .nextU256()
147 | .expect('amount argument is missing or invalid');
148 |
149 | _transfer(owner, toAddress, amount);
150 |
151 | generateEvent(TRANSFER_EVENT_NAME);
152 | }
153 |
154 | /**
155 | * Transfers tokens from the caller's account to the recipient's account.
156 | *
157 | * @param from - sender address
158 | * @param to - recipient address
159 | * @param amount - number of token to transfer
160 | *
161 | * @returns true if the transfer is successful
162 | */
163 | function _transfer(from: Address, to: Address, amount: u256): void {
164 | assert(from != to, 'Transfer failed: cannot send tokens to own account');
165 |
166 | const currentFromBalance = _balance(from);
167 | const currentToBalance = _balance(to);
168 | // @ts-ignore
169 | const newToBalance = currentToBalance + amount;
170 |
171 | assert(currentFromBalance >= amount, 'Transfer failed: insufficient funds');
172 | assert(newToBalance >= currentToBalance, 'Transfer failed: overflow');
173 | // @ts-ignore
174 | _setBalance(from, currentFromBalance - amount);
175 | _setBalance(to, newToBalance);
176 | }
177 |
178 | // ==================================================== //
179 | // ==== ALLOWANCE ==== //
180 | // ==================================================== //
181 |
182 | /**
183 | * Returns the allowance set on the owner's account for the spender.
184 | *
185 | * @param binaryArgs - Args object serialized as a string containing:
186 | * - the owner's account (address)
187 | * - the spender's account (address).
188 | */
189 | export function allowance(binaryArgs: StaticArray): StaticArray {
190 | const args = new Args(binaryArgs);
191 | const owner = new Address(
192 | args.nextString().expect('owner argument is missing or invalid'),
193 | );
194 | const spenderAddress = new Address(
195 | args.nextString().expect('spenderAddress argument is missing or invalid'),
196 | );
197 |
198 | return u256ToBytes(_allowance(owner, spenderAddress));
199 | }
200 |
201 | /**
202 | * Increases the allowance of the spender on the owner's account by the amount.
203 | *
204 | * This function can only be called by the owner.
205 | *
206 | * @param binaryArgs - Args object serialized as a string containing:
207 | * - the spender's account (address);
208 | * - the amount (u256).
209 | */
210 | export function increaseAllowance(binaryArgs: StaticArray): void {
211 | const owner = Context.caller();
212 |
213 | const args = new Args(binaryArgs);
214 | const spenderAddress = new Address(
215 | args.nextString().expect('spenderAddress argument is missing or invalid'),
216 | );
217 | const amount = args
218 | .nextU256()
219 | .expect('amount argument is missing or invalid');
220 |
221 | // @ts-ignore
222 | let newAllowance = _allowance(owner, spenderAddress) + amount;
223 | if (newAllowance < amount) {
224 | newAllowance = u256.Max;
225 | }
226 |
227 | _approve(owner, spenderAddress, newAllowance);
228 |
229 | generateEvent(APPROVAL_EVENT_NAME);
230 | }
231 |
232 | /**
233 | * Decreases the allowance of the spender the on owner's account by the amount.
234 | *
235 | * This function can only be called by the owner.
236 | *
237 | * @param binaryArgs - Args object serialized as a string containing:
238 | * - the spender's account (address);
239 | * - the amount (u256).
240 | */
241 | export function decreaseAllowance(binaryArgs: StaticArray): void {
242 | const owner = Context.caller();
243 |
244 | const args = new Args(binaryArgs);
245 | const spenderAddress = new Address(
246 | args.nextString().expect('spenderAddress argument is missing or invalid'),
247 | );
248 | const amount = args
249 | .nextU256()
250 | .expect('amount argument is missing or invalid');
251 |
252 | const current = _allowance(owner, spenderAddress);
253 |
254 | let newAllowance = u256.Zero;
255 |
256 | if (current > amount) {
257 | // @ts-ignore
258 | newAllowance = current - amount;
259 | }
260 |
261 | _approve(owner, spenderAddress, newAllowance);
262 |
263 | generateEvent(APPROVAL_EVENT_NAME);
264 | }
265 |
266 | /**
267 | * Transfers token ownership from the owner's account to the recipient's account
268 | * using the spender's allowance.
269 | *
270 | * This function can only be called by the spender.
271 | * This function is atomic:
272 | * - both allowance and transfer are executed if possible;
273 | * - or if allowance or transfer is not possible, both are discarded.
274 | *
275 | * @param binaryArgs - Args object serialized as a string containing:
276 | * - the owner's account (address);
277 | * - the recipient's account (address);
278 | * - the amount (u256).
279 | */
280 | export function transferFrom(binaryArgs: StaticArray): void {
281 | const spenderAddress = Context.caller();
282 |
283 | const args = new Args(binaryArgs);
284 | const owner = new Address(
285 | args.nextString().expect('ownerAddress argument is missing or invalid'),
286 | );
287 | const recipient = new Address(
288 | args.nextString().expect('recipientAddress argument is missing or invalid'),
289 | );
290 | const amount = args
291 | .nextU256()
292 | .expect('amount argument is missing or invalid');
293 |
294 | const spenderAllowance = _allowance(owner, spenderAddress);
295 |
296 | assert(
297 | spenderAllowance >= amount,
298 | 'transferFrom failed: insufficient allowance',
299 | );
300 |
301 | _transfer(owner, recipient, amount);
302 |
303 | // @ts-ignore
304 | _approve(owner, spenderAddress, spenderAllowance - amount);
305 |
306 | generateEvent(TRANSFER_EVENT_NAME);
307 | }
308 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/MRC721/__tests__/MRC721Enumerable-internals.spec.ts:
--------------------------------------------------------------------------------
1 | import { resetStorage, setDeployContext } from '@massalabs/massa-as-sdk';
2 | import { u256 } from 'as-bignum/assembly';
3 | import {
4 | _update,
5 | _balanceOf,
6 | _totalSupply,
7 | _constructor,
8 | _ownerOf,
9 | _transferFrom,
10 | _decreaseTotalSupply,
11 | _increaseTotalSupply,
12 | } from '../enumerable/MRC721Enumerable-internals';
13 | import { getOwnedTokens } from './helpers';
14 |
15 | const caller = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
16 | const owner1 = caller;
17 | const owner2 = 'AU178qZCfaNXkz9tQiXJcVfAEnYGJ27UoNtFFJh3BiT8jTfY8P2D';
18 | const zeroAddress = '';
19 | const tokenIds = [
20 | u256.fromU32(1),
21 | u256.fromU32(2),
22 | u256.fromU32(3),
23 | u256.fromU32(4),
24 | u256.fromU32(5),
25 | ];
26 |
27 | const NFTName = 'MASSA_NFT';
28 | const NFTSymbol = 'NFT';
29 |
30 | function mint(to: string, tokenId: u256): void {
31 | _update(to, tokenId, zeroAddress);
32 | }
33 |
34 | function transfer(from: string, to: string, tokenId: u256): void {
35 | _update(to, tokenId, from);
36 | }
37 |
38 | function burn(owner: string, tokenId: u256): void {
39 | _update(zeroAddress, tokenId, owner);
40 | }
41 |
42 | describe('NFT Enumerable Internals', () => {
43 | beforeEach(() => {
44 | resetStorage();
45 | setDeployContext(caller);
46 | _constructor(NFTName, NFTSymbol);
47 | });
48 |
49 | describe('Initialization', () => {
50 | it('should have zero total supply initially', () => {
51 | expect(_totalSupply()).toStrictEqual(u256.Zero);
52 | });
53 | });
54 |
55 | describe('Total Supply Management', () => {
56 | it('should update total supply when token is minted', () => {
57 | mint(owner1, tokenIds[0]);
58 | expect(_totalSupply()).toStrictEqual(u256.One);
59 | });
60 |
61 | it('should update total supply when token is burned', () => {
62 | mint(owner1, tokenIds[0]);
63 | expect(_totalSupply()).toStrictEqual(u256.One);
64 | burn(owner1, tokenIds[0]);
65 | expect(_totalSupply()).toStrictEqual(u256.Zero);
66 | });
67 |
68 | it('should not allow total supply to exceed u256.Max', () => {
69 | // Set total supply to u256.Max - 1
70 | // @ts-ignore
71 | const nearMaxSupply = u256.Max - u256.One;
72 | // @ts-ignore
73 | _increaseTotalSupply(nearMaxSupply);
74 | // @ts-ignore
75 | expect(_totalSupply()).toStrictEqual(nearMaxSupply);
76 |
77 | // Mint one more token should succeed (totalSupply = u256.Max)
78 | mint(owner1, tokenIds[0]);
79 | expect(_totalSupply()).toStrictEqual(u256.Max);
80 |
81 | // Minting another token should fail due to overflow
82 | expect(() => {
83 | _increaseTotalSupply(u256.One);
84 | }).toThrow('Total supply overflow'); // Ensure your contract throws this exact error
85 | });
86 |
87 | it('should not allow total supply to underflow', () => {
88 | // Ensure total supply is zero
89 | expect(_totalSupply()).toStrictEqual(u256.Zero);
90 |
91 | // Attempt to decrease supply by 1 should fail
92 | expect(() => {
93 | _decreaseTotalSupply(u256.One);
94 | }).toThrow('Total supply underflow'); // Ensure your contract throws this exact error
95 |
96 | // Set total supply to 1
97 | _increaseTotalSupply(u256.One);
98 | expect(_totalSupply()).toStrictEqual(u256.One);
99 |
100 | // Decrease supply by 1 should succeed
101 | _decreaseTotalSupply(u256.One);
102 | expect(_totalSupply()).toStrictEqual(u256.Zero);
103 |
104 | // Attempt to decrease supply by another 1 should fail
105 | expect(() => {
106 | _decreaseTotalSupply(u256.One);
107 | }).toThrow('Total supply underflow'); // Ensure your contract throws this exact error
108 | });
109 | });
110 |
111 | describe('Owner Token Enumeration', () => {
112 | it('should return correct balances and owned tokens after minting', () => {
113 | mint(owner1, tokenIds[0]);
114 | mint(owner1, tokenIds[1]);
115 | mint(owner2, tokenIds[2]);
116 |
117 | expect(_balanceOf(owner1)).toStrictEqual(u256.fromU32(2));
118 | expect(_balanceOf(owner2)).toStrictEqual(u256.One);
119 |
120 | const owner1Tokens = getOwnedTokens(owner1);
121 | expect(owner1Tokens.length).toBe(2);
122 | expect(owner1Tokens).toContainEqual(tokenIds[0]);
123 | expect(owner1Tokens).toContainEqual(tokenIds[1]);
124 |
125 | const owner2Tokens = getOwnedTokens(owner2);
126 | expect(owner2Tokens.length).toBe(1);
127 | expect(owner2Tokens).toContainEqual(tokenIds[2]);
128 | });
129 |
130 | it('should update balances and tokens after transfer', () => {
131 | mint(owner1, tokenIds[0]);
132 | mint(owner1, tokenIds[1]);
133 |
134 | transfer(owner1, owner2, tokenIds[0]);
135 |
136 | expect(_balanceOf(owner1)).toStrictEqual(u256.One);
137 | expect(_balanceOf(owner2)).toStrictEqual(u256.One);
138 |
139 | // Verify ownership
140 | expect(_ownerOf(tokenIds[0])).toStrictEqual(owner2);
141 | expect(_ownerOf(tokenIds[1])).toStrictEqual(owner1);
142 |
143 | // Verify owned tokens
144 | const owner1Tokens = getOwnedTokens(owner1);
145 | expect(owner1Tokens.length).toBe(1);
146 | expect(owner1Tokens).toContainEqual(tokenIds[1]);
147 |
148 | const owner2Tokens = getOwnedTokens(owner2);
149 | expect(owner2Tokens.length).toBe(1);
150 | expect(owner2Tokens).toContainEqual(tokenIds[0]);
151 | });
152 | });
153 |
154 | describe('Token Transfers and Ownership', () => {
155 | it('should update ownership after transfer', () => {
156 | mint(owner1, tokenIds[0]);
157 | transfer(owner1, owner2, tokenIds[0]);
158 |
159 | expect(_ownerOf(tokenIds[0])).toStrictEqual(owner2);
160 | expect(_balanceOf(owner1)).toStrictEqual(u256.Zero);
161 | expect(_balanceOf(owner2)).toStrictEqual(u256.One);
162 |
163 | // Verify owned tokens
164 | const owner1Tokens = getOwnedTokens(owner1);
165 | expect(owner1Tokens.length).toBe(0);
166 |
167 | const owner2Tokens = getOwnedTokens(owner2);
168 | expect(owner2Tokens.length).toBe(1);
169 | expect(owner2Tokens).toContainEqual(tokenIds[0]);
170 | });
171 |
172 | it('should handle transfer to owner with balance', () => {
173 | mint(owner1, tokenIds[0]);
174 | mint(owner2, tokenIds[1]);
175 |
176 | transfer(owner1, owner2, tokenIds[0]);
177 |
178 | expect(_ownerOf(tokenIds[0])).toStrictEqual(owner2);
179 | expect(_balanceOf(owner1)).toStrictEqual(u256.Zero);
180 | expect(_balanceOf(owner2)).toStrictEqual(u256.fromU32(2));
181 |
182 | // Verify owned tokens
183 | const owner1Tokens = getOwnedTokens(owner1);
184 | expect(owner1Tokens.length).toBe(0);
185 |
186 | const owner2Tokens = getOwnedTokens(owner2);
187 | expect(owner2Tokens.length).toBe(2);
188 | expect(owner2Tokens).toContainEqual(tokenIds[0]);
189 | expect(owner2Tokens).toContainEqual(tokenIds[1]);
190 | });
191 | });
192 |
193 | describe('Token Burning and Ownership', () => {
194 | it('should update balances and tokens after burning', () => {
195 | mint(owner1, tokenIds[0]);
196 | mint(owner1, tokenIds[1]);
197 |
198 | burn(owner1, tokenIds[0]);
199 |
200 | expect(_balanceOf(owner1)).toStrictEqual(u256.One);
201 | expect(_totalSupply()).toStrictEqual(u256.One);
202 |
203 | // Verify that accessing the burned token's owner returns empty string
204 | expect(_ownerOf(tokenIds[0])).toStrictEqual('');
205 |
206 | // Verify owned tokens
207 | const owner1Tokens = getOwnedTokens(owner1);
208 | expect(owner1Tokens.length).toBe(1);
209 | expect(owner1Tokens).toContainEqual(tokenIds[1]);
210 | });
211 |
212 | it('should handle burning multiple tokens', () => {
213 | mint(owner1, tokenIds[0]);
214 | mint(owner1, tokenIds[1]);
215 |
216 | burn(owner1, tokenIds[0]);
217 | burn(owner1, tokenIds[1]);
218 |
219 | expect(_balanceOf(owner1)).toStrictEqual(u256.Zero);
220 | expect(_totalSupply()).toStrictEqual(u256.Zero);
221 |
222 | // Verify owned tokens
223 | const owner1Tokens = getOwnedTokens(owner1);
224 | expect(owner1Tokens.length).toBe(0);
225 | });
226 | });
227 |
228 | describe('Complex Token Interactions', () => {
229 | it('should handle mints, transfers, and burns correctly', () => {
230 | // Mint tokens to owner1 and owner2
231 | mint(owner1, tokenIds[0]);
232 | mint(owner1, tokenIds[1]);
233 | mint(owner2, tokenIds[2]);
234 |
235 | // Transfer tokenIds[1] from owner1 to owner2
236 | transfer(owner1, owner2, tokenIds[1]);
237 |
238 | // Burn tokenIds[0] owned by owner1
239 | burn(owner1, tokenIds[0]);
240 |
241 | // Verify total supply
242 | expect(_totalSupply()).toStrictEqual(u256.fromU32(2));
243 |
244 | // Verify balances
245 | expect(_balanceOf(owner1)).toStrictEqual(u256.Zero);
246 | expect(_balanceOf(owner2)).toStrictEqual(u256.fromU32(2));
247 |
248 | // Verify ownership
249 | expect(_ownerOf(tokenIds[1])).toStrictEqual(owner2);
250 | expect(_ownerOf(tokenIds[2])).toStrictEqual(owner2);
251 |
252 | // Verify that accessing the burned token's owner returns empty string
253 | expect(_ownerOf(tokenIds[0])).toStrictEqual('');
254 |
255 | // Verify owned tokens
256 | const owner1Tokens = getOwnedTokens(owner1);
257 | expect(owner1Tokens.length).toBe(0);
258 |
259 | const owner2Tokens = getOwnedTokens(owner2);
260 | expect(owner2Tokens.length).toBe(2);
261 | expect(owner2Tokens).toContainEqual(tokenIds[1]);
262 | expect(owner2Tokens).toContainEqual(tokenIds[2]);
263 | });
264 | });
265 |
266 | describe('Error Handling and Boundary Conditions', () => {
267 | it('should not mint existing token ID', () => {
268 | mint(owner1, tokenIds[0]);
269 |
270 | expect(() => {
271 | mint(owner1, tokenIds[0]);
272 | }).toThrow('Token already minted');
273 | });
274 |
275 | it('should not transfer nonexistent token', () => {
276 | expect(() => {
277 | transfer(owner1, owner2, tokenIds[0]);
278 | }).toThrow('Nonexistent token');
279 | });
280 |
281 | it('should not transfer to an invalid address', () => {
282 | mint(owner1, tokenIds[0]);
283 |
284 | expect(() => {
285 | _transferFrom(owner1, zeroAddress, tokenIds[0]);
286 | }).toThrow('Unauthorized to');
287 | });
288 |
289 | it('should not burn nonexistent token', () => {
290 | expect(() => {
291 | burn(owner1, tokenIds[0]);
292 | }).toThrow('Nonexistent token');
293 | });
294 |
295 | it('should have zero total supply after all tokens are burned', () => {
296 | mint(owner1, tokenIds[0]);
297 | burn(owner1, tokenIds[0]);
298 |
299 | expect(_totalSupply()).toStrictEqual(u256.Zero);
300 |
301 | // Check token ownership has been cleared
302 | expect(_ownerOf(tokenIds[0])).toStrictEqual(zeroAddress);
303 |
304 | // Verify owned tokens
305 | const owner1Tokens = getOwnedTokens(owner1);
306 | expect(owner1Tokens.length).toBe(0);
307 | });
308 |
309 | it('should mint a token with id=u256.Max', () => {
310 | mint(owner1, u256.Max);
311 | expect(_totalSupply()).toStrictEqual(u256.One);
312 | expect(_balanceOf(owner1)).toStrictEqual(u256.One);
313 | expect(_ownerOf(u256.Max)).toStrictEqual(owner1);
314 | const owner1Tokens = getOwnedTokens(owner1);
315 | expect(owner1Tokens.length).toBe(1);
316 | expect(owner1Tokens).toContainEqual(u256.Max);
317 | });
318 | });
319 | });
320 |
--------------------------------------------------------------------------------
/smart-contracts/assembly/contracts/dns/__tests__/dns.spec.ts:
--------------------------------------------------------------------------------
1 | import { blackListKey, dns1_getBlacklisted } from './../dns';
2 | import {
3 | dns1_setResolver,
4 | dns1_isDescriptionValid,
5 | dns1_resolver,
6 | dns1_isDnsValid,
7 | dns1_addWebsitesToBlackList,
8 | dns1_isBlacklisted,
9 | dns1_deleteEntryFromDNS,
10 | dns1_deleteEntriesFromDNS,
11 | dns1_getOwnerWebsiteList,
12 | constructor,
13 | } from '../dns';
14 | import { Storage, mockAdminContext } from '@massalabs/massa-as-sdk';
15 | import { Args, byteToBool, stringToBytes } from '@massalabs/as-types';
16 | import {
17 | changeCallStack,
18 | resetStorage,
19 | } from '@massalabs/massa-as-sdk/assembly/vm-mock/storage';
20 | import { ownerAddress, setOwner } from '../../utils';
21 |
22 | // address of admin caller set in vm-mock. must match with adminAddress of @massalabs/massa-as-sdk/vm-mock/vm.js
23 | const deployerAddress = 'AU12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
24 | // address of the contract set in vm-mock. must match with contractAddr of @massalabs/massa-as-sdk/vm-mock/vm.js
25 | const contractAddr = 'AS12BqZEQ6sByhRLyEuf0YbQmcF2PsDdkNNG1akBJu9XcjZA1eT';
26 |
27 | const websiteAddr = 'A12UBnqTHDQALpocVBnkPNy7y5CndUJQTLutaVDDFgMJcq5kQiKq';
28 |
29 | const dnsAdmin = 'AU1qDAxGJ387ETi9JRQzZWSPKYq4YPXrFvdiE4VoXUaiAt38JFEC';
30 |
31 | const user1Addr = 'AU125TiSrnD2YatYfEyRAWnBdD7TEuVbvGFkFgDuaYc2bdKyqKtb';
32 |
33 | function switchUser(user: string): void {
34 | changeCallStack(user + ' , ' + contractAddr);
35 | }
36 |
37 | beforeAll(() => {
38 | resetStorage();
39 | mockAdminContext(true);
40 | });
41 |
42 | describe('DNS contract tests', () => {
43 | test('constructor', () => {
44 | constructor([]);
45 | expect(ownerAddress([])).toStrictEqual(stringToBytes(deployerAddress));
46 | });
47 |
48 | test('change dns admin', () => {
49 | const serializedDnsAdmin = new Args().add(dnsAdmin).serialize();
50 | setOwner(serializedDnsAdmin);
51 | expect(ownerAddress([])).toStrictEqual(stringToBytes(dnsAdmin));
52 | });
53 |
54 | test('description length', () => {
55 | const validDescriptionLessThan280 =
56 | 'Valid description with less than 280 characters.';
57 | const invalidDescriptionMoreThan280 =
58 | 'Invalid description exceeding the maximum limit of 280 characters.' +
59 | 'x'.repeat(300);
60 | const validDescriptionExactly280 = 'x'.repeat(280);
61 |
62 | expect(dns1_isDescriptionValid(validDescriptionLessThan280)).toBe(true);
63 | expect(dns1_isDescriptionValid(invalidDescriptionMoreThan280)).toBe(false);
64 | expect(dns1_isDescriptionValid(validDescriptionExactly280)).toBe(true);
65 | });
66 |
67 | test('invalid dns entry', () => {
68 | expect(() => {
69 | const setResolverArgs = new Args()
70 | .add('invalid dns entry')
71 | .add(deployerAddress)
72 | .serialize();
73 | dns1_setResolver(setResolverArgs);
74 | }).toThrow();
75 | });
76 |
77 | describe('DNS Name Validity', () => {
78 | test('valid DNS name', () => {
79 | const validDnsEntries = [
80 | 'example',
81 | 'example123',
82 | 'example_name',
83 | 'example-name',
84 | 'example_name-123',
85 | ];
86 |
87 | validDnsEntries.forEach((entry) => {
88 | expect(dns1_isDnsValid(entry)).toBe(true);
89 | });
90 | });
91 |
92 | test('invalid DNS name', () => {
93 | const invalidDnsEntries = [
94 | 'example@',
95 | 'example!',
96 | 'example$',
97 | 'example%',
98 | 'example^',
99 | 'example&',
100 | 'example*',
101 | 'example(',
102 | 'example)',
103 | 'example=',
104 | ];
105 |
106 | invalidDnsEntries.forEach((entry) => {
107 | expect(dns1_isDnsValid(entry)).toBe(false);
108 | });
109 | });
110 | });
111 |
112 | test('create dns entry', () => {
113 | switchUser(user1Addr);
114 |
115 | const name = 'test';
116 | const desc = 'website description';
117 |
118 | const setResolverArgs = new Args()
119 | .add(name)
120 | .add(websiteAddr)
121 | .add(desc)
122 | .serialize();
123 |
124 | dns1_setResolver(setResolverArgs);
125 |
126 | const stored = new Args(dns1_resolver(new Args().add(name).serialize()));
127 |
128 | expect(stored.nextString().unwrap()).toBe(websiteAddr, 'wrong websiteAddr');
129 | expect(stored.nextString().unwrap()).toBe(user1Addr, 'wrong owner address');
130 | expect(stored.nextString().unwrap()).toBe(desc, 'wrong description');
131 | });
132 |
133 | test('add dns entry with empty description', () => {
134 | switchUser(deployerAddress);
135 |
136 | const name = 'my-website';
137 | const desc = '';
138 | const setResolverArgs = new Args()
139 | .add(name)
140 | .add(websiteAddr)
141 | .add(desc)
142 | .serialize();
143 |
144 | dns1_setResolver(setResolverArgs);
145 |
146 | const stored = new Args(dns1_resolver(new Args().add(name).serialize()));
147 |
148 | expect(stored.nextString().unwrap()).toBe(websiteAddr);
149 | expect(stored.nextString().unwrap()).toBe(deployerAddress);
150 | expect(stored.nextString().unwrap()).toBe(desc);
151 | });
152 |
153 | test('try to book an already booked DNS', () => {
154 | expect(() => {
155 | const setResolverArgs = new Args()
156 | .add('test')
157 | .add(deployerAddress)
158 | .serialize();
159 | dns1_setResolver(setResolverArgs);
160 | }).toThrow();
161 | });
162 |
163 | describe('DNS blacklist tests', () => {
164 | const name = 'backlisted';
165 | const desc = 'backlisted website description';
166 |
167 | beforeAll(() => {
168 | // set a dns entry
169 | switchUser(user1Addr);
170 | const setResolverArgs = new Args()
171 | .add(name)
172 | .add(websiteAddr)
173 | .add(desc)
174 | .serialize();
175 | dns1_setResolver(setResolverArgs);
176 | });
177 |
178 | test('blacklist name not being admin', () => {
179 | switchUser(user1Addr);
180 | expect(() => {
181 | const blacklistArgs = new Args().add(['blacklist1']).serialize();
182 | dns1_addWebsitesToBlackList(blacklistArgs);
183 | }).toThrow();
184 | });
185 |
186 | test('add multiple websites to blacklist being dns admin', () => {
187 | switchUser(dnsAdmin);
188 | const websiteNames = ['flappy', 'example', 'website'];
189 | const args = new Args().add(websiteNames);
190 | const websiteNamesBinary = args.serialize();
191 |
192 | // Clear existing blacklist (if any)
193 | Storage.set(blackListKey, new Args().add([] as string[]).serialize());
194 |
195 | // Call the addWebsitesToBlackList function with the list of website names
196 | dns1_addWebsitesToBlackList(websiteNamesBinary);
197 |
198 | // Retrieve the updated blacklist from storage
199 | const updatedBlacklist = new Args(dns1_getBlacklisted())
200 | .nextStringArray()
201 | .unwrap();
202 |
203 | // Check if the website names have been added to the blacklist
204 | expect(updatedBlacklist).toStrictEqual(websiteNames);
205 |
206 | // Call again the addWebsitesToBlackList function with the same list of website names
207 | // To check that we don't blacklist twice
208 | dns1_addWebsitesToBlackList(websiteNamesBinary);
209 |
210 | // Retrieve the updated blacklist from storage
211 | const finalBlacklist = new Args(dns1_getBlacklisted())
212 | .nextStringArray()
213 | .unwrap();
214 |
215 | // Check if we always get the same website names and not twice
216 | expect(finalBlacklist).toStrictEqual(websiteNames);
217 |
218 | // Test the isBlacklisted function for a blacklisted website name
219 | const blacklistedName = 'example';
220 |
221 | // Expect the isBlacklisted return to be true since 'example' is blacklisted
222 | expect(
223 | byteToBool(
224 | dns1_isBlacklisted(new Args().add(blacklistedName).serialize()),
225 | ),
226 | ).toBe(true);
227 |
228 | // Test the isBlacklisted function for a non-blacklisted website name
229 | const nonblacklistedName = 'example2';
230 |
231 | // Expect the isBlacklisted return to be false since 'example2' is non blacklisted
232 | expect(
233 | byteToBool(
234 | dns1_isBlacklisted(new Args().add(nonblacklistedName).serialize()),
235 | ),
236 | ).toBe(false);
237 | });
238 |
239 | test('blacklist names and try to set resolver with a blacklisted name', () => {
240 | switchUser(dnsAdmin);
241 | // Blacklist a list of names
242 | const blacklistNames = ['blacklist1', 'blacklist2', 'blacklist3'];
243 | const blacklistArgs = new Args().add(blacklistNames);
244 | const blacklistArgsBinary = blacklistArgs.serialize();
245 | dns1_addWebsitesToBlackList(blacklistArgsBinary);
246 |
247 | // Try to set resolver with a blacklisted name
248 | const blacklistedName = 'blacklist1';
249 |
250 | expect(() => {
251 | const setResolverArgs = new Args()
252 | .add(blacklistedName)
253 | .add(deployerAddress)
254 | .add('')
255 | .serialize();
256 | dns1_setResolver(setResolverArgs);
257 | }).toThrow();
258 | });
259 | });
260 | describe('deleteEntriesFromDNS', () => {
261 | test('delete a single DNS entry as not website owner', () => {
262 | switchUser(dnsAdmin);
263 | expect(() => {
264 | const args = new Args().add('test').serialize();
265 | dns1_deleteEntryFromDNS(args);
266 | }).toThrow();
267 | });
268 |
269 | test('delete DNS entry as website owner', () => {
270 | switchUser(user1Addr);
271 |
272 | // Create a DNS entry for testing
273 | const names = ['test-website'];
274 | const websiteAddr =
275 | 'A1qth3jk2Yb5FcP9NYJh8MuFqEsyzRwqWGruL4uxATRrpPhLPVus';
276 | const description = 'Test website description';
277 |
278 | const setResolverArgs = new Args()
279 | .add(names[0])
280 | .add(websiteAddr)
281 | .add(description)
282 | .serialize();
283 |
284 | dns1_setResolver(setResolverArgs);
285 |
286 | // Ensure that the DNS entry has been created
287 | const storedEntry = new Args(
288 | dns1_resolver(new Args().add(names[0]).serialize()),
289 | );
290 | expect(storedEntry.nextString().unwrap()).toBe(websiteAddr);
291 | expect(storedEntry.nextString().unwrap()).toBe(user1Addr);
292 | expect(storedEntry.nextString().unwrap()).toBe(description);
293 |
294 | const currentList = new Args(
295 | dns1_getOwnerWebsiteList(new Args().add(user1Addr).serialize()),
296 | )
297 | .nextString()
298 | .unwrap();
299 |
300 | expect(currentList).toBe('test,backlisted,test-website');
301 |
302 | // Delete the DNS entry using deleteEntriesFromDNS
303 | switchUser(user1Addr);
304 | const deleteArgs = new Args().add(names).serialize();
305 | dns1_deleteEntriesFromDNS(deleteArgs);
306 |
307 | const stored = new Args(
308 | dns1_resolver(new Args().add(names[0]).serialize()),
309 | );
310 |
311 | // Ensure that the DNS entry has been deleted
312 | expect(stored.nextString().unwrap()).toBeNull;
313 | // The owner's list should contains only 2 entries since 'test-website' has been deleted
314 | const newList = new Args(
315 | dns1_getOwnerWebsiteList(new Args().add(user1Addr).serialize()),
316 | )
317 | .nextString()
318 | .unwrap();
319 | expect(newList).toBe('test,backlisted'); // The owner's list should contains only 2 entries
320 | });
321 | });
322 | });
323 |
--------------------------------------------------------------------------------