Skip to content

Commit

Permalink
feat(api-evm): support evm_call method (#464)
Browse files Browse the repository at this point in the history
* Call view method

* Add ethers dep

* Add additional evm tests

* Remove only

* Add ethers to api-evm

* Prepare buffer data

* Return success

* Response order

* Custom RPC error

* Handle revert

* Schema uses address

* Use prefixed hex

* style: resolve style guide violations

* Use prefixedHex name

---------

Co-authored-by: sebastijankuzner <[email protected]>
  • Loading branch information
sebastijankuzner and sebastijankuzner authored Mar 4, 2024
1 parent bffb90d commit a30005b
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 14 deletions.
19 changes: 16 additions & 3 deletions packages/api-common/source/rcp/processor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Hapi from "@hapi/hapi";
import { inject, injectable } from "@mainsail/container";
import { Contracts, Identifiers } from "@mainsail/contracts";
import { Contracts, Exceptions, Identifiers } from "@mainsail/contracts";

import { getRcpId, prepareRcpError } from "./utils";

Expand Down Expand Up @@ -33,11 +33,24 @@ export class Processor implements Contracts.Api.RPC.Processor {

try {
return {
id: getRcpId(request),
jsonrpc: "2.0",
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
id: getRcpId(request),
result: await action.handle(payload.params),
};
} catch {
} catch (error) {
if (error instanceof Exceptions.RpcError) {
return {
jsonrpc: "2.0",
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
id: getRcpId(request),
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
error: {
code: error.code,
message: error.message,
},
};
}
return prepareRcpError(getRcpId(request), Contracts.Api.RPC.ErrorCode.InternalError);
}
}
Expand Down
6 changes: 4 additions & 2 deletions packages/api-common/source/rcp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ export const prepareRcpError = (
id: Contracts.Api.RPC.Id,
errorCode: Contracts.Api.RPC.ErrorCode,
): Contracts.Api.RPC.Error => ({
jsonrpc: "2.0",
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
id,
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
error: {
code: errorCode,
message: errorMessageMap[errorCode],
},
id,
jsonrpc: "2.0",
});
1 change: 1 addition & 0 deletions packages/api-evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@mainsail/container": "workspace:*",
"@mainsail/contracts": "workspace:*",
"@mainsail/kernel": "workspace:*",
"ethers": "6.11.0",
"joi": "17.11.0"
},
"devDependencies": {
Expand Down
40 changes: 33 additions & 7 deletions packages/api-evm/source/actions/call.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { injectable } from "@mainsail/container";
import { Contracts } from "@mainsail/contracts";
import { inject, injectable } from "@mainsail/container";
import { Contracts, Exceptions, Identifiers } from "@mainsail/contracts";
import { ethers } from "ethers";

type BlockTag = "latest" | "earliest" | "pending";

type TxData = {
from: string;
to: string;
data: string;
};

@injectable()
export class CallAction implements Contracts.Api.RPC.Action {
@inject(Identifiers.Evm.Instance)
private readonly evm!: Contracts.Evm.Instance;

public readonly name: string = "eth_call";

public readonly schema = {
Expand All @@ -15,9 +27,9 @@ export class CallAction implements Contracts.Api.RPC.Action {
{
additionalProperties: false,
properties: {
data: { type: "string" },
from: { type: "string" },
to: { type: "string" },
data: { $ref: "prefixedHex" },
from: { $ref: "address" },
to: { $ref: "address" },
},
required: ["from", "to", "data"],
type: "object",
Expand All @@ -28,7 +40,21 @@ export class CallAction implements Contracts.Api.RPC.Action {
type: "array",
};

public async handle(parameters: any): Promise<any> {
return `OK ${this.name}`;
public async handle(parameters: [TxData, BlockTag]): Promise<any> {
const [data] = parameters;

const txContext = {
caller: data.from,
data: Buffer.from(ethers.getBytes(data.data)),
recipient: data.to,
};

const result = await this.evm.view(txContext);

if (result.success) {
return `0x${result.output?.toString("hex")}`;
}

throw new Exceptions.RpcError("execution reverted");
}
}
1 change: 1 addition & 0 deletions packages/contracts/source/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./p2p";
export * from "./plugins";
export * from "./pool";
export * from "./processor";
export * from "./rpc";
export * from "./runtime";
export * from "./state";
export * from "./validation";
10 changes: 10 additions & 0 deletions packages/contracts/source/exceptions/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Exception } from "./base";

export class RpcError extends Exception {
public constructor(
message: string,
public code: number = -32_000,
) {
super(message);
}
}
7 changes: 6 additions & 1 deletion packages/crypto-validation/source/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchemaObject } from "ajv";

export const schemas: Record<"alphanumeric" | "hex", SchemaObject> = {
export const schemas: Record<"alphanumeric" | "hex" | "prefixedHex", SchemaObject> = {
alphanumeric: {
$id: "alphanumeric",
pattern: "^[a-z0-9]+$",
Expand All @@ -11,4 +11,9 @@ export const schemas: Record<"alphanumeric" | "hex", SchemaObject> = {
pattern: "^[0123456789a-f]+$",
type: "string",
},
prefixedHex: {
$id: "prefixedHex",
pattern: "^0x[0-9a-f]+$",
type: "string",
},
};
1 change: 1 addition & 0 deletions packages/evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"devDependencies": {
"@napi-rs/cli": "^2.18.0",
"ethers": "6.11.0",
"uvu": "^0.5.6"
},
"engines": {
Expand Down
42 changes: 41 additions & 1 deletion packages/evm/source/instance.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Contracts } from "@mainsail/contracts";
import { ethers } from "ethers";

import { describe, Sandbox } from "../../test-framework";
import { bytecode } from "../test/fixtures/MainsailERC20.json";
import { abi, bytecode } from "../test/fixtures/MainsailERC20.json";
import { wallets } from "../test/fixtures/wallets";
import { prepareSandbox } from "../test/helpers/prepare-sandbox";
import { Instance } from "./instance";
Expand Down Expand Up @@ -29,6 +30,45 @@ describe<{
assert.equal(result.deployedContractAddress, "0x0c2485e7d05894BC4f4413c52B080b6D1eca122a");
});

it("should deploy, transfer and call balanceOf", async ({ instance }) => {
const [sender, recipient] = 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");

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

const iface = new ethers.Interface(abi);
const amount = ethers.parseEther("1000");

const transferEncodedCall = iface.encodeFunctionData("transfer", [recipient.address, amount]);
const transferResult = await instance.transact({
caller: sender.address,
data: Buffer.from(ethers.getBytes(transferEncodedCall)),
recipient: contractAddress,
});

assert.true(transferResult.success);
assert.equal(transferResult.gasUsed, 52_222n);

const balanceOfEncodedCall = iface.encodeFunctionData("balanceOf", [recipient.address]);
const balanceOfResult = await instance.view({
caller: sender.address,
data: Buffer.from(ethers.getBytes(balanceOfEncodedCall)),
recipient: contractAddress,
});

assert.true(balanceOfResult.success);
assert.equal(balanceOfResult.gasUsed, 24_295n);
});

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

Expand Down
5 changes: 5 additions & 0 deletions packages/evm/test/fixtures/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ export const wallets = [
passphrase:
"violin hello resist adult roof breeze blood old tell source enforce token void wagon sweet detail raw coast viable garden cause gasp soap fat",
},
{
address: "0xEcC2717Ac3558141bFe0f512ACD5c62C5AB303C7",
passphrase:
"extend coach swift member onion reduce furnace cash romance hope ginger project breeze siren foot river rocket ten picnic quick mimic identify aspect forward",
},
];
21 changes: 21 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit a30005b

Please sign in to comment.