Skip to content

reef-chain/evm-provider

Repository files navigation

@reef-chain/evm-provider

evm-provider implements a web3 provider which can interact with the Reef chain EVM.

If you only care about developing Solidity contracts on the Reef chain, @reef-chain/evm-provider is used in our Hardhat Reef environment. The environment simplifies and abstracts all the low-level intricacies, so you can only focus on the Solidity part. See hardhat-reef-examples repo for more examples.

If you need more control, then it can also be used as a Substrate provider to query or to interact with the Reef chain using the same calls as in the Polkadot.js.

Installation

Install dependencies with yarn see issue.

Yarn

yarn add @reef-chain/evm-provider

Getting started

To create a Provider instance, use the following code:

import {
  TestAccountSigningKey,
  Provider,
  Signer,
} from "@reef-chain/evm-provider";
import { WsProvider, Keyring } from "@polkadot/api";
import { createTestPairs } from "@polkadot/keyring/testingPairs";
import { KeyringPair } from "@polkadot/keyring/types";

const WS_URL = process.env.WS_URL || "ws://127.0.0.1:9944";
const seed = process.env.SEED;

const setup = async () => {
  const provider = new Provider({
    provider: new WsProvider(WS_URL),
  });

  await provider.api.isReady;

  let pair: KeyringPair;
  if (seed) {
    const keyring = new Keyring({ type: "sr25519" });
    pair = keyring.addFromUri(seed);
  } else {
    const testPairs = createTestPairs();
    pair = testPairs.alice;
  }

  const signingKey = new TestAccountSigningKey(provider.api.registry);
  signingKey.addKeyringPair(pair);

  const signer = new Signer(provider, pair.address, signingKey);

  // Claim default account
  if (!(await signer.isClaimed())) {
    console.log(
      "No claimed EVM account found -> claimed default EVM account: ",
      await signer.getAddress()
    );
    await signer.claimDefaultAccount();
  }

  return {
    signer,
    provider,
  };
};

export default setup;

with this object you can interact with the Substrate chain.

If you want to interact with injected sources (e.g. from Polkadot{.js}) you can do the following:

import { Provider, Signer, } from "@reef-chain/evm-provider";
import { WsProvider } from "@polkadot/api";
import { web3Accounts, web3Enable } from "@polkadot/extension-dapp";

const WS_URL = process.env.WS_URL || "ws://127.0.0.1:9944";
const seed = process.env.SEED;

const setup = async () => {
  
  // Return an array of all the injected sources
  // (this needs to be called first)
  const allInjected = await web3Enable('your dapp');

  const injected;
  if (allInjected[0] && allInjected[0].signer) {
    injected = allInjected[0].signer;
  }

  // Return an array of { address, meta: { name, source } }
  // (meta.source contains the name of the extension)
  const allAccounts = await web3Accounts();

  let account;
  if (allAccounts[0] && allAccounts[0].address) {
    account = allAccounts[0].address;
  }

  const provider = new Provider({
    provider: new WsProvider(WS_URL)
  });

  await provider.api.isReady;

  const signer = new Signer(provider, account, injected);

  // Claim default account
  if (!(await signer.isClaimed())) {
    console.log(
      "No claimed EVM account found -> claimed default EVM account: ",
      await signer.getAddress()
    );
    await signer.claimDefaultAccount();
  }

  return {
    signer,
    provider,
  };
};

export default setup;

EVM interaction

Most, but not all, of evm-provider API is compatible with ethers.js. If you are not familiar with ethers.js, you can start by looking at its documentation. See our Reefswap example on how it uses the above setup script to deploy and interact with the EVM.

Get EVM address

// ethers
let accounts = await this.provider.listAccounts();
let selectedAccount = accounts[0];

// evm-provider
let selectedAccount = await this.signer.queryEvmAddress();

Claim EVM address

If you would like to inject an evm address that you already own you can do so via the claimAccount extrinsic. The script below illustrates how this can be done.

import { Keyring, WsProvider } from '@polkadot/api';
import { ethers } from 'ethers';
import { createClaimEvmSignature } from './utils';
import { Provider } from '.';

const WS_URL = process.env.WS_URL || 'ws://127.0.0.1:9944';
// reef address - 5H728gLgx4yuCSVEwGCAfLo3RtzTau9F6cTNqNJtrqqjACWq
const reefPrivKeyRaw = process.env.REEF_PRIV_KEY || "0x0000000000000000000000000000000000000000000000000000000000000000";
const ethPrivKey = process.env.ETH_PRIV_KEY || "0x81376b9868b292a46a1c486d344e427a3088657fda629b5f4a647822d329cd6a";

const main = async (): Promise<void> =>  {
    const provider = new Provider({
        provider: new WsProvider(WS_URL)
    });
    await provider.api.isReady;

    const keyring = new Keyring({ type: 'sr25519' });
    const reefKey = keyring.addFromUri(reefPrivKeyRaw);
    const ethKey = new ethers.Wallet(ethPrivKey);

    const msg = createClaimEvmSignature(reefKey.address);
    let signature = await ethKey.signMessage(msg);

    await provider.api.tx.evmAccounts.claimAccount(
        ethKey.address,
        signature
    ).signAndSend(reefKey);

    process.exit();
};

main();

Build payload, sign and send transaction

This example illustrates how to build a payload to be signed with an EVM account. Once signed, the signature is added to the extrinsic and sent to the chain.

import { PopulatedTransaction } from "ethers";
import { ethers } from "ethers";
import { Provider } from '.';
import { buildPayload, sendSignedTransaction } from './utils';

const buildSignAndSend = async (provider: Provider, signerAddress: string): Promise<void> =>  {
    const contract = new ethers.Contract(
      flipperContractAddress,
      FlipperAbi,
      provider as any
    );

    const tx: PopulatedTransaction = await contract.populateTransaction.flip();

    const { payload, extrinsic } = await buildPayload(provider, signerAddress, tx);

    // (...) Add logic to send payload to signer and receive signature

    extrinsic.addSignature(signerAddress, signature, payload);

    const txResult = await sendSignedTransaction(
      provider,
      signerAddress,
      tx,
      payload,
      extrinsic,
      signature
    );
};

Provider

The Provider provides an API for interacting with nodes and is an instance of ethers.js AbstractProvider.

Signer

The Signer class can sign transactions and messages using a private key. When using the wallet for the first time, make sure to always claim the EVM account for the wallet you are using:

signer.claimDefaultAccount();

before performing any EVM calls otherwise it may lead to InsufficientBalance errors.

Gas limit and storage limit

In addition to the gas limit (processing), the Reef chain also charges a storage fee. When you interact with the EVM, Reef chain will estimate both fees and as such the fees will be invisible to the user. This should work in 99% of cases. It assumes you have at least 60 REEF tokens on the signing account. However, sometimes the heuristics (usually for more complex contracts) are wrong. In this case you can force the values of gasLimit and storageLimit by adding them to the options dictionary at the end of every call, for example:

await factory.deploy(<contract_args>, {
  gasLimit: 1000000,
  customData: { storageLimit: 1000000 }
});

If you require maximum flexibility evm-provider exports maximum gas and storage limit:

import { MAX_GAS_LIMIT, MAX_STORAGE_LIMIT } from "@reef-chain/evm-provider";

which default to U64MAX and U32MAX respectively.

Which API can I use to query by transaction hash?

There is no such API. Substrate does not expose a "query-by-tx-hash" RPC, nor are transactions indexed by hash on the Substrate node. The reason for this is that transaction hashes are non-unique across the chain, although they will generally be unique inside a block.

Please use GraphQL for this purpose.

How to query EVM events and logs?

See above.

Versions

  • versions 1.*.* work from Reef v8 chain onwards
    • no longer requires resolutions with ethers@5.0.9
  • versions 0.*.* work from Reef v0 to v7