Skip to main content

Getting Started

This guide explains how to create your first smart contract and deploy it on the Archethic testnet.

Setup​

Before creating a smart contract, set up your development environment to use WebAssembly (WASM).

You need to have NodeJS & npm installed

Prerequisites​

Ensure you have Node.js and npm installed.

Create a new project​

Run the following command to create a new project:

npm create @archethicjs/ae-contract-as my_contract

This creates a new project in the my_contract folder with the following structure:

my_contract/
├── assembly/
│ ├── index.ts # Main contract code
│ ├── tsconfig.json # Typescript configuration for AssemblyScript
├── tests
├── index.test.ts # Main test file
├── asconfig.json # AssemblyScript configuration
├── package.json # Project dependencies and scripts
├── README.md # Project documentation
└── tsconfig.json # TypeScript configuration

Install SDK​

Navigate to your project folder and install the SDK:

cd my_contract
npm install

Writing your first contract​

/assembly/index.ts
import {
ActionResult,
Context,
ContextWithParams,
TriggerType,
} from "@archethicjs/ae-contract-as/assembly";

// Define the contract state
class State {
counter: i32 = 0;
}

// Initialize the contract during creation
export function onInit(context: Context<State>): State {
return new State();
}

class IncArgs {
value!: u32;
}

// Define an action triggered by a transaction
@action(TriggerType.Transaction)
export function inc(context: ContextWithParams<State, IncArgs>): ActionResult<State> {
const state = context.state;

// Validate the input
if (context.arguments.value == 0)
throw new Error("increment value must be greater than 0")

// Update the state
state.counter += context.arguments.value;

// Return the updated state
return new ActionResult<State>().setState(state)
}

In this example, the inc function expects an argument to increment the contract's state with each incoming transaction.

Test & Simulation​

Contract tests simulate the execution of WASM modules as the blockchain runtime would. Use the tests folder to define test cases.

/tests/index.test.ts
import { describe, it, expect } from 'vitest'
import { readFileSync } from "fs";
import { getContract } from "@archethicjs/ae-contract-test";

describe("inc", () => {
it("should increment the state", async () => {
const wasmBuffer = readFileSync("./dist/contract.wasm");
const contract = await getContract(wasmBuffer);

// Increment from an undefined state
let result = contract.inc({ value: 1 });
expect(result?.state.counter).toBe(1);

// Increment from a computed state
result = contract.inc({ value: 1 });
expect(result?.state.counter).toBe(2);

// Increment with a predefined state
result = contract.inc({ value: 1 }, { state: { counter: 2 } });
expect(result?.state.counter).toBe(3);
});
});

Simulate state​

To simulate a specific state of the contract - in order to speed up the setup - you can hardcode it in the tests

Example of test simulating state
it("should increment with state", async () => {
const wasmBuffer = readFileSync("./dist/contract.wasm");
const contract = await getContract(wasmBuffer);

// Simulate a specific initial state
const result = contract.inc({ state: { counter: 2 } });
expect(result?.state.counter).toBe(3);
});

Simulate constants​

To simulate constants such as contract address, genesis, balance, etc.., you can hardcode them in the tests

Example of test simulating state
it("should display balance as input", async () => {
const wasmBuffer = readFileSync("./dist/contract.wasm");

const contract = await getContract(wasmBuffer);
const result = contract.currentBalance({
balance: {
uco: 100_000_000
}
})
expect(result).toBe('UCO: 100000000')
})

Mock Blockchain Calls​

Example of test using mock
it("should fetch balance", async () => {
const wasmBuffer = readFileSync("./dist/contract.wasm");
const contract = await getContract(wasmBuffer);

const address = new Address("0000583a6a1d9a536ed7f3c8e7f3981ed2f7ff8cb3c3f8f2b9eb0c862d81f75d28cb")

// We simulate getBalance call
const contract = await getContract(wasmBuffer, {
ioMocks: {
getBalance(requestedAddress: Address): Balance {
if (Address.compare(requestedAddress, address)) {
return { uco: 100_000_000, token: [] }
}
return { uco: 0, token: [] }
}
}
});

const result = contract.fetchBalance({ address: address })
expect(result).toBe('UCO: 100000000')
});

Deployment​

Configure your deployment environment by creating the following file:

/archethic.config.js
export default {
endpoint: process.env["ENDPOINT"] || "https://testnet.archethic.net",
seed: process.env["SEED"], // Optional if using the Archethic wallet
}

Then, we need to create a script to implement the deployment and execute the transactions

/scripts/deploy.js
import { getContext } from "@archethicjs/ae-contract-test"
import { Utils } from "@archethicjs/sdk"

async function main() {
const ctx = await getContext()
const fundingAccount = await ctx.getAccount()

// Request testnet funds
await fundingAccount.requestFaucet()

const contractAccount = ctx.getRandomAccount()

// Fund the contract's chain
const fundingTx = ctx.archethicClient.transaction.new()
.setType("transfer")
.addUCOTransfer(contractAccount.address, Utils.parseBigInt("1"))

console.log("Funding contract...")
await fundingAccount.sendTransaction(fundingTx)

console.log("Deploying contract...")
const transactionAddress = await ctx.deployContract(contractAccount)

console.log(`Contract deployed at: ${transactionAddress}`)
process.exit(0)
}

main().catch((error) => {
console.error(error);
process.exit(1)
});

We can finally execute them

node scripts/deploy.js
tip

Congratulations! Your first Archethic WASM contract is now live

Upgrading Contracts​

To allow upgrades, specify an authorized address in the configuration:

/archethic.config.js
export default {
endpoint: process.env["ENDPOINT"] || "https://testnet.archethic.net",
seed: process.env["SEED"],
upgradeAddress: "0000..." // Address authorized for upgrades
}

Create an upgrade script:

/scripts/upgrade.js
import { getContext, getUpgradeContractTx } from "@archethicjs/ae-contract-test"

async function main() {
const ctx = await getContext()

if (process.env["CONTRACT"] === undefined) {
throw new Error("CONTRACT env var is required")
}

const fundingAccount = await ctx.getAccount()
const contractAddress = process.env["CONTRACT"]

console.log(`Upgrading contract ${contractAddress}`)

await ctx.updateContract(fundingAccount, contractAddress)

console.log("Contract upgraded")
process.exit(0)
}

main().catch((error) => {
console.error(error);
process.exit(1)
});

Run the upgrade script:

CONTRACT=ADDRESS_OF_THE_DEPLOYED_CONTRACT node scripts/upgrade.js