はじめに
TruffleやOpenZeppelinを利用して発行したERC20ベースのTokenを送金するのに、MistやMetamaskを使うことがあると思います。
本記事ではこれらを用いず、コードベースで作成したトランザクションをネットワークに送る方法を行います。
EthereumのTransactionについて
Ethereum yellow paper の4-2に詳細が書いてありますが、トランザクションは、
- nonce: 送信者が今までに送信したTransactionの数
- gasPrice:Transactionが実行された結果として消費されるgasの価格 (wei/gas)
- gasLimit:Transactionが実行される前に支払われるgas. このgasLimitを超えてTransactionが実行されるということはない
- to:送信先。今回の場合だとERC20ベースTokenのコントラクトアドレス
- value:Transactionと同時に送るETH
- data:コントラクトを実行する内容が入っている
- v, r, s :Transactionの署名と対応しているデータ
で基本構成されています。このデータの形式によって、コントラクト作成なのか、アカウント作成なのか、コントラクト内のメソッドを呼び出すのか、変わってきます。
Transactionを作成するにはこの仕様に沿う必要があります。go-ethereum上では、transaction.goにstructとして定義されています。
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
このTransactionを作るには、このtransaction.go内のfunc NewTransaction() を用いるのですが、ここでポイントとなるのは、dataの準備の仕方です。
func NewTransaction(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction { |
return newTransaction(nonce, &to, amount, gasLimit, gasPrice, data) |
}
TransactionのDataについて
Dataには、コントラクトで実行するメソッドについてのデータを入れる必要があります。Ethereumで実行される為には、The Contract Application Binary Interface (ABI)という形式にエンコードしなければなりません。
ABIの仕様の詳細は、Contract ABI Specificationに書かれています。
このdataは、
- コントラクトで呼び出したいメソッド(Method ID)
- メソッド引数をそれぞれハッシュ化して32byteにしたもの
をつなぎ合わせた形式です。(例としては、Contract ABI Specificationのexampleが分かりやすいです。)
Method IDとは、コントラクトのメソッドをbyte形式にしてKECCAK-256でハッシュかし、先頭から4バイト取ってきたものです。
今回は、発行したトークンを送金したいので、ERC20で定義されている、transfer(address,uint256) を使います。
コードでは下記のようになります。
transferFnSignature := []byte("transfer(address,uint256)")
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]
引数も同様に、バイト形式にしてハッシュ化します。ハッシュ化したのち、32バイトになるように0埋めを行います。この0埋めもgo-ethereumでは準備されています。LeftPadBytesメソッドを使います。
toAddress := common.HexToAddress(receiveAddress)
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
pIntAmount := big.NewInt(int64(amount))
paddedAmount := common.LeftPadBytes(pIntAmount.Bytes(), 32)
以上のmethodId、oaddedAddress、paddedAmountを、appendを使って結合させれば、dataの作成が完了です。
//トランザクションで送るデータを作成
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
Transactionの送信
作成したトランザクションを自分で署名して、ネットワークに送れば完了です。
signedTx, err := types.SignTx(tx, types.HomesteadSigner{}, privateKey)
if err != nil {
log.Fatal(err)
}
//サインしたトランザクションをRopstenNetworkに送る。
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
実行環境
今回のGo, go-ethereumの環境は下記の通りとなっています。
$ geth version
Geth
Version: 1.8.13-stable
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.10.3
Operating System: darwin
GOPATH=/Users/akitk/go_workspace
GOROOT=/usr/local/opt/go/libexec
実際に作成したコード
今回、Ropstenネットワークに接続する為に、infuraを利用しました。ご自身で準備した、Ropstenネットワークに繋がっているethereumクライアントに接続しても良いと思います。(下記のprivateKey、contractAddressなど、ダミーデータを入れています。)
package main
import (
"context"
"crypto/ecdsa"
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/sha3"
"github.com/ethereum/go-ethereum/ethclient"
)
func sendExternalRawTransaction(receiveAddress string, amount float64) (transaction string) {
//Ropstenネットワークに接続
client, err := ethclient.Dial("https://ropsten.infura.io")
if err != nil {
log.Fatal(err)
}
//PrivateKeyを読み込む
privateKey, err := crypto.HexToECDSA("f47321dc18a9bcafa063d0980cbcdcb1ff19e7430bf501612c21da0648cf2f8")
if err != nil {
log.Fatal(err)
}
// PrivateKeyからPublickeyを生成
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
log.Fatal("Error casting public key to ECDSA")
}
//PublicKeyから、送金主アドレスを生成
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
//Ropstenネットワークから、Nonce情報を読み取る
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Fatal(err)
}
//トークン送金Transactionをテストネット送るためのgasLimit、
value := big.NewInt(0) //(オプション)後で使用する関数NewTransactionの引数で必要になるため設定。Transactionと同時に送るETHの量を設定できます。
gasLimit := uint64(2000000)
//ロプステンネットワークから、現在のgasPriceを取得。トランザクションがマイニングされずに放置されることを防ぐ。
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
//送金先を指定
toAddress := common.HexToAddress(receiveAddress)
//トークンコントラクトアドレスを指定
tokenAddress := common.HexToAddress("0x43aff81f88c18bc85281039df68002872b233afc")
//ERC20のどの関数を使用するか指定。https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sendtransaction
transferFnSignature := []byte("transfer(address,uint256)")
//hash化し、先頭から4バイトまで取得。これで使用する関数を指定したことになる。
hash := sha3.NewKeccak256()
hash.Write(transferFnSignature)
methodID := hash.Sum(nil)[:4]
//0埋め
paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
//送金額を設定
pIntAmount := big.NewInt(int64(amount))
//0埋め
paddedAmount := common.LeftPadBytes(pIntAmount.Bytes(), 32)
//トランザクションで送るデータを作成
var data []byte
data = append(data, methodID...)
data = append(data, paddedAddress...)
data = append(data, paddedAmount...)
/***** Preparing signed transaction *****/
tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)
signedTx, err := types.SignTx(tx, types.HomesteadSigner{}, privateKey)
if err != nil {
log.Fatal(err)
}
//サインしたトランザクションをRopstenNetworkに送る。
err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Signed tx sent: %s", signedTx.Hash().Hex())
return signedTx.Hash().Hex()
}
上記の関数で、ご自身で発行したERC20トークンコントラクトのアドレス、ご自身のPrivateKey、送金先、金額を変更すれば、Tokenの送金ができます。また、ERC20の他のメソッドを実行するトランザクションを作成されたい場合は、dataをそのメソッドに合わせて変更すれば、作成が可能です。
以上、ご参考になれば幸いです。