Skip to content

Commit

Permalink
fix lint smart contract file
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben-Rey committed May 3, 2024
1 parent eabf220 commit d028274
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 116 deletions.
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions packages/massa-web3/src/experimental/basicElements/bytecode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { BlockchainClient } from '../client'
import { MAX_GAS_EXECUTE } from '../smartContract'
import { ZERO } from '../utils'
import { PrivateKey } from './keys'
import { Operation } from './operation'
import {
OperationManager,
ExecuteOperation,
calculateExpirePeriod,
OperationType,
} from './operationManager'

type ExecuteOption = {
fee?: bigint
periodToLive?: number
maxCoins?: bigint
maxGas?: bigint
datastore?: Map<Uint8Array, Uint8Array>
}

/**
* A class to compile and execute byte code.
*
* @remarks
* The difference between byte code and a smart contract is that the byte code is the raw code that will be
* executed on the blockchain, while a smart contract is the code that is already deployed on the blockchain.
* The byte code is only ephemeral and will be executed only once.
* A smart contract has an address and exposes functions that can be called multiple times.
*
*/

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function compile(_source: string): Promise<Uint8Array> {
throw new Error('Not implemented')
}

/**
*
* Executes a byte code on the blockchain.
*
* @param client - The client to connect to the desired blockchain.
* @param privateKey - The private key of the account that will execute the byte code.
* @param byteCode - The byte code to execute.
* @param opts - Optional execution details.
*
* @returns The operation.
*/
export async function execute(
client: BlockchainClient,
privateKey: PrivateKey,
byteCode: Uint8Array,
opts: ExecuteOption
): Promise<Operation> {
const operation = new OperationManager(privateKey, client)
const details: ExecuteOperation = {
fee: opts?.fee ?? (await client.getMinimalFee()),
expirePeriod: calculateExpirePeriod(
await client.fetchPeriod(),
opts?.periodToLive
),
type: OperationType.ExecuteSmartContractBytecode,
maxCoins: opts?.maxCoins ?? BigInt(ZERO),
// TODO: implement max gas
maxGas: opts?.maxGas || MAX_GAS_EXECUTE,
contractDataBinary: byteCode,
datastore: opts.datastore,
}

return new Operation(client, await operation.send(details))
}
7 changes: 4 additions & 3 deletions packages/massa-web3/src/experimental/client/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { OperationStatus } from '../basicElements'
import { NodeStatus, Slot } from '../generated/client'

export interface SendOperationInput {
export type SendOperationInput = {
data: Uint8Array
publicKey: string
signature: string
}

export interface SCOutputEvent {
export type SCOutputEvent = {
data: string
}

export interface EventFilter {
export type EventFilter = {
start?: Slot
end?: Slot
smartContractAddress?: string
Expand All @@ -24,6 +24,7 @@ export interface EventFilter {
/*
* Blockchain client functions needed by the Operation class to send operations to the blockchain.
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface BlockchainClient {
sendOperation(data: SendOperationInput): Promise<string>
fetchPeriod(): Promise<number>
Expand Down
161 changes: 55 additions & 106 deletions packages/massa-web3/src/experimental/smartContract.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { u64ToBytes, u8toByte } from '@massalabs/web3-utils'
import {
PrivateKey,
Operation,
OperationManager,
ExecuteOperation,
calculateExpirePeriod,
OperationType,
Address,
Expand All @@ -15,74 +13,17 @@ import { BlockchainClient } from './client'
import { Account } from './account'
import { ErrorInsufficientBalance, ErrorMaxGas } from './errors'
import { deployer } from './generated/deployer-bytecode'
import { ONE, ZERO } from './utils'
import { execute } from './basicElements/bytecode'

// TODO: Move to constants file
export const MAX_GAS_EXECUTE = 3980167295n
export const MAX_GAS_CALL = 4294167295n
export const MIN_GAS_CALL = 2100000n

const MASTER_KEY = 0x00
const DEFAULT_PERIODS_TO_LIVE = 9

interface ExecuteOption {
fee?: bigint
periodToLive?: number
maxCoins?: bigint
maxGas?: bigint
datastore?: Map<Uint8Array, Uint8Array>
}

/**
* A class to compile and execute byte code.
*
* @remarks
* The difference between byte code and a smart contract is that the byte code is the raw code that will be
* executed on the blockchain, while a smart contract is the code that is already deployed on the blockchain.
* The byte code is only ephemeral and will be executed only once.
* A smart contract has an address and exposes functions that can be called multiple times.
*
*/
export class ByteCode {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
static compile(_source: string): Promise<Uint8Array> {
throw new Error('Not implemented')
}

/**
*
* Executes a byte code on the blockchain.
*
* @param client - The client to connect to the desired blockchain.
* @param privateKey - The private key of the account that will execute the byte code.
* @param byteCode - The byte code to execute.
* @param opts - Optional execution details.
*
* @returns The operation.
*/
static async execute(
client: BlockchainClient,
privateKey: PrivateKey,
byteCode: Uint8Array,
opts: ExecuteOption
): Promise<Operation> {
const operation = new OperationManager(privateKey, client)
const details: ExecuteOperation = {
fee: opts?.fee ?? (await client.getMinimalFee()),
expirePeriod: calculateExpirePeriod(
await client.fetchPeriod(),
opts?.periodToLive
),
type: OperationType.ExecuteSmartContractBytecode,
maxCoins: opts?.maxCoins ?? 0n,
// TODO: implement max gas
maxGas: opts?.maxGas || MAX_GAS_EXECUTE,
contractDataBinary: byteCode,
datastore: opts.datastore,
}

return new Operation(client, await operation.send(details))
}
}

interface DatastoreContract {
type DatastoreContract = {
data: Uint8Array
args: Uint8Array
coins: bigint
Expand All @@ -106,24 +47,27 @@ function populateDatastore(
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)))
datastore.set(
new Uint8Array([MASTER_KEY]),
u64ToBytes(BigInt(contracts.length))
)

contracts.forEach((contract, i) => {
datastore.set(u64ToBytes(BigInt(i + 1)), contract.data)
datastore.set(u64ToBytes(BigInt(i + ONE)), contract.data)
if (contract.args) {
datastore.set(
new Args()
.addU64(BigInt(i + 1))
.addUint8Array(u8toByte(0))
.addU64(BigInt(i + ONE))
.addUint8Array(u8toByte(ZERO))
.serialize(),
contract.args
)
}
if (contract.coins > 0) {
if (contract.coins > ZERO) {
datastore.set(
new Args()
.addU64(BigInt(i + 1))
.addUint8Array(u8toByte(1))
.addU64(BigInt(i + ONE))
.addUint8Array(u8toByte(ONE))
.serialize(),
u64ToBytes(BigInt(contract.coins))
)
Expand All @@ -140,11 +84,20 @@ type CommonOptions = {
periodToLive?: number
}

type DeployOptions = CommonOptions & {
smartContractCoins?: bigint
type DeployOptions = {
fee?: bigint
maxGas?: bigint
maxCoins?: bigint
periodToLive?: number
waitFinalExecution?: boolean
}

type DeployContract = {
byteCode: Uint8Array
parameter: Uint8Array
coins: bigint
}

type CallOptions = CommonOptions

/**
Expand Down Expand Up @@ -176,8 +129,7 @@ export class SmartContract {
*
* @param client - The client to connect to the desired blockchain.
* @param account - The account that will deploy the smart contract.
* @param byteCode - The byte code of the smart contract.
* @param Args - The arguments of the smart contract constructor.
* @param contract - The contract to deploy.
* @param opts - Optional deployment details.
*
* @returns The deployed smart contract.
Expand All @@ -187,11 +139,12 @@ export class SmartContract {
static async deploy(
client: BlockchainClient,
account: Account,
byteCode: Uint8Array,
Args: Uint8Array,
// TODO: Handle multiple contracts
contract: DeployContract,
opts: DeployOptions
): Promise<SmartContract> {
const totalCost = StorageCost.smartContract(byteCode.length) + opts.coins
const totalCost =
StorageCost.smartContract(contract.byteCode.length) + contract.coins

if (
(await client.getBalance(account.address.toString(), false)) < totalCost
Expand All @@ -201,44 +154,40 @@ export class SmartContract {

const datastore = populateDatastore([
{
data: byteCode,
args: Args,
coins: opts.smartContractCoins ?? 0n,
data: contract.byteCode,
args: contract.parameter,
coins: contract.coins ?? BigInt(ZERO),
},
])

const operation = await ByteCode.execute(
client,
account.privateKey,
deployer,
{
fee: opts?.fee ?? (await client.getMinimalFee()),
periodToLive: opts?.periodToLive,
maxCoins: totalCost,
maxGas: opts?.maxGas,
datastore,
}
)
const operation = await execute(client, account.privateKey, deployer, {
fee: opts?.fee ?? (await client.getMinimalFee()),
periodToLive: opts?.periodToLive,
maxCoins: totalCost,
maxGas: opts?.maxGas,
datastore,
})

const event = opts.waitFinalExecution
? await operation.getFinalEvents()
: await operation.getSpeculativeEvents()

if (event.length === 0) {
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
const firstEvent = event.at(-ONE)

if (!firstEvent) {
throw new Error('no event received.')
}
// @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)
if (firstEvent?.context.is_error) {
const parsedData = JSON.parse(firstEvent.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
// TODO: What if multiple smart contracts are deployed in the same operation?
const addr = event.at(-1).data.split(': ')[1]
const addr = firstEvent.data.split(': ')[ONE]

// TODO: What if multiple smart contracts are deployed in the same operation?
return new SmartContract(client, addr)
Expand All @@ -258,10 +207,9 @@ export class SmartContract {
parameter: Uint8Array,
opts: CallOptions
): Promise<Operation> {
if (!opts.coins) {
opts.coins = 0n
}
if (opts.coins > 0n) {
opts.coins = opts.coins ?? BigInt(ZERO)

if (opts.coins > BigInt(ZERO)) {
const balance = await this.client.getBalance(account.address.toString())
if (balance < opts.coins) {
throw new ErrorInsufficientBalance({
Expand All @@ -274,7 +222,7 @@ export class SmartContract {
opts.fee = opts.fee ?? (await this.client.getMinimalFee())

if (!opts.maxGas) {
opts.maxGas = await this.getGasEstimation()
opts.maxGas = await SmartContract.getGasEstimation()
} else {
if (opts.maxGas > MAX_GAS_CALL) {
throw new ErrorMaxGas({ isHigher: true, amount: MAX_GAS_CALL })
Expand Down Expand Up @@ -320,8 +268,9 @@ export class SmartContract {
* Currently, it returns a predefined maximum gas value.
* @returns A promise that resolves to the estimated gas amount in bigint.
* TODO: Implement dynamic gas estimation using dry run call.
* TODO: Remove static if needed.
*/
async getGasEstimation(): Promise<bigint> {
static async getGasEstimation(): Promise<bigint> {
return MAX_GAS_CALL
}
}
1 change: 1 addition & 0 deletions packages/massa-web3/src/experimental/utils/noMagic.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const FIRST = 0
export const ZERO = 0
export const ONE = 1
Loading

1 comment on commit d028274

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for experimental massa-web3

Caution

Test run failed

St.❔
Category Percentage Covered / Total
🟒 Statements 94.64% 53/56
🟒 Branches 100% 13/13
🟒 Functions 92.31% 12/13
🟒 Lines 94.44% 51/54

Test suite run failed

Failed tests: 0/10. Failed suites: 4/7.
  ● Test suite failed to run

    src/web3/ClientFactory.ts:70:9 - error TS2322: Type 'null' is not assignable to type 'Web3Account'.

    70     let account: Web3Account = null
               ~~~~~~~
    src/web3/ClientFactory.ts:109:9 - error TS2322: Type 'null' is not assignable to type 'Web3Account'.

    109     let account: Web3Account = null
                ~~~~~~~


  ● Test suite failed to run

    src/utils/Xbqcrypto.ts:76:19 - error TS2322: Type 'number | undefined' is not assignable to type 'number'.
      Type 'undefined' is not assignable to type 'number'.

    76   return { value, bytes }
                         ~~~~~

      src/utils/Xbqcrypto.ts:72:3
        72   bytes: number
             ~~~~~
        The expected type comes from property 'bytes' which is declared here on type '{ value: number; bytes: number; }'


  ● Test suite failed to run

    src/experimental/crypto/cross-browser.ts:80:22 - error TS2769: No overload matches this call.
      The last overload gave the following error.
        Argument of type 'Promise<ArrayBuffer>' is not assignable to parameter of type 'WithImplicitCoercion<string> | { [Symbol.toPrimitive](hint: "string"): string; }'.

    80   return Buffer.from(crypto.subtle.exportKey('raw', derivedKey))
                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

      ../../node_modules/@types/node/buffer.d.ts:309:13
        309             from(
                        ~~~~~
        310                 str:
            ~~~~~~~~~~~~~~~~~~~~
        ... 
        315                 encoding?: BufferEncoding,
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        316             ): Buffer;
            ~~~~~~~~~~~~~~~~~~~~~~
        The last overload is declared here.


  ● Test suite failed to run

    src/experimental/crypto/cross-browser.ts:80:22 - error TS2769: No overload matches this call.
      The last overload gave the following error.
        Argument of type 'Promise<ArrayBuffer>' is not assignable to parameter of type 'WithImplicitCoercion<string> | { [Symbol.toPrimitive](hint: "string"): string; }'.

    80   return Buffer.from(crypto.subtle.exportKey('raw', derivedKey))
                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

      ../../node_modules/@types/node/buffer.d.ts:309:13
        309             from(
                        ~~~~~
        310                 str:
            ~~~~~~~~~~~~~~~~~~~~
        ... 
        315                 encoding?: BufferEncoding,
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        316             ): Buffer;
            ~~~~~~~~~~~~~~~~~~~~~~
        The last overload is declared here.

Report generated by πŸ§ͺjest coverage report action from d028274

Please sign in to comment.