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

# はじめに（Introduction）

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

# サンプル（Sample）

`Wallet` の [`populateTransaction`](https://docs.ethers.org/v6/api/providers/#Signer-populateTransaction) 関数を使うことでトランザクションを送信せずともトランザクションの足りないパラメータを設定されます。
実際に以下のコードを動かしてみます。（チェインはPolygonを選択しています。）

```javascript
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);
});
```

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

```javascript
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
}
```

`from`、`nonce`、`gasLimit`、`chainId`、`type`、`maxFeePerGas`、`maxPriorityFeePerGas`が追加されていることがわかります。

[`populateTransaction`](https://docs.ethers.org/v6/api/providers/#Signer-populateTransaction) のドキュメントは以下となります。

https://docs.ethers.org/v6/api/providers/#Signer-populateTransaction

> <src>signer.populateTransaction(tx: TransactionRequest)⇒ Promise< TransactionLike < string > >
> 不足しているプロパティを入力して、ネットワークに送信する準備します。
>
> - `to` アドレスと `from` アドレスを解決します
> - `from` が指定されている場合は、この署名者と一致することを確認します
> - `signer.getNonce("pending")` を介して `nonce` を入力します
> - `signer.estimateGas(tx)` を介して `gasLimit` を入力します
> - `signer.provider.getNetwork()` を介して `chainId` を入力します
> - タイプとそのタイプの関連料金データを入力します (レガシー トランザクションの場合は `gasPrice`、`EIP-1559` の場合は `maxFeePerGas` など)

# ソースコード（Source Code）

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

https://github.com/ethers-io/ethers.js/blob/v6.13.5/src.ts/providers/abstract-signer.ts#L95-L214

## `from`

[`abstract-signer.ts#L98`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L98) が以下となります。

```javascript
        const pop = await populate(this, tx);
```

[`populate`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L31-L51) を見てみます。

```javascript
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`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L100-L102) が以下となります。

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

`nonce`が設定されていない場合は、`getNonce` （パラメータは `pending` ）の値を設定しています。
[`getNonce`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L86-L88) を見てみます。

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

`getTransactionCount` は、JSON-RPCの [eth_getTransactionCount](https://ethereum.github.io/execution-apis/api-documentation#eth_getTransactionCount) で取得した値を返します。

## `gasLimit`

[`abstract-signer.ts#L104-L106`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L104-L106) が以下となります。

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

`gasLimit`が設定されていない場合は、`estimateGas` の値を設定しています。
[`estimateGas`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L216-L218) を見てみます。

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

`estimateGas` は、JSON-RPCの [eth_estimateGas](https://ethereum.github.io/execution-apis/api-documentation#eth_estimateGas) で取得した値を返します。

## `chainId`

[`abstract-signer.ts#L108-L115`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L108-L115) が以下となります。

```javascript
        // 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](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-signer.ts#L143-L144) この部分で `getFeeData` からデータを取得します。

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

[`getFeeData`](https://github.com/ethers-io/ethers.js/blob/ce7212d03d6867081603794f0480f31d053823c4/src.ts/providers/abstract-provider.ts#L920-L964) を見てみます。

```javascript
    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` では最初に `_block` 、 `gasPrice` 、 `priorityFee` を取得します。

`_block` はJSON-RPCの [eth_getBlockByNumber](https://ethereum.github.io/execution-apis/api-documentation#eth_getBlockByNumber) （パラメータは `latest` `false` ）で取得したデータが設定されます。

`gasPrice` はJSON-RPCの [eth_gasPrice](https://ethereum.github.io/execution-apis/api-documentation#eth_gasPrice) で取得した値が設定されます。

`priorityFee` はJSON-RPCの [eth_maxPriorityFeePerGas](https://ethereum.github.io/execution-apis/api-documentation#eth_maxPriorityFeePerGas) で取得した値が設定されます。

`_block` に `baseFeePerGas` がある場合、`maxPriorityFeePerGas` と `maxFeePerGas` を設定します。

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

`maxFeePerGas` は `block` の `baseFeePerGas` を `2`倍したものに `maxPriorityFeePerGas` を加算した値を設定します。

`feeData` は `gasPrice`、`maxFeePerGas`、`maxPriorityFeePerGas` を含んだデータとなります。

### EIP-1559

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

```javascript
                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;
                        }
                    }
```

`type` は `2` を設定します。
`maxFeePerGas`、`maxPriorityFeePerGas` は `feeData` の値を設定します。

### Legacy

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

```javascript
                } 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;
```

`type` は `0` を設定します。
`gasPrice` は `feeData` の値を設定します。

# まとめ（Conclusion）

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