こんにちは、たわです。0x APIを使用したときのメモです。
ERC20の残高を取得する
Ethereum上でトークンを発行するときの規格としてERC20があります。
その規格を踏襲しているトークンであれば同じAPIで操作することができるので、便利です。
ですが、実際に各トークンを発行しているコントラクトのアドレスが別々なので、それをまず把握して、そのコントラクトに対してリクエストを発行する必要があります...。
これが地味にめんどくさいのですが、0x APIを使って少し楽になるような実装をしてみました。
実装の流れ
それでは実際にやってみた実装の流れです。
0x APIでトークンの情報を取得
0xはEthereum上のトークンの交換をスムーズに行うために開発されているプロトコルです。
この0xが実際に処理を行うために必要なロジックや便利ツールを用意してくれているのでそれを活用しました。
0x: Powering the decentralized exchange of tokens on Ethereum
用意された便利ツールの中で、最近公開された0x APIを使いました。
ブロックチェーンは意識せずに普通に情報が取得できるエンドポイントになっているので詳しくなくても取得できます。
その中に、対応したトークンの情報を返すエンドポイントがあります。
0x: Powering the decentralized exchange of tokens on Ethereum
こんな感じでリクエストを飛ばすことができます。
const queries = {}; const qs = new URLSearchParams(queries); const url = new URL("https://api.0x.org/swap/v0/tokens" + "?" + qs); const response = await fetch(url); const json = await response.json();
すると、レスポンスはこのように返ってきます。
[ { symbol: "DAI", address: "0x6b175474e89094c44da98b954eedeac495271d0f", name: "Dai Stablecoin", decimals: 18 }, { symbol: "REP", address: "0x1985365e9f78359a9B6AD760e32412f4a445E862", name: "Augur Reputation", decimals: 18 }, // ... ]
これでトークンの情報とアドレスがわかります。
これらの情報は変更されることは基本的にないと思うので、その都度APIを飛ばさずローカルに定数として置いても良いのかもしれません。
トークンごとに残高を取得する
上で取得したトークンのアドレスを使って順番に残高を取得しています。
同じEthereum上の自分のウォレットの残高を見たいだけなのに、トークンの数だけリクエストを飛ばさないといけないのがどうも不便な感じがしますが、現状仕方なさそうです。
コードは全体としてこんな感じになります。元々はクラスにしたりしていたのですが、説明のために一つの関数の中に全部盛りにしてしまっています。
public fetchTokenBalance = async () => { const ERC20_ABI = [ // balanceOf { constant: true, inputs: [{ name: "_owner", type: "address" }], name: "balanceOf", outputs: [{ name: "balance", type: "uint256" }], type: "function" } ]; const decodeAmount = (num: BigNumber, decimalPlace: number): BigNumber => { return num.div(10 ** decimalPlace); }; const address = process.env.WALLET_ADDRESS; const hostEthreumNode = process.env.INFURA_URL; const provider = new Web3.providers.HttpProvider(hostEthreumNode!); const web3 = new Web3(provider as any); const balance = ( await Promise.all( Object.keys(ERC20_TOKENS).map(async k => { const t = ERC20_TOKENS[k]; const contract = new web3.eth.Contract(ERC20_ABI as any, t.address); const balance = await contract.methods .balanceOf(address) .call({ from: address }); return { token: t.symbol, balance: balance, decimals: t.decimals }; }) ) ).reduce<Record<string, number>>((prev, cur) => { const amount = decodeAmount(new BigNumber(cur.balance), cur.decimals); prev[cur.token] = amount.toNumber(); return prev; }, {}); console.log({ balance }); };
順番に説明していきます。
Infuraでアカウント作成
本来、Ethereumのノードを自分で用意する必要がありますが、Infuraはそれをサービスとして提供しています。
なので、Infuraでアカウントを作成し、Infura経由でEthereumへアクセスすることができます。最低限の開発段階であれば無料の範囲内で足りると思います。
自分用のエンドポイントが発行されます。それを環境変数(INFURA_URL
)にセットしています。
残高を確認したいウォレットのアドレスを確認
残高を確認したいウォレットのアドレスも環境変数(WALLET_ADDRESS
)にセットしましょう。
ABIを用意
ABIはバイナリを呼び出すための型定義ファイルのようなものです。詳細は以下がわかりやすかったです。
Contract Application Binary Interface(ABI)とは - アルゴリズムとかオーダーとか
今回はERC20規格なので、インターフェイスも共通です。すべて使ってもいいですが、使用するbalanceOf
メソッドの定義のみを上のコード例では入れています。
// この部分 const ERC20_ABI = [ // balanceOf { constant: true, inputs: [{ name: "_owner", type: "address" }], name: "balanceOf", outputs: [{ name: "balance", type: "uint256" }], type: "function" } ];
各トークンコントラクトアドレスにリクエスト
情報は揃ったので実際に、残高を取得していく処理です。
先ほど取得したトークンの情報はERC20_TOKENS
に入れている前提でコードが書かれています。
それを順番に取り出し、並列でAPIリクエストを投げています。
const contract = new web3.eth.Contract(ERC20_ABI as any, t.address); const balance = await contract.methods.balanceOf(address).call({ from: address });
web3を使えば、こんな感じで残高を取得できます。
残高の桁数を直す
あとは、返ってきた情報を必要な形に加工するのですが、残高の扱いに少しくせがあるので注意です。
例えば、ZRXを0.3
持っている場合、残高の数値は3000000000000000
(3と0が17個)になっています。
なぜかというと、TOKENの情報の中のdecimals
に書いてあるように有効桁数が18桁で、残高は最小桁数で表現されるという仕様だからのようです。
つまり、0.00000000000000001
が最小単位(= 1)として表現されます。少しややこしいですね...。
なので、decodeAmount
に書かれているように、10の有効桁数乗で割るという処理を入れています。
こんな感じになりました
今回は、reduce
関数でトークンのシンボルがキーになるオブジェクトに直したのでこんな感じのデータになりました。
{ DAI: 0, WETH: 0.037725, ZRX: 0, LINK: 1.6150693860197611, REN: 0, ... }
数としてかなりあるので、必要なトークンだけ取得するように処理を書き直すとより良いかもしれません。
まとめ
ERC20のトークンの残高を取得しました。0x APIは便利なのですが、ものによってはEthereumノードにアクセスしないといけなかったりと少しややこしい感じがしています。
知見もあまりネット上になくかなり探り探りやっているので、また雑に色んなものを公開していきたいと思います。ベターな方法があればぜひ教えていただければ幸いです。