Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example contracts showcase #294

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/contract-showcase-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Contract Showcase CI

on:
pull_request:
paths:
- contracts_showcase/**

defaults:
run:
working-directory: ./contracts_showcase/

jobs:
collect_dirs:
runs-on: ubuntu-latest
outputs:
dirs: ${{ steps.dirs.outputs.dirs }}
steps:
- uses: actions/checkout@v2
- id: dirs
run: echo "dirs=$(ls -d ./*/ | jq --raw-input --slurp --compact-output 'split("\n")[:-1]')" >> ${GITHUB_OUTPUT}

run_tests:
needs: collect_dirs
runs-on: ubuntu-latest
strategy:
matrix:
dir: ${{ fromJson(needs.collect_dirs.outputs.dirs) }}
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Install node
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"

- name: Install dependencies
working-directory: ./contracts_showcase/${{ matrix.dir }}
run: npm install

- name: Test
working-directory: ./contracts_showcase/${{ matrix.dir }}
run: npm run start
env:
JSON_RPC_URL_PUBLIC: ${{ secrets.JSON_RPC_URL_PUBLIC }}
WALLET_SECRET_KEY: ${{ secrets.WALLET_PRIVATE_KEY }}
7 changes: 7 additions & 0 deletions contracts_showcase/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## How to add a new contract showcase

Copy/Paste one of the sub-folder here.

In the `assembly/contracts` copy the source code of your contract and in `interaction.ts`, deploy your contract and make some possible interactions with it using massa-web3. You can use the examples from the other sub-folder to create yours.

Try trigger
7 changes: 7 additions & 0 deletions contracts_showcase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Contracts showcase

In this section you will find examples of smart contracts showcasing usage of objects available in the `massa-as-sdk` package.

Each of the examples has a `assembly/contracts` folder containing the smart contract codes and a `interaction.ts` file that contains all the deployment and interaction with the smart contract.

You can go to any folder copy the `.env.example` to a `.env` add your secret key, update the RPC if you don't want to use buildnet and run `npm run start`. It will run all the actions in the `interaction.ts`
2 changes: 2 additions & 0 deletions contracts_showcase/accesscontrol/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
WALLET_SECRET_KEY=
JSON_RPC_URL_PUBLIC=https://buildnet.massa.net/api/v2
2 changes: 2 additions & 0 deletions contracts_showcase/accesscontrol/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
node_modules/
21 changes: 21 additions & 0 deletions contracts_showcase/accesscontrol/asconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"targets": {
"debug": {
"outFile": "build/main.wasm",
"sourceMap": true,
"debug": true
},
"release": {
"outFile": "build/main.wasm",
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 0,
"converge": false,
"noAssert": false
}
},
"options": {
"exportRuntime": true,
"bindings": "esm"
}
}
41 changes: 41 additions & 0 deletions contracts_showcase/accesscontrol/assembly/contracts/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Args, stringToBytes } from "@massalabs/as-types";
import {
Address,
Storage,
Context,
caller,
} from "@massalabs/massa-as-sdk";
import { AccessControl } from "@massalabs/massa-as-sdk/assembly/security";

const controller = new AccessControl<u8>(1);

const ADMIN = controller.newPermission("admin");
const USER = controller.newPermission("user");

export function constructor(argsSer: StaticArray<u8>): void {
if (!Context.isDeployingContract()) {
return;
}

const args = new Args(argsSer);
const admin = new Address(args.nextString().unwrap());
const user = new Address(args.nextString().unwrap());
controller.grantPermission(ADMIN, admin);
controller.grantPermission(USER, admin);
controller.grantPermission(USER, user);
Storage.set("admin", admin.toString());
}

export function changeAdmin(argsSer: StaticArray<u8>): void {
const args = new Args(argsSer);
const newAdmin = new Address(args.nextString().unwrap());
controller.mustHavePermission(ADMIN, caller());
controller.grantPermission(ADMIN, newAdmin);
controller.revokePermission(ADMIN, caller());
Storage.set("admin", newAdmin.toString());
}

export function getAdmin(): StaticArray<u8> {
controller.mustHaveAnyPermission(USER | ADMIN, caller());
return stringToBytes(Storage.get("admin"));
}
254 changes: 254 additions & 0 deletions contracts_showcase/accesscontrol/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import {
IDeploymentInfo,
ISCData,
deploySC,
} from "@massalabs/massa-sc-deployer";
import {
CHAIN_ID,
MAX_GAS_DEPLOYMENT,
WalletClient,
fromMAS,
Args,
EventPoller,
Client,
ClientFactory,
ProviderType,
IProvider,
IEventFilter,
MAX_GAS_CALL,
bytesToStr,
IEvent,
EOperationStatus,
} from "@massalabs/massa-web3";
import path from "path";
import { fileURLToPath } from "url";
import { readFileSync } from "fs";
import * as dotenv from "dotenv";

// Load environment variables from .env file
dotenv.config();

// Helper function to get environment variable from env
function getEnvVariable(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing ${key} in .env file`);
}
return value;
}

// Helper function to read bytecode from file
function getByteCode(folderName: string, fileName: string): Buffer {
// Obtain the current file name and directory paths
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
return readFileSync(path.join(__dirname, folderName, fileName));
}

// Helper function to get the address from the deployment result
async function getEventsFromOp(opId: string): Promise<IEvent[]> {
// Wait for the operation to be speculative success or Speculative failure
// First promise that resolves stops the execution
await web3Client
.smartContracts()
.awaitMultipleRequiredOperationStatus(opId, [
EOperationStatus.SPECULATIVE_SUCCESS,
EOperationStatus.SPECULATIVE_ERROR,
]);

// Filter to get the deployment event
const eventsFilter = {
start: null,
end: null,
original_caller_address: null,
original_operation_id: opId,
emitter_address: null,
is_final: false,
} as IEventFilter;

// Get the deployment event
const deploymentEvents = await EventPoller.getEventsOnce(
eventsFilter,
web3Client
);
return deploymentEvents;
}

// Get environment variables
const publicApi = getEnvVariable("JSON_RPC_URL_PUBLIC");
const secretKey = getEnvVariable("WALLET_SECRET_KEY");

// Define deployment parameters
const chainId = CHAIN_ID.BuildNet; // Choose the chain ID corresponding to the network you want to deploy to
const maxGas = 2980167295n; // Gas for deployment Default is the maximum gas allowed for deployment
const fees = 0n; // Fees to be paid for deployment. Default is 0

// Create an account using the secret key
const deployerAccount = await WalletClient.getAccountFromSecretKey(secretKey);

// Create a random account for the user
const userAccount = await WalletClient.walletGenerateNewAccount();

// Create a web3 client
const web3Client: Client = await ClientFactory.createCustomClient(
[
{ url: publicApi, type: ProviderType.PUBLIC } as IProvider,
// Using a placeholder IP since the script doesn't require a real one, though massa-web3 does.
{ url: publicApi, type: ProviderType.PRIVATE } as IProvider,
],
chainId,
true,
deployerAccount
);

// Send some coins to the user account
web3Client.wallet().sendTransaction({
fee: 0n,
amount: fromMAS(1),
recipientAddress: userAccount.address!,
});

// Deploy the smart contract
const deploymentResult = await deploySC(
publicApi, // JSON RPC URL
deployerAccount, // account deploying the smart contract(s)
[
{
data: getByteCode("build", "main.wasm"), // smart contract bytecode
coins: fromMAS(0.1), // coins for deployment
args: new Args()
.addString(deployerAccount.address!)
.addString(userAccount.address!), // arguments for deployment
} as ISCData,
// Additional smart contracts can be added here for deployment
],
chainId, // chain ID
fees, // fees for deployment
maxGas, // maximum gas for deployment
false // wait for the deployment finality
);

// Get the address of the deployed smart contract
const deploymentEvents = await getEventsFromOp(deploymentResult.opId);
// Get the smart contract address from the deployment event
if (deploymentEvents.length === 0) {
throw new Error("No events found for the deployment");
}
const scAddress = deploymentEvents[0].data.split(": ")[1];
console.log(`Smart contract deployed at address: ${scAddress}`);

// Get the adminAddress
const adminAddress = bytesToStr(
(
await web3Client.smartContracts().readSmartContract({
targetAddress: scAddress,
targetFunction: "getAdmin",
parameter: new Args(),
maxGas: MAX_GAS_CALL,
callerAddress: deployerAccount.address!,
})
).returnValue
);

// Check if the admin address is the expected one
if (adminAddress !== deployerAccount.address) {
throw new Error("Admin address is not the expected one");
}

// Use the user account to interact with the smart contract
const web3ClientUser: Client = await ClientFactory.createCustomClient(
[
{ url: publicApi, type: ProviderType.PUBLIC } as IProvider,
// Using a placeholder IP since the script doesn't require a real one, though massa-web3 does.
{ url: publicApi, type: ProviderType.PRIVATE } as IProvider,
],
chainId,
true,
userAccount
);

console.log(
"Trying to change the admin address with the user account (not allowed)..."
);

// Try to change the admin address with the user account that is not the admin (should fail)
const opId = await web3ClientUser.smartContracts().callSmartContract({
targetAddress: scAddress,
functionName: "changeAdmin",
parameter: new Args().addString(userAccount.address!),
maxGas: MAX_GAS_CALL,
fee: 0n,
});

const callEvents = await getEventsFromOp(opId);
if (callEvents.length === 0) {
throw new Error("No events found for the call");
}
if (
!callEvents[0].data.includes(
"VM execution error: RuntimeError: Runtime error: error: User does not have 'admin' permission."
)
) {
throw new Error("The call should have failed");
} else {
console.log("The call to change admin from user failed as expected");
}

// Get the adminAddress
const adminAddress2 = bytesToStr(
(
await web3Client.smartContracts().readSmartContract({
targetAddress: scAddress,
targetFunction: "getAdmin",
parameter: new Args(),
maxGas: MAX_GAS_CALL,
callerAddress: deployerAccount.address!,
})
).returnValue
);

// Check if the admin address is the expected one
if (adminAddress2 !== deployerAccount.address) {
throw new Error("Admin address is not the expected one");
}

console.log(
"Trying to change the admin address with the deployer account (allowed)..."
);

// Change the admin address with the deployer account
const opId2 = await web3Client.smartContracts().callSmartContract({
targetAddress: scAddress,
functionName: "changeAdmin",
parameter: new Args().addString(userAccount.address!),
maxGas: MAX_GAS_CALL,
fee: 0n,
});

// Get the event for the call
const callEvents2 = await getEventsFromOp(opId2);
if (callEvents2.length !== 0) {
throw new Error("No events is expected for the call");
}

console.log("Admin address changed successfully");

// Get the adminAddress
const adminAddress3 = bytesToStr(
(
await web3Client.smartContracts().readSmartContract({
targetAddress: scAddress,
targetFunction: "getAdmin",
parameter: new Args(),
maxGas: MAX_GAS_CALL,
callerAddress: deployerAccount.address!,
})
).returnValue
);

// Check if the admin address is the expected one
if (adminAddress3 !== userAccount.address) {
throw new Error("Admin address is not the expected one");
}

process.exit(0);
Loading
Loading