概要
本記事では、cosmosのCLIを使わず、cosmos-sdkを使ってトランザクションを送信してみたい方を対象に、サンプルコードを用いながら具体的な方法を説明していきます。
本記事ではシングルシグアカウントの送金のみを扱います。
gasLimitの推定、マルチシグアカウントについては本記事で扱いませんのでご了承ください。
cosmos-sdkとは
cosmos-hubというブロックチェーンのソフトウェア開発キットで、goで実装されています。
サンプルコード
- サンプルコードを用いて具体的な手順を説明します。
- 依存関係:
- go1.18
- github.com/cosmos/cosmos-sdk v0.45.15-ics
package main
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client"
clienttx "github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
"github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/go-bip39"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
)
// singlesigの送金のみ扱う。multisignの送金は別記事で説明予定。
func main() {
// 1. ニーモニックからaddressの秘密鍵を復元
privKey, err := recoverPrivKeyFromMnemonic("mnemonic") // mnemonicにウォレットのニーモニックを入れてください
pubKey := privKey.PubKey()
fromAddress := sdk.AccAddress(pubKey.Address())
clientCtx, err := createContext(fromAddress)
// 2. 送金元のアカウントの状態を取得
account, err := clientCtx.AccountRetriever.GetAccount(clientCtx, fromAddress)
if err != nil {
panic(err.Error())
}
accountNumber := account.GetAccountNumber() // accountNumber: 入金された時にわかるアカウントに付与されるユニークな数字
sequence := account.GetSequence() // sequence: アカウントのシークエンス
txf := createFactory(clientCtx, accountNumber, sequence)
// 3. factoryを使って署名前のトランザションを作成
coins := []sdk.Coin{
{
Denom: "uatom",
Amount: sdk.NewInt(int64(1)), // 例: 1uatom = 0.000001atomを送る場合。
},
}
msg := &banktypes.MsgSend{
FromAddress: fromAddress.String(),
ToAddress: "toAddress", // toAddressに送金先アドレスを入れてください。
Amount: coins,
}
if err := msg.ValidateBasic(); err != nil {
panic(err.Error())
}
// txBuilderはcosmos-sdkの署名前のトランザションを作成するための構造体
txBuilder, err := clienttx.BuildUnsignedTx(txf, msg, msg)
if err != nil {
panic(err.Error())
}
// トランザクションの署名者の情報
signerData := authsigning.SignerData{
ChainID: clientCtx.ChainID,
AccountNumber: accountNumber,
Sequence: sequence,
}
// 署名されるデータ
signBytes, err := clientCtx.TxConfig.SignModeHandler().GetSignBytes(DefaultSignModes[0], signerData, txBuilder.GetTx())
if err != nil {
panic(err.Error())
}
// 4. 署名を作成
signature, err := privKey.Sign(signBytes)
if err != nil {
panic(err.Error())
}
// 署名者と署名のデータ
sigData := signing.SingleSignatureData{
SignMode: DefaultSignModes[0],
Signature: signature,
}
// cosmos-sdkで署名を扱うためのデータ構造
sigV2 := signing.SignatureV2{
PubKey: pubKey,
Data: &sigData,
Sequence: sequence,
}
// 5. 署名をtxBuilderにセット
if err := txBuilder.SetSignatures(sigV2); err != nil {
panic(err.Error())
}
// 6. rawTxを作成
rawTx, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
panic(err.Error())
}
// 7. rawTxをnodeに送信
res, err := clientCtx.BroadcastTx(rawTx)
if err != nil {
panic(err.Error())
}
fmt.Println(res.TxHash)
fmt.Println(res.Code) // 送信に成功した場合、0が返されます。
fmt.Println(res.RawLog) // 送信に成功した場合、"[]"が返されます。
}
// ニーモニックをimportしてaddressの秘密鍵を復元します
func recoverPrivKeyFromMnemonic(mnemonic string) (*secp256k1.PrivKey, error) {
seed := bip39.NewSeed(mnemonic, "")
master, ch := hd.ComputeMastersFromSeed([]byte(seed))
priv, err := hd.DerivePrivateKeyForPath(master, ch, "m/44'/118'/0'/0/0") // key path: BIP44参照。
if err != nil {
return nil, err
}
return &secp256k1.PrivKey{Key: priv}, nil
}
// cosmos-sdkでトランザクションを生成するためのcontextを作成します。
func createContext(fromAddress sdk.AccAddress) (client.Context, error) {
encodingConfig := simapp.MakeTestEncodingConfig()
nodeURI := "https://rpc.sentry-01.theta-testnet.polypore.xyz:443"
rpcClient, err := rpchttp.New(nodeURI, "/websocket")
if err != nil {
return client.Context{}, err
}
var clientCtx client.Context
return clientCtx.WithChainID("theta-testnet-001").
WithFromAddress(fromAddress).
WithTxConfig(encodingConfig.TxConfig).
WithLegacyAmino(encodingConfig.Amino).
WithInterfaceRegistry(encodingConfig.InterfaceRegistry).
WithNodeURI(nodeURI).
WithAccountRetriever(types.AccountRetriever{}).
WithClient(rpcClient).WithBroadcastMode("sync"), nil
}
// cosmos-sdkでトランザクションの作成と署名を容易にするためのFactoryを作成します。
func createFactory(clientCtx client.Context, accountNumber, sequence uint64) clienttx.Factory {
var txf clienttx.Factory
return txf.WithChainID(clientCtx.ChainID).
WithTxConfig(clientCtx.TxConfig).
WithAccountRetriever(clientCtx.AccountRetriever).
WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON).
WithAccountNumber(accountNumber).
WithSequence(sequence).
WithGas(200000). // 仮で大きめの固定値を使います。シミュレート関数を使用してgasLimitを推定することもできます。
WithGasPrices("0.0025uatom") // nodeの推奨設定値
}
サンプルコードの補足
ニーモニックからaddressの秘密鍵を復元
- ニーモニックについては、BIP39を参照してください。gaiaや別のwalletで作成したアカウントの任意のニーモニックをimportすることができます。
- key pathについては、BIP44を参照してください。
"m/44'/118'/0'/0/0"
のようなフォーマットで入力してください。
送金元のアカウントの状態を取得
- cosmosでは、アドレスに最初の入金があった後にユニークなaccountNumberが付与されます。
- また、トランザクションを送信するたびにアドレスのsequenceがインクリメントされていきます。
- トランザクションの作成と署名に、このaccountNumberとsequenceを使用します。
factoryを使って署名前のトランザションを作成
- トランザクションの作成と署名を容易にするためにcosmos-sdkのFactoryを使用します。
- txBuilderは、署名前のトランザションを作成するためのcosmos-sdkで実装されている構造体です。
rawTxをnodeに送信
- トランザクションの送信に成功した場合、mintscan.io のようなブロックチェーンエクスプローラーでトランザクションを確認できるようになります。
まとめ
本記事では、cosmos-hub(gaia)のCLIを使わず、サンプルコードを用いながらシングルシグの送金の方法を説明しました。
記事内で扱わなかった、cosmos-sdkを用いたgasLimitの推定とマルチシグアカウントについては、また別の機会に記事を書きたいと思います!
株式会社Gincoではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。
参考
- 公式ページ: https://cosmos.network/
- Cosmos Hub: https://hub.cosmos.network/main/hub-overview/overview.html
- Cosmos SDK: https://docs.cosmos.network/main
- Gaia API doc: https://v1.cosmos.network/rpc/v0.45.1