0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ethers.js がトランザクションに自動的に設定する値

Posted at

はじめに(Introduction)

EVM互換のブロックチェーンを利用する場合によく使用するJavaScript系のライブラリは ethers.jsWeb3.js だと思います。
Web3.js はトランザクションを発行する際に必要なパラメータを設定する必要がありますが、ethers.js は少ないパラメータでもトランザクションを送信することが出来ます。
これは、ethers.js が足りないパラメータを自動的に設定しているからです。
実際にどんな値が設定されているのかをソースコードを見てます。

サンプル(Sample)

WalletpopulateTransaction 関数を使うことでトランザクションを送信せずともトランザクションの足りないパラメータを設定されます。
実際に以下のコードを動かしてみます。(チェインはPolygonを選択しています。)

import { ethers } from "ethers";
import { PRIVATE_KEY, ALCHEMY_API_KEY } from "./secret.js";

async function main() {
    // Provider
    const provider = new ethers.JsonRpcProvider(`https://polygon-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`);
    // Wallet
    const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
    // Tx
    let tx = {
        to: "0x0000000000000000000000000000000000000000",
        value: ethers.parseEther("0.001"),
    };
    console.dir(tx);
    tx = await wallet.populateTransaction(tx);
    console.dir(tx);
}


main().then(() => {
    process.exit(0);
}).catch((error) => {
    console.error(error);
    process.exit(1);
});

結果例が以下のようになります。(一部変更しています。)

Ethers.js 6.13.5
{
  to: '0x0000000000000000000000000000000000000000',
  value: 1000000000000000n
}
{
  to: '0x0000000000000000000000000000000000000000',
  value: 1000000000000000n,
  from: '<ウォレット アドレス>',
  nonce: 0,
  gasLimit: 21000n,
  chainId: 137n,
  type: 2,
  maxFeePerGas: 29386549337n,
  maxPriorityFeePerGas: 29386549302n
}

fromnoncegasLimitchainIdtypemaxFeePerGasmaxPriorityFeePerGasが追加されていることがわかります。

populateTransaction のドキュメントは以下となります。

signer.populateTransaction(tx: TransactionRequest)⇒ Promise< TransactionLike < string > >
不足しているプロパティを入力して、ネットワークに送信する準備します。

  • to アドレスと from アドレスを解決します
  • from が指定されている場合は、この署名者と一致することを確認します
  • signer.getNonce("pending") を介して nonce を入力します
  • signer.estimateGas(tx) を介して gasLimit を入力します
  • signer.provider.getNetwork() を介して chainId を入力します
  • タイプとそのタイプの関連料金データを入力します (レガシー トランザクションの場合は gasPriceEIP-1559 の場合は maxFeePerGas など)

ソースコード(Source Code)

ethers.js バージョン 6.13.5 のソースコードを見てます。

from

abstract-signer.ts#L98 が以下となります。

        const pop = await populate(this, tx);

populate を見てみます。

async function populate(signer: AbstractSigner, tx: TransactionRequest): Promise<TransactionLike<string>> {
    let pop: any = copyRequest(tx);

    if (pop.to != null) { pop.to = resolveAddress(pop.to, signer); }

    if (pop.from != null) {
        const from = pop.from;
        pop.from = Promise.all([
            signer.getAddress(),
            resolveAddress(from, signer)
        ]).then(([ address, from ]) => {
            assertArgument(address.toLowerCase() === from.toLowerCase(),
                "transaction from mismatch", "tx.from", from);
            return address;
        });
    } else {
        pop.from = signer.getAddress();
    }

    return await resolveProperties(pop);
}

from が設定されていない場合は、pop.from = signer.getAddress(); としてウォレットのアドレスを設定しています。

nonce

abstract-signer.ts#L100-L102 が以下となります。

        if (pop.nonce == null) {
            pop.nonce = await this.getNonce("pending");
        }

nonceが設定されていない場合は、getNonce (パラメータは pending )の値を設定しています。
getNonce を見てみます。

    async getNonce(blockTag?: BlockTag): Promise<number> {
        return checkProvider(this, "getTransactionCount").getTransactionCount(await this.getAddress(), blockTag);
    }

getTransactionCount は、JSON-RPCの eth_getTransactionCount で取得した値を返します。

gasLimit

abstract-signer.ts#L104-L106 が以下となります。

        if (pop.gasLimit == null) {
            pop.gasLimit = await this.estimateGas(pop);
        }

gasLimitが設定されていない場合は、estimateGas の値を設定しています。
estimateGas を見てみます。

    async estimateGas(tx: TransactionRequest): Promise<bigint> {
        return checkProvider(this, "estimateGas").estimateGas(await this.populateCall(tx));
    }

estimateGas は、JSON-RPCの eth_estimateGas で取得した値を返します。

chainId

abstract-signer.ts#L108-L115 が以下となります。

        // Populate the chain ID
        const network = await (<Provider>(this.provider)).getNetwork();
        if (pop.chainId != null) {
            const chainId = getBigInt(pop.chainId);
            assertArgument(chainId === network.chainId, "transaction chainId mismatch", "tx.chainId", tx.chainId);
        } else {
            pop.chainId = network.chainId;
        }

chainIdが設定されていない場合は、provider から getNetwork を用いてネットワーク情報を取得し chainId の値を設定しています。

GAS関連

条件が複雑そうですが、ここでは何も設定されていない場合の処理をたどっていきたいと思います。

abstract-signer.ts#L143-L144 この部分で getFeeData からデータを取得します。

            // We need to get fee data to determine things
            const feeData = await provider.getFeeData();

getFeeData を見てみます。

    async getFeeData(): Promise<FeeData> {
        const network = await this.getNetwork();


        const getFeeDataFunc = async () => {
            const { _block, gasPrice, priorityFee } = await resolveProperties({
                _block: this.#getBlock("latest", false),
                gasPrice: ((async () => {
                    try {
                        const value = await this.#perform({ method: "getGasPrice" });
                        return getBigInt(value, "%response");
                    } catch (error) { }
                    return null
                })()),
                priorityFee: ((async () => {
                    try {
                        const value = await this.#perform({ method: "getPriorityFee" });
                        return getBigInt(value, "%response");
                    } catch (error) { }
                    return null;
                })())
            });


            let maxFeePerGas: null | bigint = null;
            let maxPriorityFeePerGas: null | bigint = null;


            // These are the recommended EIP-1559 heuristics for fee data
            const block = this._wrapBlock(_block, network);
            if (block && block.baseFeePerGas) {
                maxPriorityFeePerGas = (priorityFee != null) ? priorityFee: BigInt("1000000000");
                maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas;
            }


            return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
        };


        // Check for a FeeDataNetWorkPlugin
        const plugin = <FetchUrlFeeDataNetworkPlugin>network.getPlugin("org.ethers.plugins.network.FetchUrlFeeDataPlugin");
        if (plugin) {
            const req = new FetchRequest(plugin.url);
            const feeData = await plugin.processFunc(getFeeDataFunc, this, req);
            return new FeeData(feeData.gasPrice, feeData.maxFeePerGas, feeData.maxPriorityFeePerGas);
        }


        return await getFeeDataFunc();
    }

プラグインは設定していないので、 getFeeDataFunc が呼ばれます。
getFeeDataFunc では最初に _blockgasPricepriorityFee を取得します。

_block はJSON-RPCの eth_getBlockByNumber (パラメータは latest false )で取得したデータが設定されます。

gasPrice はJSON-RPCの eth_gasPrice で取得した値が設定されます。

priorityFee はJSON-RPCの eth_maxPriorityFeePerGas で取得した値が設定されます。

_blockbaseFeePerGas がある場合、maxPriorityFeePerGasmaxFeePerGas を設定します。

maxPriorityFeePerGaspriorityFee がある場合はその値をない場合は 1000000000 を設定します。

maxFeePerGasblockbaseFeePerGas2倍したものに maxPriorityFeePerGas を加算した値を設定します。

feeDatagasPricemaxFeePerGasmaxPriorityFeePerGas を含んだデータとなります。

EIP-1559

ネットワークが EIP-1559 をサポートしている場合は以下のように設定されます。

                if (feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null) {
                    // The network supports EIP-1559!


                    // Upgrade transaction from null to eip-1559
                    pop.type = 2;


                    if (pop.gasPrice != null) {
                        // Using legacy gasPrice property on an eip-1559 network,
                        // so use gasPrice as both fee properties
                        const gasPrice = pop.gasPrice;
                        delete pop.gasPrice;
                        pop.maxFeePerGas = gasPrice;
                        pop.maxPriorityFeePerGas = gasPrice;


                    } else {
                        // Populate missing fee data


                        if (pop.maxFeePerGas == null) {
                            pop.maxFeePerGas = feeData.maxFeePerGas;
                        }


                        if (pop.maxPriorityFeePerGas == null) {
                            pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
                        }
                    }

type2 を設定します。
maxFeePerGasmaxPriorityFeePerGasfeeData の値を設定します。

Legacy

ネットワークがレガシーの場合は以下のように設定されます。

                } else if (feeData.gasPrice != null) {
                    // Network doesn't support EIP-1559...


                    // ...but they are trying to use EIP-1559 properties
                    assert(!hasEip1559, "network does not support EIP-1559", "UNSUPPORTED_OPERATION", {
                            operation: "populateTransaction" });


                    // Populate missing fee data
                    if (pop.gasPrice == null) {
                        pop.gasPrice = feeData.gasPrice;
                    }


                    // Explicitly set untyped transaction to legacy
                    // @TODO: Maybe this shold allow type 1?
                    pop.type = 0;

type0 を設定します。
gasPricefeeData の値を設定します。

まとめ(Conclusion)

各JSON-RPCを利用して値を設定していることがわかりました。
ガス関連もEIP1559にも対応しているようです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?