Skip to content

Commit

Permalink
refactor(evm): improve error handling (#463)
Browse files Browse the repository at this point in the history
* handle errors more gracefully

* setup initial tests

* ci

* add try catch

* style: resolve style guide violations

* fix

---------

Co-authored-by: oXtxNt9U <[email protected]>
  • Loading branch information
oXtxNt9U and oXtxNt9U authored Mar 1, 2024
1 parent 5710ff7 commit bffb90d
Show file tree
Hide file tree
Showing 12 changed files with 584 additions and 84 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,16 @@ jobs:
run: cd packages/crypto-transaction-validator-resignation && pnpm run test
- name: Test crypto-transaction-vote
run: cd packages/crypto-transaction-vote && pnpm run test
- name: Test crypto-transaction-evm-call
run: cd packages/crypto-transaction-evm-call && pnpm run test
- name: Test crypto-validation
run: cd packages/crypto-validation && pnpm run test
- name: Test crypto-wif
run: cd packages/crypto-wif && pnpm run test
- name: Test database
run: cd packages/database && pnpm run test
- name: Test evm
run: cd packages/evm && pnpm run test
- name: Test fees
run: cd packages/fees && pnpm run test
- name: Test fees-burn
Expand Down
22 changes: 13 additions & 9 deletions packages/crypto-transaction-evm-call/source/handlers/evm-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,19 @@ export class EvmCallTransactionHandler extends Handlers.TransactionHandler {

const sender = await walletRepository.findByPublicKey(transaction.data.senderPublicKey);

const result = await this.evm.transact({
caller: sender.getAddress(),
data: Buffer.from(evmCall.payload, "hex"),
recipient: transaction.data.recipientId,
});
try {
const result = await this.evm.transact({
caller: sender.getAddress(),
data: Buffer.from(evmCall.payload, "hex"),
recipient: transaction.data.recipientId,
});

// TODO: handle result
// - like subtracting gas from sender
// - populating indexes, etc.
this.logger.debug(`executed EVM call (success=${result.success}, gasUsed=${result.gasUsed})`);
// TODO: handle result
// - like subtracting gas from sender
// - populating indexes, etc.
this.logger.debug(`executed EVM call (success=${result.success}, gasUsed=${result.gasUsed})`);
} catch (error) {
this.logger.critical(`invalid EVM call: ${error.stack}`);
}
}
}
1 change: 1 addition & 0 deletions packages/evm/Cargo.lock

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

1 change: 1 addition & 0 deletions packages/evm/bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ serde_json = { workspace = true }
tokio = { workspace = true }

napi = { version = "2.15.2", default-features = false, features = [
"anyhow",
"napi9",
"serde-json",
"tokio_rt",
Expand Down
42 changes: 42 additions & 0 deletions packages/evm/bindings/src/ctx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use napi::{JsBuffer, JsString};
use napi_derive::napi;
use revm::primitives::{Address, Bytes};

use crate::utils;

#[napi(object)]
pub struct JsTransactionContext {
pub caller: JsString,
/// Omit recipient when deploying a contract
pub recipient: Option<JsString>,
pub data: JsBuffer,
}

pub struct TxContext {
pub caller: Address,
/// Omit recipient when deploying a contract
pub recipient: Option<Address>,
pub data: Bytes,
}

impl TryFrom<JsTransactionContext> for TxContext {
type Error = anyhow::Error;

fn try_from(value: JsTransactionContext) -> std::result::Result<Self, Self::Error> {
let buf = value.data.into_value()?;

let recipient = if let Some(recipient) = value.recipient {
Some(utils::create_address_from_js_string(recipient)?)
} else {
None
};

let tx_ctx = TxContext {
recipient,
caller: utils::create_address_from_js_string(value.caller)?,
data: Bytes::from(buf.as_ref().to_owned()),
};

Ok(tx_ctx)
}
}
84 changes: 9 additions & 75 deletions packages/evm/bindings/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
use std::{str::FromStr, sync::Arc};

use ctx::{JsTransactionContext, TxContext};
use mainsail_evm_core::EvmInstance;
use napi::{bindgen_prelude::*, JsBigInt, JsBuffer, JsObject, JsString};
use napi_derive::napi;
use result::{JsTransactionResult, TxResult};
use revm::primitives::{AccountInfo, Address, Bytes, ExecutionResult, Log, U256};

pub struct TxResult {
gas_used: u64,
gas_refunded: u64,
success: bool,
// TODO: expose additional data needed to JS
deployed_contract_address: Option<String>,
logs: Option<Vec<Log>>,
output: Option<Bytes>,
}
mod ctx;
mod result;
mod utils;

// A complex struct which cannot be exposed to JavaScript directly.
pub struct EvmInner {
Expand All @@ -24,26 +20,6 @@ pub struct EvmInner {
// NOTE: we guarantee that this can be sent between threads, since it only is accessed through a mutex
unsafe impl Send for EvmInner {}

pub struct TxContext {
pub caller: Address,
/// Omit recipient when deploying a contract
pub recipient: Option<Address>,
pub data: Bytes,
}

impl From<JsTransactionContext> for TxContext {
fn from(value: JsTransactionContext) -> Self {
let buf = value.data.into_value().unwrap();
TxContext {
caller: Address::from_str(value.caller.into_utf8().unwrap().as_str().unwrap()).unwrap(),
recipient: value
.recipient
.map(|v| Address::from_str(v.into_utf8().unwrap().as_str().unwrap()).unwrap()),
data: Bytes::from(buf.as_ref().to_owned()),
}
}
}

pub struct UpdateAccountInfoCtx {
pub address: Address,
pub balance: U256,
Expand Down Expand Up @@ -198,14 +174,6 @@ impl EvmInner {
}
}

#[napi(object)]
pub struct JsTransactionContext {
pub caller: JsString,
/// Omit recipient when deploying a contract
pub recipient: Option<JsString>,
pub data: JsBuffer,
}

#[napi(object)]
pub struct JsAccountInfo {
pub address: JsString,
Expand All @@ -220,16 +188,6 @@ pub struct JsEvmWrapper {
evm: Arc<tokio::sync::Mutex<EvmInner>>,
}

#[napi(object)]
pub struct JsTransactionResult {
pub gas_used: JsBigInt,
pub gas_refunded: JsBigInt,
pub success: bool,
pub deployed_contract_address: Option<JsString>,
pub logs: serde_json::Value,
pub output: Option<JsBuffer>,
}

#[napi]
impl JsEvmWrapper {
#[napi(constructor)]
Expand All @@ -241,19 +199,19 @@ impl JsEvmWrapper {

#[napi(ts_return_type = "Promise<JsTransactionResult>")]
pub fn transact(&mut self, node_env: Env, tx_ctx: JsTransactionContext) -> Result<JsObject> {
let tx_ctx = TxContext::from(tx_ctx);
let tx_ctx = TxContext::try_from(tx_ctx)?;
node_env.execute_tokio_future(
Self::transact_async(self.evm.clone(), tx_ctx),
|&mut node_env, result| Ok(Self::to_result(node_env, result)),
|&mut node_env, result| Ok(result::JsTransactionResult::new(node_env, result)?),
)
}

#[napi(ts_return_type = "Promise<JsTransactionResult>")]
pub fn view(&mut self, node_env: Env, tx_ctx: JsTransactionContext) -> Result<JsObject> {
let tx_ctx = TxContext::from(tx_ctx);
let tx_ctx = TxContext::try_from(tx_ctx)?;
node_env.execute_tokio_future(
Self::view_async(self.evm.clone(), tx_ctx),
|&mut node_env, result| Ok(Self::to_result(node_env, result)),
|&mut node_env, result| Ok(result::JsTransactionResult::new(node_env, result)?),
)
}

Expand Down Expand Up @@ -345,30 +303,6 @@ impl JsEvmWrapper {
},
}
}

fn to_result(node_env: Env, result: TxResult) -> JsTransactionResult {
JsTransactionResult {
gas_used: node_env.create_bigint_from_u64(result.gas_used).unwrap(),
gas_refunded: node_env
.create_bigint_from_u64(result.gas_refunded)
.unwrap(),
success: result.success,
deployed_contract_address: result
.deployed_contract_address
.map(|a| node_env.create_string_from_std(a).unwrap()),
logs: result
.logs
.map(|l| serde_json::to_value(l).unwrap())
.unwrap_or_else(|| serde_json::Value::Null),

output: result.output.map(|o| {
node_env
.create_buffer_with_data(Into::<Vec<u8>>::into(o))
.unwrap()
.into_raw()
}),
}
}
}

fn convert_u256_to_bigint(node_env: Env, value: U256) -> JsBigInt {
Expand Down
55 changes: 55 additions & 0 deletions packages/evm/bindings/src/result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use napi::{JsBigInt, JsBuffer, JsString};
use napi_derive::napi;
use revm::primitives::{Bytes, Log};

#[napi(object)]
pub struct JsTransactionResult {
pub gas_used: JsBigInt,
pub gas_refunded: JsBigInt,
pub success: bool,
pub deployed_contract_address: Option<JsString>,

// TODO: typing
pub logs: serde_json::Value,
pub output: Option<JsBuffer>,
}

pub struct TxResult {
pub gas_used: u64,
pub gas_refunded: u64,
pub success: bool,
// TODO: expose additional data needed to JS
pub deployed_contract_address: Option<String>,
pub logs: Option<Vec<Log>>,
pub output: Option<Bytes>,
}

impl JsTransactionResult {
pub fn new(node_env: napi::Env, result: TxResult) -> anyhow::Result<Self> {
let deployed_contract_address =
if let Some(contract_address) = result.deployed_contract_address {
Some(node_env.create_string_from_std(contract_address)?)
} else {
None
};

let result = JsTransactionResult {
gas_used: node_env.create_bigint_from_u64(result.gas_used)?,
gas_refunded: node_env.create_bigint_from_u64(result.gas_refunded)?,
success: result.success,
deployed_contract_address,
logs: result
.logs
.map(|l| serde_json::to_value(l).unwrap())
.unwrap_or_else(|| serde_json::Value::Null),
output: result.output.map(|o| {
node_env
.create_buffer_with_data(Into::<Vec<u8>>::into(o))
.unwrap()
.into_raw()
}),
};

Ok(result)
}
}
11 changes: 11 additions & 0 deletions packages/evm/bindings/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use std::str::FromStr;

use anyhow;
use napi::JsString;
use revm::primitives::Address;

pub(crate) fn create_address_from_js_string(js_str: JsString) -> anyhow::Result<Address> {
let js_str = js_str.into_utf8()?;
let slice = js_str.as_str()?;
Ok(Address::from_str(slice)?)
}
62 changes: 62 additions & 0 deletions packages/evm/source/instance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Contracts } from "@mainsail/contracts";

import { describe, Sandbox } from "../../test-framework";
import { bytecode } from "../test/fixtures/MainsailERC20.json";
import { wallets } from "../test/fixtures/wallets";
import { prepareSandbox } from "../test/helpers/prepare-sandbox";
import { Instance } from "./instance";

describe<{
sandbox: Sandbox;
instance: Contracts.Evm.Instance;
}>("Instance", ({ it, assert, beforeEach }) => {
beforeEach(async (context) => {
await prepareSandbox(context);

context.instance = context.sandbox.app.resolve<Contracts.Evm.Instance>(Instance);
});

it("should deploy contract successfully", async ({ instance }) => {
const [sender] = wallets;

const result = await instance.transact({
caller: sender.address,
data: Buffer.from(bytecode.slice(2), "hex"),
});

assert.true(result.success);
assert.equal(result.gasUsed, 964_156n);
assert.equal(result.deployedContractAddress, "0x0c2485e7d05894BC4f4413c52B080b6D1eca122a");
});

it("should revert on invalid call", async ({ instance }) => {
const [sender] = wallets;

let result = await instance.transact({
caller: sender.address,
data: Buffer.from(bytecode.slice(2), "hex"),
});

const contractAddress = result.deployedContractAddress;
assert.defined(contractAddress);

result = await instance.transact({
caller: sender.address,
data: Buffer.from("0xdead", "hex"),
recipient: contractAddress,
});

assert.false(result.success);
assert.equal(result.gasUsed, 21_070n);
});

it("should throw on invalid tx context caller", async ({ instance }) => {
await assert.rejects(
async () =>
await instance.transact({
caller: "badsender_",
data: Buffer.from(bytecode.slice(2), "hex"),
}),
);
});
});
Loading

0 comments on commit bffb90d

Please sign in to comment.