Infura APIを使用してアリスからボブにERC20トークンを送る(Rubyでトランザクションを生成・署名しInfura APIを使ってEthereumネットワークにブロードキャストする)

Jun 1, 2020 00:26 · 1659 words · 4 minute read

少し前にAkhTokenというERC20トークンを作ってみたが、このトークンをRubyのプログラムから他の人に送れるようになったのでそのメモ。

AkhtTokenはInfuraというノードホスティングサービスを使っていて、InfuraはブロックチェーンにアクセスするためのJSON RPC APIを提供している。

トークンを送るためにはERC20標準に定義されているtransferというコントラクトの関数をコールすればいいのだけど、InfuraのAPIとしては提供されていない。 つまり宛先と量をピッと指定してシュッとトークンを送信することはできない。

じゃあどうするかというと、eth_sendRawTransactionという生の(?)トランザクションを受けつけるものがあるのでこれを使う。

つまり、トランザクションのデータを作り、送信者の秘密鍵で署名したものをInfura APIに投げるという感じになる。

まずはethgemを使ってトランザクションを作る。

tx = Eth::Tx.new({
  data: hex_data,             # コントラクトに送るデータ(後述)
  gas_limit: 4_000_000,       # 今回はテキトーに指定
  gas_price: 22_000_000_000,  # 今回はテキトーに指定
  nonce: nonce.hex,           # ナンス(後述)
  to: contract_address,       # コントラクト(AkhToken)のアドレス(受信者のアドレスではない)
  value: 0                    # 送信するトークンの量ことではない
})

これでトランザクションが生成できる。
後述の部分については以下。

data: hex_dataは、コールしたいコントラクトの関数シグネチャと、その関数に渡す引数から生成する。

  • 今回はABI仕様のtransfer(address,uint256)という関数を実行してトークンを送信したいのでこれを使う
  • 引数であるaddress(受信者のアドレス)unit256(送信するトークンの量)を指定する

このあたりが詳しい。

# function selector
signature = "transfer(address,uint256)"
selector = Digest::SHA3.hexdigest(signature, 256).slice(0..7) # 最初の4bytes

# transferの1つ目の引数(address)
arg1_address = reciever_address.slice(2..-1).rjust(64, "0")

# transferの2つ目の引数(uint256)
amount = 1_000_000_000_000_000_000 # wei
arg2_uint = amount.to_s(16).rjust(64, "0")

hex_data = "0x" + selector + arg1_address + arg2_uint

このデータはEVM(Ethereum Virtual Machine)のコントラクト呼び出しとして解釈されて、コントラクトに定義されてる関数が呼び出される、という感じ。

次にnonce: nonce.hexの部分。

ここにはナンスと呼ばれる値を指定する。ナンスというのは重要な仕組みで、これによってトランザクションの作成順を認識したり、トランザクションの複製を防いだり(重複保護)できる。具体的な値としては、そのアドレスから送信された承認済みトランザクション数を使う。

これはInfura APIのeth_getTransactionCountで取得できる。

これでトランザクションができた。

次は送信者の秘密鍵でこのトランザクションに署名する。

key = Eth::Key.new priv: sender_private_key
tx.sign key

これで署名済みトランザクションができた。あとはInfura APIを叩けば実際にトークンがアリスからボブに送られる。

uri = URI.parse("https://xxx.infura.io/xx/xxx") # infuraのエンドポイント

req = Net::HTTP::Post.new(uri)
req.content_type = "application/json"
req.body = { jsonrpc: "2.0", method: "eth_sendRawTransaction", params: [tx.hex], id: 1 }.to_json
req_options = { use_ssl: uri.scheme == "https" }

response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
  http.request(req)
end

めでたくAkhTokenを任意のアドレスに送りつけるスクリプトが完成した。

これがわかればどんなコントラクト関数でも実行できる(ような気がしてる)。

トランザクションにはtoは必要だけどfrom(送信者のアドレス)は不要。なぜなら、送信者のアドレスは送信者の署名から導出できるから(秘密鍵による署名から公開鍵が導出できて、公開鍵からアドレスが導出できる)。

あとEthereum本来のトランザクションにはECDSA署名に関するv, r, sという要素がある。今回のRubyコードでは署名時にgemが内部で計算してくれてるので使用者が意識することはないけど、何者なのかぼんやりとしかわかってないので気持ち悪さもある…とはいえ、軽く調べた感じこのあたりは難しい話っぽいから一旦思考停止することにした。

とにかく、AkhTokenを任意のアドレスに送りつけるスクリプトが完成した。