LoginSignup
1
4

More than 1 year has passed since last update.

[Ethereum] web3.jsでテストネットワークにコントラクトをデプロイしてみる

Posted at

はじめに

Ethereumのアプリを開発する際、Remixやtruffleといった開発用フレームワークを使用すると比較的簡単に開発が出来ると思います。
今回そういったフレームワークを使わずにテストネットワークにスマートコントラクトをデプロイしようとして10日くらいハマってしまいました。。
truffleを使用したデプロイ方法は結構ネット上に転がっていたんですが、truffleを使用しない場合の記事があまり無かったので解決できた方法を備忘録として残しておきます。

前提

  1. MetaMaskにアカウントを持っていること(networkはropstenを使用)
  2. Infuraにアカウントを持っていること(networkはropstenを使用)
  3. Node.js, npm, Web3.jsはインストール済みであること

ハマったところ

  1. MetaMask(ウォレット)で管理しているアカウントを、EthereumノードのInfuraエンドポイントと紐付ける処理
  2. テストネットワーク上にコントラクトをデプロイするトランザクションを生成するため、プライベートキーでトランザクションに署名し、署名済みトランザクションをネットワークに送る処理

最初、これまでHttpProviderに localhost:8545 を指定していた箇所をInfuraのエンドポイントに変更すれば良いと考えていましたが、これだとMetaMaskのアカウントを使用できずアカウントが無い形になってしまっていました。

sample.js
const Web3 = require('web3');
// providerにInfuraのAPIエンドポイントを設定
const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/<my Infura api endpoint>"));

// アカウントを取得する
web3.eth.getAccounts().then(console.log);

こんな感じでプロバイダーを設定しアカウントを取得してみます

$ node sample.js
> []

空のアカウントリストが返ってきます。
まぁ、どこにもMetaMaskの情報を記載していないので当然と言えば当然ですけど。。

どうやらプライベートネットワーク上で色々試していた時はクライアントツールのGethがバックグラウンドでアカウントの管理やトランザクションへの署名をしてくれていたようです。

これの解決に7日くらいかかりました。。。

解決策

HDWalletProviderを使う。
ずばりコレでした。

HDWalletとは

HDWallet(Hierarchy Deterministic Wallet)とは、1つのSeedから秘密鍵を生成し、階層的に管理するウォレットのことです。
特徴は、

  • Seedから鍵を階層的に生成する
  • Seedがあれば何度でもHDWalletを再構築できる

というもので、このHDWalletを実装したプロバイダーを使うことでMetaMaskのアカウントをInfura上で使用することが可能になります。

ちなみにHDWalletで必要なSeedの値はMetaMaskではmnemonic(ニーモニック)と呼ばれる12~24の単語の集まりです。なのでweb3でHDWalletを使用する際は前もってMetaMaskのmnemonicを用意しておく必要があります。

実装してみる

実装の流れ

トランザクションを作成し、Ethereumネットワークにデプロイするには、

  1. HDWalletProviderをインストール
  2. コントラクトオブジェクトを生成
  3. トランザクションを作成
  4. トランザクションに署名し、トランザクションのrawデータを取得
  5. 取得したrawデータをネットワークに送りデプロイする

という流れで進めるようです。
なんとなく全体の流れはイメージ出来たので1つずつ順を追って実装してみます。

@truffle/hdwallet-providerのインストール

まずはnpm公式を参考に@truffle/hdwallet-providerをインストールします。

npm install @truffle/hdwallet-provider

インストールディレクトリのnode_moduleに@truffleが格納されていれば使用可能な状態になっています。

コントラクトの作成とコンパイル

今回は簡単にgetリクエストを受け付けると"Hello World"の文字列をレスポンスするコントラクトを作成してみます。

sample_greeting.sol
contract SampleGreeting {
  string greeting = 'Hello World';

  function getGreet() public view returns (string memory) {
    return greeting;
  }
}

コンパイルします。

$ solc --bin --abi sample_greeting.sol 

> ======= sample_greeting.sol:SampleGreeting =======
Binary:
60806040526040518060400160405280600b81526020017f48656c6c6f20576f726c640000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610105565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b6000600282049050600182168061011d57607f821691505b6020821081141561013157610130610137565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610232806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d705a4b514610030575b600080fd5b61003861004e565b6040516100459190610119565b60405180910390f35b60606000805461005d9061018a565b80601f01602080910402602001604051908101604052809291908181526020018280546100899061018a565b80156100d65780601f106100ab576101008083540402835291602001916100d6565b820191906000526020600020905b8154815290600101906020018083116100b957829003601f168201915b5050505050905090565b60006100eb8261013b565b6100f58185610146565b9350610105818560208601610157565b61010e816101eb565b840191505092915050565b6000602082019050818103600083015261013381846100e0565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561017557808201518184015260208101905061015a565b83811115610184576000848401525b50505050565b600060028204905060018216806101a257607f821691505b602082108114156101b6576101b56101bc565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f830116905091905056fea2646970667358221220316cca1abf85d891616645d88d5a234baaa634f4345b2c4d557a0761bf6d2a2b64736f6c63430008070033
Contract JSON ABI
[{"inputs":[],"name":"getGreet","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]

バイナリデータとJSON ABIが取得出来ました。

コントラクトオブジェクトを作成する

コントラクトの記述は出来ましたので、次はjsファイルを作成していきます。
まずはコントラクトオブジェクトを生成し中身を確認してみます。

sample.js
// web3パッケージをインポートし、インスタンスを作成
const Web3 = require('web3');

// MetaMaskのニーモニックを変数に格納
const mnemonic = 'My MetaMask 12 words';

// @truffle/hdwallet-providerをインポートし、MetaMaskニーモニックとInfuraのAPIエンドポイントを引数にインスタンスを作成
const HDWalletProvider = require('@truffle/hdwallet-provider');
let provider = new HDWalletProvider(mnemonic, 'https://ropsten.infura.io/v3/<my Infura api endpoint>', 0, 1);

// HDWalletProviderを引数としてweb3インスタンスを作成
let web3 = new Web3(provider);

// コントラクトのバイナリデータとJSON ABIを変数に格納
let bin = '0x60806040526040518060400160405280600b81526020017f48656c6c6f20576f726c640000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610105565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b6000600282049050600182168061011d57607f821691505b6020821081141561013157610130610137565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610232806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d705a4b514610030575b600080fd5b61003861004e565b6040516100459190610119565b60405180910390f35b60606000805461005d9061018a565b80601f01602080910402602001604051908101604052809291908181526020018280546100899061018a565b80156100d65780601f106100ab576101008083540402835291602001916100d6565b820191906000526020600020905b8154815290600101906020018083116100b957829003601f168201915b5050505050905090565b60006100eb8261013b565b6100f58185610146565b9350610105818560208601610157565b61010e816101eb565b840191505092915050565b6000602082019050818103600083015261013381846100e0565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561017557808201518184015260208101905061015a565b83811115610184576000848401525b50505050565b600060028204905060018216806101a257607f821691505b602082108114156101b6576101b56101bc565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f830116905091905056fea2646970667358221220316cca1abf85d891616645d88d5a234baaa634f4345b2c4d557a0761bf6d2a2b64736f6c63430008070033';
let abi = [{"inputs":[],"name":"getGreet","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}];
let data = {
  'data': bin
}
// コントラクトオブジェクトを生成
let contract = new web3.eth.Contract(abi, null, data);
console.log('contract: ', contract);

ポイントは、HDWalletProviderをnewする時にMetaMaskのニーモニック(Seed)とInfuraのAPIエンドポイントを渡してインスタンスを作成するところです。
ニーモニックを渡すことでHDWalletProviderでMetaMaskのアカウントを構築できるので、同じアカウントを使用することが出来ます。 ←まだ勉強中なのでこの表現が正しいか微妙です。。

とりあえずこの内容でコントラクトオブジェクトの中身を見てみます。

$ node sample.js
> contract:  Contract {
  ~中略~
  _provider: HDWalletProvider {
    walletHdpath: "m/44'/60'/0'/0/",
    wallets: { '0x1a4d93e78905a7f4fc7a2b8ca1b789aaaa10f102': [Wallet] },
    addresses: [ '0x1a4d93e78905a7f4fc7a2b8ca1b789aaaa10f102' ],
    chainSettings: {},
    engine: Web3ProviderEngine {
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: 30,
      _blockTracker: [PollingBlockTracker],
      _ready: [Stoplight],
      currentBlock: null,
      _providers: [Array],
      silent: false,
      _running: true,
      [Symbol(kCapture)]: false
    },
    hdwallet: EthereumHDKey { _hdkey: [HDKey] },
    initialized: Promise { <pending> },
    hardfork: 'istanbul'
  },
 ~中略~
  _address: null,
  _jsonInterface: [
    {
      inputs: [],
      name: 'getGreet',
      outputs: [Array],
      stateMutability: 'view',
      type: 'function',
      constant: true,
      payable: undefined,
      signature: '0xd705a4b5'
    }
  ]
}

_providerにHDWalletProviderの値が格納されていますね。
アドレスもMetaMaskのアドレスと同じ値になっているのでテストネットワーク上でアカウントを取得することが出来ます。

あとで使うので _jsonInterfaceのsignatureの値を控えておきます。

トランザクションに必要な値を取得する

アカウントの取得が出来たので、次はトランザクションを作成するために必要な値を集めます。
web3.jsのドキュメントによると今回コントラクトをデプロイするトランザクションに署名するために必要なデータは、

  • from
  • nonce
  • chainId
  • gas
  • gasPrice
  • data

のようです。
from, chainId, dataの値は分かっているので、nonce, gas, gasPriceの値を取得します。
以下、sample.jsを編集していきます。

sample.js
//省略

// gasPriceを取得する
web3.eth.getGasPrice().
   then((averageGasPrice) => {
       console.log("gasPrice: " + averageGasPrice);
   }).catch(console.error);

// nonceを取得する
web3.eth.getAccounts((error, result) => {
  console.log('result: ', result);
  web3.eth.getTransactionCount(result[0], (error, response) => {
    console.log('nonce: ', response);
  })
})

// gasを取得する
contract.deploy().estimateGas().
    then((estimatedGas) => {
        console.log("gas: " + estimatedGas);
    }).catch(console.error);

これを実行してみます。

$ node sample.js
> result:  [ '0x1A4D93E78905A7f4fC7a2B8ca1B789AaAA10f102' ]
  gasPrice: 1940000017
  nonce:  2
  gas: 201453

取得出来ました。
これをもとにトランザクションのオブジェクトを作成します。

sample.js
//省略

// トランザクションに必要な値を変数に格納
let sender_address = '0x1A4D93E78905A7f4fC7a2B8ca1B789AaAA10f102';
let nonce = '2';
let chainId = '3';
let gas = 201453;
let gasPrice = '1940000017';

// トランザクションオブジェクトを作成
let tx = {
  'from': sender_address,
  'nonce': nonce,
  'chainId': chainId,
  'gas': gas,
  'gasPrice': gasPrice,
  'data': bin,
}

gas以外はstring型なので注意が必要です。

トランザクションに署名する

データは揃ったので、次はトランザクションに署名します。
web3.eth.signTransaction()メソッドを使い署名を実行します。
※web3.jsにはweb3.eth.accounts.signTransaction()メソッドもありますが、正直ドキュメントを読んでも違いがよく分かっていません。(英語だし。) ただこのメソッドではうまく署名できませんでした。

sample.js
// 省略

web3.eth.signTransaction(tx, (error, response)=>{
    console.log(response);
});

実行してみます。

$ node sample.js
>
{
  raw: '0xf903f9028473a20d11830312ed8080b903a760806040526040518060400160405280600b81526020017f48656c6c6f20576f726c640000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610105565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b6000600282049050600182168061011d57607f821691505b6020821081141561013157610130610137565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610232806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d705a4b514610030575b600080fd5b61003861004e565b6040516100459190610119565b60405180910390f35b60606000805461005d9061018a565b80601f01602080910402602001604051908101604052809291908181526020018280546100899061018a565b80156100d65780601f106100ab576101008083540402835291602001916100d6565b820191906000526020600020905b8154815290600101906020018083116100b957829003601f168201915b5050505050905090565b60006100eb8261013b565b6100f58185610146565b9350610105818560208601610157565b61010e816101eb565b840191505092915050565b6000602082019050818103600083015261013381846100e0565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561017557808201518184015260208101905061015a565b83811115610184576000848401525b50505050565b600060028204905060018216806101a257607f821691505b602082108114156101b6576101b56101bc565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f830116905091905056fea2646970667358221220316cca1abf85d891616645d88d5a234baaa634f4345b2c4d557a0761bf6d2a2b64736f6c634300080700332aa0a8c6e2b783790035e4d063b4d813bc0c0a2c1df8482fa306d4e1f77d0730ace9a02b0e93e456e2528d87e708a4f5bcab07a6ae659f3fbfa93a83930d58e9975722',
  tx: {
    nonce: '0x2',
    chainId: '3',
    gas: '0x312ed',
    gasPrice: '0x73a20d11',
    data: '0x60806040526040518060400160405280600b81526020017f48656c6c6f20576f726c640000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610105565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b6000600282049050600182168061011d57607f821691505b6020821081141561013157610130610137565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610232806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d705a4b514610030575b600080fd5b61003861004e565b6040516100459190610119565b60405180910390f35b60606000805461005d9061018a565b80601f01602080910402602001604051908101604052809291908181526020018280546100899061018a565b80156100d65780601f106100ab576101008083540402835291602001916100d6565b820191906000526020600020905b8154815290600101906020018083116100b957829003601f168201915b5050505050905090565b60006100eb8261013b565b6100f58185610146565b9350610105818560208601610157565b61010e816101eb565b840191505092915050565b6000602082019050818103600083015261013381846100e0565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561017557808201518184015260208101905061015a565b83811115610184576000848401525b50505050565b600060028204905060018216806101a257607f821691505b602082108114156101b6576101b56101bc565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f830116905091905056fea2646970667358221220316cca1abf85d891616645d88d5a234baaa634f4345b2c4d557a0761bf6d2a2b64736f6c63430008070033',
    from: '0x1a4d93e78905a7f4fc7a2b8ca1b789aaaa10f102'
  }
}

無事rawデータが取得出来ました。

署名済みトランザクションをテストネットにデプロイする

トランザクションに署名しrawデータが取れたので、Ethereumテストネットワークにデプロイしてみます。
使用するのはweb3.eth.sendSignedTransaction()メソッドです。rawデータを引数で渡して実行すれば良いようです。

sample.js
// 省略

// rawデータを変数に格納
let raw = '0xf903f9028473a20d11830312ed8080b903a760806040526040518060400160405280600b81526020017f48656c6c6f20576f726c640000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610105565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b6000600282049050600182168061011d57607f821691505b6020821081141561013157610130610137565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b610232806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d705a4b514610030575b600080fd5b61003861004e565b6040516100459190610119565b60405180910390f35b60606000805461005d9061018a565b80601f01602080910402602001604051908101604052809291908181526020018280546100899061018a565b80156100d65780601f106100ab576101008083540402835291602001916100d6565b820191906000526020600020905b8154815290600101906020018083116100b957829003601f168201915b5050505050905090565b60006100eb8261013b565b6100f58185610146565b9350610105818560208601610157565b61010e816101eb565b840191505092915050565b6000602082019050818103600083015261013381846100e0565b905092915050565b600081519050919050565b600082825260208201905092915050565b60005b8381101561017557808201518184015260208101905061015a565b83811115610184576000848401525b50505050565b600060028204905060018216806101a257607f821691505b602082108114156101b6576101b56101bc565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f830116905091905056fea2646970667358221220316cca1abf85d891616645d88d5a234baaa634f4345b2c4d557a0761bf6d2a2b64736f6c634300080700332aa0a8c6e2b783790035e4d063b4d813bc0c0a2c1df8482fa306d4e1f77d0730ace9a02b0e93e456e2528d87e708a4f5bcab07a6ae659f3fbfa93a83930d58e9975722';

web3.eth.sendSignedTransaction(raw).on('receipt', console.log);

それでは実行します。
テストネットワークはテスト版ですが実際のP2Pネットワークです。
マイニングしてもらうのに少し時間がかかります。

$ node sample.js
>
{
  blockHash: '0xfdcb5227f3c97c6ab0c5ed1e98a55629c1586eec839abdd8a74eb8bf0191cdab',
  blockNumber: 10932762,
  contractAddress: '0xD9b40b4712c79B8e7d2F11Ec6737F1A398d81B28',
  cumulativeGasUsed: 3962943,
  effectiveGasPrice: '0x73a20d11',
  from: '0x1a4d93e78905a7f4fc7a2b8ca1b789aaaa10f102',
  gasUsed: 201453,
  logs: [],
  logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
  status: true,
  to: null,
  transactionHash: '0xf82caf39cbca351edef64e8eef1cef20fd2135c88a6b809d639237ad2bddb983',
  transactionIndex: 30,
  type: '0x0'
}

約10~20秒くらい待つとレスポンスがありました。
トランザクションハッシュの値とコントラクトアドレスが返ってきたのでデプロイ出来たようです。

スマートコントラクトにリクエストしてみる

早速コントラクトアカウントにcallしてコントラクトの値を取得できるか試してみます。
callメソッドのtoとdataパラメーターには上で取得した値を格納します。
to: contractAddress
data: signature

sample.js
//省略

// コントラクトアドレスとsignatureを変数に格納
let recipient_address = '0xD9b40b4712c79B8e7d2F11Ec6737F1A398d81B28';
let signature = '0xd705a4b5';

// アカウントを取得し、その後callメソッドを実行する処理
web3.eth.getAccounts((error, result) => {
  let sender_address = result[0]
  let call_data = {
    'from': sender_address,
    'to': recipient_address,
    'data': signature
  }

  web3.eth.call(call_data, (error, response)=>{
    console.log('response: ', response);
    provider.engine.stop();
  })
})

実行してみます。

$ node sample.js
> response:  0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c64000000000000000000000000000000000000000000

Hex表記ですが、Hello Worldが帰ってきました。
成功です。無事スマートコントラクトはデプロイ出来たようです。
あー、長かった。

おわりに

HDWalletの仕組みをちゃんと理解できていないのでこの方法が正しいのか分かりませんがどうにかデプロイすることが出来たと思います。
もし同じようにハマっている人がいれば参考になると幸いです。

参考サイト

【ビットコイン】ウォレットの概要とHDウォレットの仕組み
HDWalletの仕組みについてすごく参考になりました。ありがとうございます。


Restore private key from it's seed or mnemonic phrase
HDWalletの具体的な実装内容について参考になりました。

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4