はじめに(Introduction)
EVM互換のブロックチェーンを利用する場合によく使用するJavaScript系のライブラリは ethers.js と Web3.js だと思います。
Web3.js はトランザクションを発行する際に必要なパラメータを設定する必要がありますが、ethers.js は少ないパラメータでもトランザクションを送信することが出来ます。
これは、ethers.js が足りないパラメータを自動的に設定しているからです。
実際にどんな値が設定されているのかをソースコードを見てます。
サンプル(Sample)
Wallet
の populateTransaction
関数を使うことでトランザクションを送信せずともトランザクションの足りないパラメータを設定されます。
実際に以下のコードを動かしてみます。(チェインは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
}
from
、nonce
、gasLimit
、chainId
、type
、maxFeePerGas
、maxPriorityFeePerGas
が追加されていることがわかります。
populateTransaction
のドキュメントは以下となります。
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
のソースコードを見てます。
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
では最初に _block
、 gasPrice
、 priorityFee
を取得します。
_block
はJSON-RPCの eth_getBlockByNumber (パラメータは latest
false
)で取得したデータが設定されます。
gasPrice
はJSON-RPCの eth_gasPrice で取得した値が設定されます。
priorityFee
はJSON-RPCの eth_maxPriorityFeePerGas で取得した値が設定されます。
_block
に baseFeePerGas
がある場合、maxPriorityFeePerGas
と maxFeePerGas
を設定します。
maxPriorityFeePerGas
は priorityFee
がある場合はその値をない場合は 1000000000
を設定します。
maxFeePerGas
は block
の baseFeePerGas
を 2
倍したものに maxPriorityFeePerGas
を加算した値を設定します。
feeData
は gasPrice
、maxFeePerGas
、maxPriorityFeePerGas
を含んだデータとなります。
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;
}
}
type
は 2
を設定します。
maxFeePerGas
、maxPriorityFeePerGas
は feeData
の値を設定します。
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;
type
は 0
を設定します。
gasPrice
は feeData
の値を設定します。
まとめ(Conclusion)
各JSON-RPCを利用して値を設定していることがわかりました。
ガス関連もEIP1559にも対応しているようです。