LoginSignup
8
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-29

成果物

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

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

スクリーンショット 2019-08-29 10.26.48.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の形式に合わせる必要があります。
簡単に言うと、
1 関数名(引数の型スペース無、名前は省略)を256でハッシュし
  その最初の4Byteだけ使います。
2 引数として渡すデータを16進数で渡す。ただし、32Byteの長さになるように0でパッティング
(3 引数が可変長なら付加情報が必要です。詳しくは、ここを見て!
最後に、こいつらを全部繋げて先頭に”0x”をつければ完成。

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経由で送付しています。

  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が動くとやったー!ってなりました。

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

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

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

3 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 ~

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

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

8
2
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
8
2