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

feat(api-evm): support evm_call method #464

Merged
merged 14 commits into from
Mar 4, 2024
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> = {
sebastijankuzner marked this conversation as resolved.
Show resolved Hide resolved
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.

Loading