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

refactor(evm): improve error handling #463

Merged
merged 6 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading