ブロックチェーンのトランザクションを確認するのに欠かせないExplorer。
XDCネットワークだとXDCScanになるけど、正直、何書いてあるかよくわからない。
ブロックチェーンを理解する上でも、ブロックチェーンに何が記録されているのか、実際に記録されたデータはどのように格納されて、どうやって情報を得られるのか、Explorerと同じ値が自分で取れたら分かるんじゃないの?という軽いノリでやってみます。
前提
- 所詮素人。間違いはいっぱいあると思う。
- 同じブロックチェーン上のアドレスでも、ウォレット(EOA)とコントラクトは似て非なるもの(個人的感想)で、コントラクトの方が扱いやすい(と思っている)ので、今回の題材はXDCネットワークのネイティブオラクルであるPluginのデータフィード用のOracle Contract Address(OCA)をベースとする。
- サンプルとしてOCAのトランザクションを表示させるので、Plugiin Nodeをしていない人には同じ作業はしんどいかも。
- やったことは結果論であって、最適解かどうかはわからない。
ゴール
XDCScanと同じ情報を自力で取ってくる!
XDCScan
Overview
Log
これらの値が自分で取ってこれるようになると、ブロックチェーンに記録された情報がきっと今より読めるようになると思う。
環境準備
Nodejsを使っていくので、まずは環境構築。
Nodejs環境セットアップ
cd ~
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
source ~/.nvm/nvm.sh
nvm install 18.12.0
nvm use 18.12.0
node --version
作業ディレクトリ作成
mkdir explorer_test
cd explorer_test
必要なNodejsパッケージインストール
npm install xdc3 readline-sync abi-decoder
xdc3
: ブロックチェーンと対話するためのライブラリ。web3.js
のV1からフォークされている。web3.js
でも問題ないが、最新のものは少しロジックの書き方が違うので注意。
readline-sync
: ターミナルからの入力を受け取るライブラリ。プログラムを対話式で動かせる。
abi-decoder
: Consensys(Metamaskの開発元)が提供しているブロックチェーンにハッシュ化されて記録されているデータを復元化してくれるライブラリ(超便利)
まずはOverview
まず、何をもとにデータを拾ってくるか・・・
一番手っ取り早いのはTransaction Hash
(TxHash/TxID)。
TxHashはブロックチェーン上のトランザクションを識別する一意の番号。
特定の一意のデータを取得できるため、検証にはもってこい。
OCAの履歴からTransaction Hashを1つ決める
Transaction情報を見てみる
まずはプログラムを作る
nano explorer_1.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションを取得
const tx = await xdc3.eth.getTransaction(TXHASH);
//explorer_1 表示してみる
console.log(tx);
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる
node explorer_1.js
(まとめ)Transactionの詳細を見ると何がわかるか
eth.getTransaction(TxHash)
Transaction Hashをキーとして、トランザクションの詳細情報を確認できる。
-
Transaction Hash
:トランザクションハッシュ。トランザクションごとに一意に振られるID -
Method
:実行されたトランザクションメソッドのシグネチャ(署名)←Input Dataの頭10文字(0x + 8バイト) -
Block
:トランザクションが格納されているブロック番号 -
from
:トランザクションの送信アドレス(今回だとOCAにフィード値を送り込んだNode Address) -
to
:トランザクションの受信アドレス(今回だとOCA) -
value
:送金されたネイティブトークン(XDC) -
Gas Price
:ガスプライス(XDC2.0になったので、12.5Gweiに設定されている) -
Gas Limit
:トランザクションガスリミット。Contractを実行する際、消費する最大ガス量。Contract実行時にこれ以上のガスを消費する場合、エラーになる。※これとは別にブロックガスリミットもある。これはBlockに格納されるTransaction全体の最大ガス量。ブロックガスリミットを超えない分だけのTransactionが1つのBlockに収まる -
Txn Type
:トランザクションタイプ。ややこしそうなので割愛。EIP1559で検索すると良いかも -
Nonce
:そのアドレスで実行されたトランザクションの総数 -
Position
:(=transactionIndex)同じブロック内に格納されたトランザクションの位置(何番目のトランザクションか) -
Input Data
:トランザクションで送信されたデータ
ここまででまだ見つけられていない情報
-
Timestamp
:トランザクションが生成された日時 -
Transaction Action
:トランザクションの処理(関数名) -
Usage by Txn
:トランザクションで使用されたガス量
Block情報を見てみる
BlockはTransactionを束ねたもの。複数のTransactionをBlockの単位でブロックチェーンに記録する。
まずはプログラムを作る
nano explorer_2.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションを取得
const tx = await xdc3.eth.getTransaction(TXHASH);
// TxHashが格納されているブロックの情報を取得
const block = await xdc3.eth.getBlock(tx.blockNumber);
//explorer_2 表示してみる
console.log(block);
//timestampの答え合わせ
const blockDate = new Date(block.timestamp * 1000);
console.log('timestamp: ' + blockDate);
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる
node explorer_2.js
Transaction Receipt情報を見てみる
トランザクションレシートはブロックチェーンにおけるトランザクションの実行結果を記録したもの。
まずはプログラムを作る
nano explorer_3.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
//explorer_3 表示してみる
console.log(txrec);
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる
node explorer_3.js
念の為確認
①Status
トランザクションエラーでの確認
正解。
Status : true
はXDCScanではSuccess
Status : false
はXDCScanではFailed
②Usage by Txn
違うトランザクションハッシュを確認。
正解。
XDCScanに表示されるのはgasUsed
cumulativeGasUsed
ではない。
cumulativeGasUsed
はこのトランザクションに関連する処理全体で使われたGasの量だそうです。ものによってはInternal Transactionなど関連するトランザクションも含めた総量らしいです。もう少し勉強が必要なようです。
次にLogs
実はgetTransactionReceipt
ではLogsも取得できています。
次に少し編集して見やすくしてみます。
まずはプログラムを作る
nano explorer_4.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
//explorer_4 logだけ表示してみる
txrec.logs.forEach((log) => {
console.log(`Index: ${log.logIndex}`);
console.log(` Address: ${log.address}`);
console.log(` Topics:`);
log.topics.forEach((topic, index) => {
console.log(` [${index}] ${topic}`);
});
console.log(` Data(Hex): ${log.data}`);
});
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる
node explorer_4.js
かなり見やすくなったと思います。
でも、ここでもやっぱりName
(関数名っぽい)が取得できません。
さて、関数名はどうやって取得するか
ここで使うのが、abi-decoder
です。
実は、関数名はすでに取得した情報の中に含まれていました。
やっぱりまずはプログラムを作ってみます。
まずはプログラムを作る(Overview)
今回は事前準備があります。
abi-decoder
を使うためには、ContractのABI(Application Binary Interface)が必要になります。
ABIについてはググってくださいm(_ _)m
ABIはどこで手に入れるか。
今回の検証ではPluginのOCAをベースにしています。
Remix
でOCA(Operator.sol)をコンパイルするところでGetできます。
これをクリックしてコピーしたものを使います。
mkdir ABIs
nano ABIs/Operator.json
RemixでコピーしたABIを貼り付けてください。
同じもの
[
{
"inputs": [
{
"internalType": "address",
"name": "pli",
"type": "address"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address[]",
"name": "senders",
"type": "address[]"
},
{
"indexed": false,
"internalType": "address",
"name": "changedBy",
"type": "address"
}
],
"name": "AuthorizedSendersChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
}
],
"name": "CancelOracleRequest",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "specId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address",
"name": "requester",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "callbackAddr",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes4",
"name": "callbackFunctionId",
"type": "bytes4"
},
{
"indexed": false,
"internalType": "uint256",
"name": "cancelExpiration",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "dataVersion",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "OracleRequest",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
}
],
"name": "OracleResponse",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "acceptedContract",
"type": "address"
}
],
"name": "OwnableContractAccepted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferRequested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address[]",
"name": "targets",
"type": "address[]"
},
{
"indexed": false,
"internalType": "address[]",
"name": "senders",
"type": "address[]"
},
{
"indexed": false,
"internalType": "address",
"name": "changedBy",
"type": "address"
}
],
"name": "TargetsUpdatedAuthorizedSenders",
"type": "event"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "targets",
"type": "address[]"
},
{
"internalType": "address[]",
"name": "senders",
"type": "address[]"
}
],
"name": "acceptAuthorizedReceivers",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "ownable",
"type": "address[]"
}
],
"name": "acceptOwnableContracts",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"internalType": "bytes4",
"name": "callbackFunc",
"type": "bytes4"
},
{
"internalType": "uint256",
"name": "expiration",
"type": "uint256"
}
],
"name": "cancelOracleRequest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"internalType": "bytes4",
"name": "callbackFunc",
"type": "bytes4"
},
{
"internalType": "uint256",
"name": "expiration",
"type": "uint256"
}
],
"name": "cancelOracleRequestByRequester",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address payable[]",
"name": "receivers",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"name": "distributeFunds",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"internalType": "address",
"name": "callbackAddress",
"type": "address"
},
{
"internalType": "bytes4",
"name": "callbackFunctionId",
"type": "bytes4"
},
{
"internalType": "uint256",
"name": "expiration",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "data",
"type": "bytes32"
}
],
"name": "fulfillOracleRequest",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"internalType": "address",
"name": "callbackAddress",
"type": "address"
},
{
"internalType": "bytes4",
"name": "callbackFunctionId",
"type": "bytes4"
},
{
"internalType": "uint256",
"name": "expiration",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "fulfillOracleRequest2",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getAuthorizedSenders",
"outputs": [
{
"internalType": "address[]",
"name": "",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getExpiryTime",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getPluginToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "isAuthorizedSender",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "onTokenTransfer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "specId",
"type": "bytes32"
},
{
"internalType": "bytes4",
"name": "callbackFunctionId",
"type": "bytes4"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "dataVersion",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "operatorRequest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "payment",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "specId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "callbackAddress",
"type": "address"
},
{
"internalType": "bytes4",
"name": "callbackFunctionId",
"type": "bytes4"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "dataVersion",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "oracleRequest",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "ownerForward",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "ownerTransferAndCall",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "senders",
"type": "address[]"
}
],
"name": "setAuthorizedSenders",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "targets",
"type": "address[]"
},
{
"internalType": "address[]",
"name": "senders",
"type": "address[]"
}
],
"name": "setAuthorizedSendersOn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "ownable",
"type": "address[]"
},
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnableContracts",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "typeAndVersion",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdrawable",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
nano explorer_5.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションを取得
const tx = await xdc3.eth.getTransaction(TXHASH);
//explorer_5 inputだけ表示してみる
console.log('[Input]');
console.log(tx.input);
console.log();
//Inputをデコード(復号化)
const decodeInput = abiDecoder.decodeMethod(tx.input);
console.log('[Decoded Input]');
console.log(decodeInput);
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる(Overview)
node explorer_5.js
不思議ですが、16進数のデータをabi-decoder
を通すと、全く別のものが出てきました。
これでOverviewの全ての値を取得することができました。
次はLogsです。
まずはプログラムを作る(Logs)
nano explorer_6.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
//explorer_6 Logsだけ表示してみる
console.log('[Logs]');
console.log(txrec.logs);
console.log();
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
console.log('[Decoded Logs]');
console.log(decodeLogs);
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる(Logs)
node explorer_6.js
3つのLogのうち1つ目しか取得できませんでした。
それぞれのLogのAddressを見てみると・・・
1つ目のAddressは今まで追いかけてきたOCAのものです。
2つ目、3つ目は・・・Oracleにリクエストを出してきたAPIConsumer
のコントラクトアドレスでした。
つまり、APIConsumer
のABI
があればいけそう?
プログラムを作る(リベンジ)
じゃあ、サクッとABI
取ってきちゃいます。
nano ABIs/APIConsumer.json
RemixでコピーしたABIを貼り付けてください。
同じもの
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "id",
"type": "bytes32"
}
],
"name": "ChainlinkCancelled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "id",
"type": "bytes32"
}
],
"name": "ChainlinkFulfilled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "id",
"type": "bytes32"
}
],
"name": "ChainlinkRequested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferRequested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "volume",
"type": "uint256"
}
],
"name": "RequestVolume",
"type": "event"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_requestId",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "_volume",
"type": "uint256"
}
],
"name": "fulfill",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "requestVolumeData",
"outputs": [
{
"internalType": "bytes32",
"name": "requestId",
"type": "bytes32"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "volume",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "withdrawPli",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
nano explorer_6-2.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//APIConsumerのABIをインポート
const apiConsumerABI = require("./ABIs/APIConsumer.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
//APIConsumer ABIをデコーダーにセット(追加)
abiDecoder.addABI(apiConsumerABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
//explorer_6 Logsだけ表示してみる
console.log('[Logs]');
console.log(txrec.logs);
console.log();
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
console.log('[Decoded Logs]');
console.log(decodeLogs);
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる(リベンジ)
修正はAPIConsumer
のABIを読み込ませただけですが、どうでしょうか。
node explorer_6-2.js
もう少し整形してみる(自己満)
nano explorer_6-3.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//APIConsumerのABIをインポート
const apiConsumerABI = require("./ABIs/APIConsumer.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
//APIConsumer ABIをデコーダーにセット(追加)
abiDecoder.addABI(apiConsumerABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
//整形
console.log('[Decoded Logs]');
txrec.logs.forEach(log => {
console.log(`[${log.logIndex}]`);
console.log(` Address: ${log.address}`);
let functionName = decodeLogs[log.logIndex].name;
let nonIndexedData = '';
decodeLogs[log.logIndex].events.forEach((param, index) => {
if(index == 0) {
functionName = functionName + ' (' + param.type + ' ' + param.name;
}else {
functionName = functionName + ', ' + param.type + ' ' + param.name;
}
});
functionName = functionName + ')';
console.log(` Name : ${functionName}`);
console.log(` Topics : `);
log.topics.forEach((topic, index) => {
console.log(` [${index}] ${topic}`);
});
console.log(` Data : `);
console.log(` [Hex] ${log.data}`);
});
}
main();
保存は Ctrl
+x
の後、y
キー押下
実行してみる
node explorer_6-3.js
キレイに出力できました。
でも、Dataはハッシュ化された値です。
色々と触ったけど、デコード(Dec)された値を表示するには、方法は他にもあるのだろうけど、最後に紹介します。(たぶん邪道)
最終形
nano explorer_fin.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
//Letter Color
const RED = '\u001b[31m';
const YELLOW = '\u001b[33m';
const BLUE = '\u001b[34m';
const RESET = '\u001b[0m';
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//APIConsumerのABIをインポート
const apiConsumerABI = require("./ABIs/APIConsumer.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
//APIConsumer ABIをデコーダーにセット
abiDecoder.addABI(apiConsumerABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションを取得
const tx = await xdc3.eth.getTransaction(TXHASH);
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
// TxHashが格納されているブロックの情報を取得
const block = await xdc3.eth.getBlock(tx.blockNumber);
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
//編集
//timestamp
const blockDate = new Date(block.timestamp * 1000);
const status = txrec.status ? `${BLUE}Success${RESET}` : `${RED}Failed${RESET}`;
const decodeInput = abiDecoder.decodeMethod(tx.input);
//出力
//Overview
console.log(RED + '[Overview]' + RESET);
console.log(YELLOW + ' TransactionHash ' + RESET + tx.hash);
console.log(YELLOW + ' Status and method: ' + RESET + status + ' | ' + tx.input.slice(0,10));
console.log(YELLOW + ' Block: ' + RESET + tx.blockNumber);
console.log(YELLOW + ' Timestamp: ' + RESET + blockDate);
console.log(YELLOW + ' Transaction Action: ' + RESET + decodeInput.name);
console.log(YELLOW + ' From: ' + RESET + tx.from);
console.log(YELLOW + ' To: ' + RESET + tx.to);
console.log(YELLOW + ' Value: ' + RESET + tx.value);
console.log(YELLOW + ' Actual Tx Cost/Fee: ' + RESET +
xdc3.utils.fromWei(xdc3.utils.toBN(tx.gasPrice * txrec.gasUsed), 'ether') + ' XDC');
console.log(YELLOW + ' Gas Price: ' + RESET + tx.gasPrice);
console.log(YELLOW + ' Gas Limit & Usage by Txn: ' + RESET + tx.gas + ' | ' + txrec.gasUsed);
console.log(YELLOW + ' Other Attributes: ' + RESET);
console.log(YELLOW + ' Txn Type: ' + RESET + tx.type);
console.log(YELLOW + ' Nonce: ' + RESET + tx.nonce);
console.log(YELLOW + ' Position: ' + RESET + tx.transactionIndex);
console.log(YELLOW + ' Input Data: ' + RESET + tx.input);
//Logs
console.log(RED + '[Logs]' + RESET);
txrec.logs.forEach(log => {
console.log(YELLOW + ' [' + log.logIndex + ']' + RESET);
console.log(YELLOW + ' Address: ' + RESET + log.address);
let functionName = decodeLogs[log.logIndex].name;
let nonIndexedData = '';
decodeLogs[log.logIndex].events.forEach((param, index) => {
if(index == 0) {
functionName = functionName + ' (' + param.type + ' ' + param.name;
}else {
functionName = functionName + ', ' + param.type + ' ' + param.name;
}
});
functionName = functionName + ')';
console.log(YELLOW + ' Name : ' + RESET + functionName);
console.log(YELLOW + ' Topics : ' + RESET);
log.topics.forEach((topic, index) => {
console.log(YELLOW + ' [' + index + '] ' + RESET + topic);
});
console.log(YELLOW + ' Data : ' + RESET);
console.log(YELLOW + ' [Hex] ' + RESET + log.data);
});
}
main();
保存は Ctrl
+x
の後、y
キー押下
動かしてみる
node explorer_fin.js
満足!!
もうちょっと・・・
やっぱりログの値もデコードしたものを表示したい。
色々と調べていると、
-
Topics
はイベントに関連付けられたインデックス付きパラメータである -
Data
はイベントのABIエンコードされた非インデックスパラメータを指す
そもそも、スマートコントラクトはsolidityという言語でプログラムされ、ログはそのプログラムの中で出力されるように設計されている。
(ここらへん、もっと勉強しないとちゃんと説明できない。)
少し脱線したけど、Dataに格納されるのは非インデックスパラメータが重要。
デコードしたログをもう少し見てみる。
nano explorer_6-4.js
explorer_6-2.js
を少し編集したもの
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//APIConsumerのABIをインポート
const apiConsumerABI = require("./ABIs/APIConsumer.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
//APIConsumer ABIをデコーダーにセット(追加)
abiDecoder.addABI(apiConsumerABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
console.log('[Decoded Logs]');
decodeLogs.forEach(log => {
console.log(log);
});
}
main();
邪道な方法で直していきます。
エンコードされたDataを表示する
nano -l node_modules/abi-decoder/index.js
nano explorer_fin_mod.js
プログラムの中身。
※TODOの部分を書き換えてください。
const Xdc3 = require("xdc3");
//abi-decoderをインポート
const abiDecoder = require('abi-decoder');
//Letter Color
const RED = '\u001b[31m';
const YELLOW = '\u001b[33m';
const BLUE = '\u001b[34m';
const RESET = '\u001b[0m';
const RPC = "https://erpc.xinfin.network";
const TXHASH = "YOUR_TRANSACTION_HASH" //TODO ご自身で取得したTxHashを入力する
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//APIConsumerのABIをインポート
const apiConsumerABI = require("./ABIs/APIConsumer.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
//APIConsumer ABIをデコーダーにセット
abiDecoder.addABI(apiConsumerABI);
async function main() {
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションを取得
const tx = await xdc3.eth.getTransaction(TXHASH);
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
// TxHashが格納されているブロックの情報を取得
const block = await xdc3.eth.getBlock(tx.blockNumber);
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
//編集
//timestamp
const blockDate = new Date(block.timestamp * 1000);
const status = txrec.status ? `${BLUE}Success${RESET}` : `${RED}Failed${RESET}`;
const decodeInput = abiDecoder.decodeMethod(tx.input);
//出力
//Overview
console.log(RED + '[Overview]' + RESET);
console.log(YELLOW + ' TransactionHash ' + RESET + tx.hash);
console.log(YELLOW + ' Status and method: ' + RESET + status + ' | ' + tx.input.slice(0,10));
console.log(YELLOW + ' Block: ' + RESET + tx.blockNumber);
console.log(YELLOW + ' Timestamp: ' + RESET + blockDate);
console.log(YELLOW + ' Transaction Action: ' + RESET + decodeInput.name);
console.log(YELLOW + ' From: ' + RESET + tx.from);
console.log(YELLOW + ' To: ' + RESET + tx.to);
console.log(YELLOW + ' Value: ' + RESET + tx.value);
console.log(YELLOW + ' Actual Tx Cost/Fee: ' + RESET +
xdc3.utils.fromWei(xdc3.utils.toBN(tx.gasPrice * txrec.gasUsed), 'ether') + ' XDC');
console.log(YELLOW + ' Gas Price: ' + RESET + tx.gasPrice);
console.log(YELLOW + ' Gas Limit & Usage by Txn: ' + RESET + tx.gas + ' | ' + txrec.gasUsed);
console.log(YELLOW + ' Other Attributes: ' + RESET);
console.log(YELLOW + ' Txn Type: ' + RESET + tx.type);
console.log(YELLOW + ' Nonce: ' + RESET + tx.nonce);
console.log(YELLOW + ' Position: ' + RESET + tx.transactionIndex);
console.log(YELLOW + ' Input Data: ' + RESET + tx.input);
//Logs
console.log(RED + '[Logs]' + RESET);
txrec.logs.forEach(log => {
console.log(YELLOW + ' [' + log.logIndex + ']' + RESET);
console.log(YELLOW + ' Address: ' + RESET + log.address);
let functionName = decodeLogs[log.logIndex].name;
let nonIndexedData = '';
decodeLogs[log.logIndex].events.forEach((param, index) => {
if(index == 0) {
functionName = functionName + ' (' + param.type + ' ' + param.name;
}else {
functionName = functionName + ', ' + param.type + ' ' + param.name;
}
//非インデックスパラメータ判定
if(!param.indexed) {
if(nonIndexedData == '') {
nonIndexedData = `${param.name}:${param.value}`;
}else {
nonIndexedData = nonIndexedData + ', ' + `${param.name}:${param.value}`;
}
}
});
functionName = functionName + ')';
console.log(YELLOW + ' Name : ' + RESET + functionName);
console.log(YELLOW + ' Topics : ' + RESET);
log.topics.forEach((topic, index) => {
console.log(YELLOW + ' [' + index + '] ' + RESET + topic);
});
console.log(YELLOW + ' Data : ' + RESET);
console.log(YELLOW + ' [Hex] ' + RESET + log.data);
//非インデックスをDataとして表示
console.log(YELLOW + ' [Dec] ' + RESET + nonIndexedData);
});
}
main();
保存は Ctrl
+x
の後、y
キー押下
ほぼ完コピ?
最後の最後
入力されたトランザクションハッシュからトランザクション情報を出力できるようにして終わり!!
nano explorer_completed.js
プログラムの中身。
※例外処理入れてないからねっ!!
const Xdc3 = require("xdc3");
const abiDecoder = require('abi-decoder');
const readlineSync = require('readline-sync');
//Letter Color
const RED = '\u001b[31m';
const YELLOW = '\u001b[33m';
const BLUE = '\u001b[34m';
const RESET = '\u001b[0m';
const RPC = "https://erpc.xinfin.network";
//OparatorのABIをインポート
const operatorABI = require("./ABIs/Operator.json");
//APIConsumerのABIをインポート
const apiConsumerABI = require("./ABIs/APIConsumer.json");
//Operator ABIをデコーダーにセット
abiDecoder.addABI(operatorABI);
//APIConsumer ABIをデコーダーにセット
abiDecoder.addABI(apiConsumerABI);
async function main() {
try {
console.log(BLUE);
const TXHASH = readlineSync.question('Input Transaction Hash(Plugin OCA was executed): ');
console.log(RESET);
if(TXHASH == '') {
console.log(RED + 'Transaction Hash is a required field.' + RESET);
return;
}
// 接続先のノードを設定
const xdc3 = new Xdc3(new Xdc3.providers.HttpProvider(RPC));
// TxHashからトランザクションを取得
const tx = await xdc3.eth.getTransaction(TXHASH);
if(tx == null) {
console.log(RED + 'Invalid Transaction Hash.' + RESET);
return;
}
// TxHashからトランザクションレシートを取得
const txrec = await xdc3.eth.getTransactionReceipt(TXHASH);
// TxHashが格納されているブロックの情報を取得
const block = await xdc3.eth.getBlock(tx.blockNumber);
//Logsをデコード(復号化)
const decodeLogs = abiDecoder.decodeLogs(txrec.logs);
//編集
//timestamp
const blockDate = new Date(block.timestamp * 1000);
const status = txrec.status ? `${BLUE}Success${RESET}` : `${RED}Failed${RESET}`;
const decodeInput = abiDecoder.decodeMethod(tx.input);
if(decodeInput == undefined) {
console.log(RED + 'This Transaction Hash does not contain any Plugin OCA transactions.' + RESET);
return;
}
//出力
//Overview
console.log(RED + '[Overview]' + RESET);
console.log(YELLOW + ' TransactionHash ' + RESET + tx.hash);
console.log(YELLOW + ' Status and method: ' + RESET + status + ' | ' + tx.input.slice(0,10));
console.log(YELLOW + ' Block: ' + RESET + tx.blockNumber);
console.log(YELLOW + ' Timestamp: ' + RESET + blockDate);
console.log(YELLOW + ' Transaction Action: ' + RESET + decodeInput.name);
console.log(YELLOW + ' From: ' + RESET + tx.from);
console.log(YELLOW + ' To: ' + RESET + tx.to);
console.log(YELLOW + ' Value: ' + RESET + tx.value);
console.log(YELLOW + ' Actual Tx Cost/Fee: ' + RESET +
xdc3.utils.fromWei(xdc3.utils.toBN(tx.gasPrice * txrec.gasUsed), 'ether') + ' XDC');
console.log(YELLOW + ' Gas Price: ' + RESET + tx.gasPrice);
console.log(YELLOW + ' Gas Limit & Usage by Txn: ' + RESET + tx.gas + ' | ' + txrec.gasUsed);
console.log(YELLOW + ' Other Attributes: ' + RESET);
console.log(YELLOW + ' Txn Type: ' + RESET + tx.type);
console.log(YELLOW + ' Nonce: ' + RESET + tx.nonce);
console.log(YELLOW + ' Position: ' + RESET + tx.transactionIndex);
console.log(YELLOW + ' Input Data: ' + RESET + tx.input);
//Logs
console.log(RED + '[Logs]' + RESET);
txrec.logs.forEach(log => {
console.log(YELLOW + ' [' + log.logIndex + ']' + RESET);
console.log(YELLOW + ' Address: ' + RESET + log.address);
let functionName = decodeLogs[log.logIndex].name;
let nonIndexedData = '';
decodeLogs[log.logIndex].events.forEach((param, index) => {
if(index == 0) {
functionName = functionName + ' (' + param.type + ' ' + param.name;
}else {
functionName = functionName + ', ' + param.type + ' ' + param.name;
}
//非インデックスパラメータ判定
if(!param.indexed) {
if(nonIndexedData == '') {
nonIndexedData = `${param.name}:${param.value}`;
}else {
nonIndexedData = nonIndexedData + ', ' + `${param.name}:${param.value}`;
}
}
});
functionName = functionName + ')';
console.log(YELLOW + ' Name : ' + RESET + functionName);
console.log(YELLOW + ' Topics : ' + RESET);
log.topics.forEach((topic, index) => {
console.log(YELLOW + ' [' + index + '] ' + RESET + topic);
});
console.log(YELLOW + ' Data : ' + RESET);
console.log(YELLOW + ' [Hex] ' + RESET + log.data);
//非インデックスをDataとして表示
console.log(YELLOW + ' [Dec] ' + RESET + nonIndexedData);
});
}catch(e) {
console.log("["+e.name+"]"+e.message);
}
}
main();
保存は Ctrl
+x
の後、y
キー押下
付録
今回、関数名を取るところが(知らない状態だと)まず1つ目の難関でしたが、ABIを使ってデコードするようにしました。
調査の過程で、関数名を取るだけであれば、ABIがなくてももっと簡単に取得できる方法がありました。