Solidityでカイジのチンチロゲームを作った話

#成果物
とりあえず、LightSailで簡易公開。
Metamask入れて、Ropstenネットワークを選択して遊べます！！
メインネットでは遊べないです。

今は閉鎖しちゃいました！
~~http://13.113.175.211:3000/~~

<img width="400px" alt="スクリーンショット 2019-08-29 10.26.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148318/747b935b-e0b3-efa7-80c7-2c9ad79f0c8b.png">


#概要
カイジのチンチロを作りたかった・・・
Ethereumを使って。
完成形はユーザ登録とユーザの持ち金(GTIP)の量を管理するにとどまりました。

Rails側で乱数生成、勝敗の決定をして
**勝敗に応じてコントラクトの関数を叩いてウォレットに紐づくユーザのGTIPを動かそう！**
という取り組みになりました。

#技術レベル
Rails（勉強したて）
Solidity(ゾンビやったくらい)
Html/CSS(ぱんぴぃ)

決して綺麗なコードではないですが、勉強の軌跡として残しておきます！

#Webソース
https://github.com/tokai-son/Gambreum_web
ちょっと整理できていないです。JSファイルに細かく分離したかった…

#Solidityの部分

```GambreumPlayer.sol 
pragma solidity ^0.4.24;

import "./GambreumTip.sol";
import "../node_modules/zeppelin-solidity/contracts/token/ERC20/StandardToken.sol";
import "../node_modules/zeppelin-solidity/contracts/ownership/Ownable.sol";

contract GambreumPlayer is GambreumTip {

	struct PlayerInfo {
		string username;
		uint winrate;
		bool locked;
	}

	event PlayerCreated(string username);

	mapping (address => PlayerInfo) public addressToPlayerInfo;
	mapping (address => uint) public addressToBalance;

	function createPlayer(string _name) public {
		emit PlayerCreated(_name);
		require(keccak256(_name) != keccak256("username"));
		string memory player_name = addressToPlayerInfo[msg.sender].username;
		require(bytes(player_name).length == 0);
		addressToPlayerInfo[msg.sender] = PlayerInfo(_name, 0, false);
		balances[msg.sender] += 100; //初期配布分の100GTIP
	}

	function viewPlayerInfo() public view returns (string username, uint winrate, bool locked) {
		PlayerInfo memory player_info = addressToPlayerInfo[msg.sender];
		return (player_info.username, player_info.winrate, player_info.locked);
	}

	function publishTokenToPlayer(uint value, address to) public onlyOwner {
		balances[to] += value;
	}

	function returnToken(uint value, address player_address) public onlyOwner {
		balances[player_address] -= value;
	}
}
```

最初やろうとしていたことからだいぶ脱線しましたw
あと、後から気づいたんですが「ERC20じゃなくてもよくね？」

が、truffleを用いた一連の開発手順は勉強になりました。
「書く、デバック(今度これ読もうかな)、コンパイル、テスト、デプロイ」

今回、こんな感じで作りました。
クライアント -> Local Rails -> infura -> Contract in Ropsten

一番苦労した事は、トランザクションをサーバ側から叩くことです。
サーバからコントラクトのオーナーアカウントで
onlyOwner属性の関数を叩く必要があります。

クライアントからコントラクト叩くのは
Web3とMetamaskちゃんが頑張ってくれました。
がサーバからトランザクションをAPI経由で叩くには…
**APIに投げる「コントラクトオーナーの署名つきのRaw Transactionを作成する必要がある」**

コードを見るとなーんだ、と思うのですが
ここにたどり着くまでに紆余曲折。

**ポイントはhex_dataの部分、Ethereum ABIの形式に合わせる必要があります。**
簡単に言うと、
１　関数名(引数の型スペース無、名前は省略)を256でハッシュし
　　その最初の４Byteだけ使います。
２　引数として渡すデータを１６進数で渡す。ただし、32Byteの長さになるように０でパッティング
（３　引数が可変長なら付加情報が必要です。詳しくは、[ここを見て!](https://solidity.readthedocs.io/en/develop/abi-spec.html)）
最後に、こいつらを全部繋げて先頭に”０x”をつければ完成。

```RawTransactionを作っています。(Ethereum.rb使いました)

  def exeRewardProc(amount, user_wallet, is_earn)
   # Create instanse from my private key whose is Gambrerum Owner.
   key = Eth::Key.new priv: "<秘密鍵 MetamaskからExportした>"

   # Get transaction count
   response = getMyTransactionCount()
   string_response = response.body
   json_response = JSON.parse(string_response)
   my_nonce = json_response["result"]

   # Create hex_data as a payload on this tx.
   if(is_earn == true)
     selector = Digest::SHA3.hexdigest("publishTokenToPlayer(uint256,address)", 256).slice(0..7) # 最初の4Byteを使う
   else
     selector = Digest::SHA3.hexdigest("returnToken(uint256,address)", 256).slice(0..7) # 最初の4Byteを使う
   end
   amount = amount.to_s(16) # 0xへ
   arg1_uint = amount.rjust(64, "0") #FIX: 10進数だから16に直す！
   arg2_address = user_wallet.slice(2..-1)
   arg2_address = arg2_address.rjust(64, "0")
   hex_data = "0x" + selector + arg1_uint + arg2_address

   tx = Eth::Tx.new({
     data: hex_data,
     gas_limit: DEFAULT_GAS_LIMIT,
     gas_price: DEFAULT_GAS_PRICE,
     nonce: my_nonce.hex,
     from: "0x6CaFf8d3958dB8EF53b1Abbe10622c26DBFa4778",
     to: CONTRACT_ADDRESS,
     value: 0
   })

   # Sign this transaction by the key
   tx.sign key

   transaction_response = sendMyRawTransaction(tx.hex)
   transaction_response_string = transaction_response.body
   logger.debug(transaction_response_string)
   transaction_response_json = JSON.parse(transaction_response_string)
   return transaction_response_json["result"]
 end
```

以下で、TransactionをAPI経由で送付しています。

```TransactionをAPI使って送付するところ
  def sendMyRawTransaction(signed_pay_load)
   uri = URI.parse(ROPSTEN_URL)
   request = Net::HTTP::Post.new(uri)
   request.content_type = "application/json"
   request.body = JSON.dump({
     "jsonrpc" => "2.0",
     "method" => "eth_sendRawTransaction",
     "params" => [
       signed_pay_load
     ],
     "id" => 1
   })

   req_options = {
     use_ssl: uri.scheme == "https",
   }

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

   return response
 end
```

なんだかんだ、サイコロ回して勝ち負け決まって
トランザクションが発行されTIPが動くとやったー！ってなりました。

ただ、まだまだ改善の余地ありです。
１　TIPを動かすトランザクションが承認されるまで待たんといかん
　　当たり前ですが、実はもうTIPないのに次の勝負！
　　なんてこともできちゃいます・・・
　　**解決策：トランザクションを発行されたらアカウントをロック
　　　　　　サーバ側でDBを別途用意し、トランザクションを監視**

２　マイナスになるくらい負けると偉いことになる
　　TIPの管理を下記のソースないのbalanceという変数で管理
　　zeppelin-solidity/contracts/token/ERC20/BasicToken.sol
　
　　**〜　uint256...これがマイナスに振れた時、世界は反転する…　〜**

　　**解決策：掛け金がマイナスにならない健全なギャンブルをして頂く。　
　　　　　　　　and
　　　　　　SafeMathを使う(ゾンビで習ってた…)**

３　Metamaskでテストネット以外を選択している場合を検知する
　　ここは後から気づきました…
　　クライアント側で「web3.eth.net.getNetworkType」を呼んで判定させれば良さそう！

```BasicToken.sol

pragma solidity ^0.4.24;


import "./ERC20Basic.sol";
import "../../math/SafeMath.sol";


/**
* @title Basic token
* @dev Basic version of StandardToken, with no allowances.
*/
contract BasicToken is ERC20Basic {
 using SafeMath for uint256;

 mapping(address => uint256) internal balances;

~ snip ~
```

理想通りにはなりませんでしたが、一応遊べる形として完成させられました。
やはり、１から手を動かして作るのは大変ですけど、楽しいですね。

今後は…
・Rspec使ってみる
・HTTPSでアクセスできるようにする
・本環境をDockerのimageにしてみる
この辺りをやってみたいと思います。