Skip to content

Commit

Permalink
First version of deployer
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben-Rey committed Apr 30, 2024
1 parent 4821931 commit b5e2138
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/massa-web3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"build": "npm-run-all clean-dist build-*",
"generate:client": "open-rpc-generator generate -c open_rpc/openrpc.config.json && mv tmp/client/typescript/src/index.ts src/experimental/generated/client.ts && rm -r tmp",
"generate:test": "ts-interface-builder src/experimental/generated/client.ts",
"generate:deployer": "node ./src/experimental/generated/generate-deployer.js",
"generate": "npm-run-all generate:*",
"test": "jest --detectOpenHandles --forceExit ./test/experimental/unit",
"test:cov": "jest --detectOpenHandles --coverage --forceExit ./test/experimental/unit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export type CallOperation = BaseSmartContractOperation & {
parameter: Uint8Array
}

export type ExecuteOperation = BaseSmartContractOperation & {
// @see https://docs.massa.net/docs/learn/operation-format-execution#executesc-operation-payload
export type ExecuteOperation = BaseOperation & {
maxGas: bigint
maxCoins: bigint
type: OperationType.ExecuteSmartContractBytecode
contractDataBinary: Uint8Array
datastore?: Map<Uint8Array, Uint8Array>
Expand Down Expand Up @@ -124,7 +127,7 @@ export class OperationManager {
case OperationType.ExecuteSmartContractBytecode:
operation = operation as ExecuteOperation
components.push(unsigned.encode(operation.maxGas))
components.push(unsigned.encode(operation.coins))
components.push(unsigned.encode(operation.maxCoins))
components.push(
unsigned.encode(BigInt(operation.contractDataBinary.length))
)
Expand Down

Large diffs are not rendered by default.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fs = require('fs');
const path = require('path');

const wasmPath = path.join(__dirname,'deployer.wasm');

const wasmData = fs.readFileSync(wasmPath);

const base64WasmData = wasmData.toString('base64');

const byteArray = Uint8Array.from(Buffer.from(base64WasmData, 'base64'));

const byteString = Array.from(byteArray).join(',');

const output = `export const deployer: Uint8Array = new Uint8Array([${byteString}]);\n`;

fs.writeFileSync(path.join(__dirname, 'deployer-bytecode.ts'), output);

console.log('Wasm bytecode written to deployer-bytecode.ts');
30 changes: 21 additions & 9 deletions packages/massa-web3/src/experimental/smartContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { BlockchainClient } from './client'
import { Account } from './account'
import { ErrorInsufficientBalance, ErrorMaxGas } from './errors'
import { deployer } from './generated/deployer-bytecode'

export const MAX_GAS_EXECUTE = 3980167295n
export const MAX_GAS_CALL = 4294167295n
Expand All @@ -24,7 +25,7 @@ const DEFAULT_PERIODS_TO_LIVE = 9
interface ExecuteOption {
fee?: bigint
periodToLive?: number
coins?: bigint
maxCoins?: bigint
maxGas?: bigint
datastore?: Map<Uint8Array, Uint8Array>
}
Expand Down Expand Up @@ -70,7 +71,7 @@ export class ByteCode {
opts?.periodToLive
),
type: OperationType.ExecuteSmartContractBytecode,
coins: opts?.coins ?? 0n,
maxCoins: opts?.maxCoins ?? 0n,
// TODO: implement max gas
maxGas: opts?.maxGas || MAX_GAS_EXECUTE,
contractDataBinary: byteCode,
Expand Down Expand Up @@ -104,6 +105,9 @@ function populateDatastore(
): Map<Uint8Array, Uint8Array> {
const datastore = new Map<Uint8Array, Uint8Array>()

// set the number of contracts in the first key of the datastore
datastore.set(new Uint8Array([0x00]), u64ToBytes(BigInt(contracts.length)))

contracts.forEach((contract, i) => {
datastore.set(u64ToBytes(BigInt(i + 1)), contract.data)
if (contract.args) {
Expand Down Expand Up @@ -187,8 +191,7 @@ export class SmartContract {
Args: Uint8Array,
opts: DeployOptions
): Promise<SmartContract> {
const totalCost =
BigInt(StorageCost.smartContract(byteCode.length)) + opts.coins
const totalCost = StorageCost.smartContract(byteCode.length) + opts.coins

if (
(await client.getBalance(account.address.toString(), false)) < totalCost
Expand All @@ -200,19 +203,18 @@ export class SmartContract {
{
data: byteCode,
args: Args,
coins: BigInt(opts.smartContractCoins),
coins: opts.smartContractCoins ?? 0n,
},
])

// TODO: fix the code to execute the deployer smart contract instead
const operation = await ByteCode.execute(
client,
account.privateKey,
byteCode,
deployer,
{
fee: opts?.fee ?? (await client.getMinimalFee()),
periodToLive: opts?.periodToLive,
coins: opts?.coins,
maxCoins: totalCost,
maxGas: opts?.maxGas,
datastore,
}
Expand All @@ -226,9 +228,19 @@ export class SmartContract {
throw new Error('no event received.')
}

// an error can occur in the deployed smart contract
// We could throw a custom deploy error with the list of errors
// @ts-expect-error TODO: Refactor the deployer smart contract logic to return the deployed address in a more readable way
if (event.at(-1).context.is_error) {
const parsedData = JSON.parse(event.at(-1).data)
throw new Error(parsedData.massa_execution_error)
}

// TODO: Refactor the deployer smart contract logic to return the deployed address in a more readable way
const addr = event[0].data.split(': ')[1]
// TODO: What if multiple smart contracts are deployed in the same operation?
const addr = event.at(-1).data.split(': ')[1]

// TODO: What if multiple smart contracts are deployed in the same operation?
return new SmartContract(client, addr)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// The entry file of your WebAssembly module.
import { Context, Storage, generateEvent } from '@massalabs/massa-as-sdk'
import { Args, stringToBytes } from '@massalabs/as-types'

/**
* This function is meant to be called only one time: when the contract is deployed.
*
* @param binaryArgs - Arguments serialized with Args
*/
export function constructor(binaryArgs: StaticArray<u8>): StaticArray<u8> {
// This line is important. It ensures that this function can't be called in the future.
// If you remove this check, someone could call your constructor function and reset your smart contract.
if (!Context.isDeployingContract()) {
return []
}

const argsDeser = new Args(binaryArgs)
const name = argsDeser
.nextString()
.expect('Name argument is missing or invalid')
generateEvent(`Constructor called with name ${name}`)
return []
}

/**
* @param _ - not used
* @returns the emitted event serialized in bytes
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function event(_: StaticArray<u8>): StaticArray<u8> {
const message = "I'm an event!"
generateEvent(message)
return stringToBytes(message)
}

export function setValueToKey(_args: StaticArray<u8>): void {
const args = new Args(_args)
const key = args.nextString().expect('Key argument is missing or invalid')
const value = args.nextString().expect('Value argument is missing or invalid')

// event the value
Storage.set(key, value)

generateEvent(`Set value ${value.toString()} to key ${key.toString()}`)
}

export function getValueFromKey(_args: StaticArray<u8>): StaticArray<u8> {
const args = new Args(_args)
const key = args.nextString().expect('Key argument is missing or invalid')
const value = Storage.get<string>(key)
return stringToBytes(value)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function sendCoins(_: StaticArray<u8>): void {
const coinAmount = Context.transferredCoins()
generateEvent(`Received ${coinAmount.toString()} coins`)
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fromMAS } from '@massalabs/web3-utils'
import { MAX_GAS_DEPLOYMENT, fromMAS } from '@massalabs/web3-utils'
import {
ByteCode,
MAX_GAS_CALL,
Expand All @@ -7,8 +7,13 @@ import {
} from '../../../src/experimental/smartContract'
import { account, client } from './setup'
import { Args } from '../../../src/experimental/basicElements'
import path from 'path'
import fs from 'fs'
import { Address } from '../../../src/experimental/basicElements'

const CONTRACT_ADDRESS = 'AS1JsLnBg4wAKJrAgNUYSs32oTpr3WgSDJdPZib3r3o2zLLRE8sP'
const CONTRACT_ADDRESS = Address.fromString(
'AS1JsLnBg4wAKJrAgNUYSs32oTpr3WgSDJdPZib3r3o2zLLRE8sP'
)
const TIMEOUT = 61000
const INSUFFICIENT_MAX_GAS = MIN_GAS_CALL - 1n

Expand Down Expand Up @@ -130,4 +135,27 @@ describe('Smart Contract', () => {
)
})
})

test('deploy', async () => {
const wasmPath = path.join(__dirname, './contracts/main.wasm')

const byteCode = fs.readFileSync(wasmPath)

const opts = {
periodToLive: 2,
coins: 3n,
maxGas: MAX_GAS_DEPLOYMENT,
}
const args = new Args().addString('myName').serialize()
const contract = await SmartContract.deploy(
client,
account,
byteCode,
new Uint8Array(args),
opts
)

const scAddress = Address.fromString(contract.contractAddress)
expect(scAddress.isEOA).toBeFalsy()
}, 60000)
})
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('Unit tests', () => {
type: OperationType.ExecuteSmartContractBytecode,
expirePeriod: 2,
maxGas: 3n,
coins: 4n,
maxCoins: 4n,
contractDataBinary: new Uint8Array([1, 2, 3, 4]),
datastore: new Map<Uint8Array, Uint8Array>([
[new Uint8Array([1, 2, 3, 4]), new Uint8Array([1, 2, 3, 4])],
Expand Down Expand Up @@ -169,7 +169,7 @@ describe('Unit tests', () => {
type: OperationType.ExecuteSmartContractBytecode,
expirePeriod: 2,
maxGas: 3n,
coins: 4n,
maxCoins: 4n,
contractDataBinary: new Uint8Array([1, 2, 3, 4]),
datastore: new Map<Uint8Array, Uint8Array>([
[new Uint8Array([1, 2, 3, 4]), new Uint8Array([1, 2, 3, 4])],
Expand Down
3 changes: 2 additions & 1 deletion packages/massa-web3/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
"node_modules",
"**/*.spec.ts"
]
}
}

0 comments on commit b5e2138

Please sign in to comment.