Help us understand the problem. What is going on with this article?

ブロックチェーンエンジニア Lv. 3 —交換所を実装する(1)—

More than 1 year has passed since last update.

前回投稿

https://qiita.com/kackytw/items/76f04b7cf1ca0ee97f02

今回の目標

前回までで、確率によってランダムな報酬を得ることができるようになりました。
今回はこの報酬を交換できるようにしたいと思います。この実装を通じて

  • コントラクト間の連携
  • コントラクトの継承
  • function修飾子

などを学んでいきたいと思います。

交換所の作り方としては、Tokenそのものに交換する機能を備えるやり方と、もう一つ別のコントラクトが仲介して、交換をするやり方が考えられますが、今回は後者のやり方で実装します。

この流れを図に表すと次のような形となります。

Contract (1).png

で、交換してそれをどないするの?というツッコミが入りそうですが、それは皆さんの想像力にお任せしたいと思います。考えればいろいろアイデアが浮かんでくると思います。

例えば、Tokenをトレーディングカードだと見立てれば、

  • ブースターパックを買う→すなわちガチャを引く
  • それらをいろいろ交換してデッキを組む
  • 他のプレーヤーと戦う

という一連の流れの基礎ができているように思えませんか?

愚直に実装

ひとまず、それぞれのTokenのコントラクトを愚直に実装してみましょう。

contract TradeGachaCoin {
    /* Public variables of the token */
    string public name;
    string public symbol;
    uint8 public decimals;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;

    function TradeGachaCoin(uint256 _supply) {
        /* if supply not given then generate 1 million of the smallest unit of the token */
        if (_supply == 0) _supply = 1000000;

        /* Unless you add other functions these variables will never change */
        balanceOf[msg.sender] = _supply;
        name = "Trade Gacha Coin";     
        symbol = "TGC";

        /* If you want a divisible token then add the amount of decimals the base unit has  */
        decimals = 1;
    }

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* Send coins */
    function transfer(address _to, uint256 _value) public {
        /* if the sender doenst have enough balance then stop */
        assert (balanceOf[msg.sender] >= _value);
        assert (balanceOf[_to] + _value >= balanceOf[_to]);

        /* Add and subtract new balances */
        balanceOf[_to] += _value;
        balanceOf[msg.sender] -= _value;

        /* Notifiy anyone listening that this transfer took place */
        Transfer(msg.sender, _to, _value);
    }

    function calcSum(uint8[3] array) pure private returns (uint sum) {
        sum = 0;
        for (uint i = 0; i<array.length; i++) {
            sum += array[i];
        }
    }

    function sample(uint8[3] array) private returns (uint index) {
        uint r = block.timestamp % calcSum(array);
        uint s = 0;
        for (uint i = 0; i < array.length; i++) {
            s += array[i];
            if (s > r) {
                break;
            }
            index++;
        }
    }

    function gacha() public {
        var lotteryWeight = [70, 20, 10];
        uint fee = 100;
        /* 料金を引き算する */
        assert(balanceOf[msg.sender] >= fee);
        balanceOf[msg.sender] -= fee;
        /* ガチャの結果に応じて報酬ゲット */
        uint index = sample(lotteryWeight);

        /* (TODO)報酬 */        
    }

    function pay() public payable {}    
}

contract NormalCoin {
    /* Public variables of the token */
    string public name;
    string public symbol;
    uint8 public decimals;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;

    function NormalCoin(uint256 _supply) {
        /* if supply not given then generate 1 million of the smallest unit of the token */
        if (_supply == 0) _supply = 1000000;

        /* Unless you add other functions these variables will never change */
        balanceOf[msg.sender] = _supply;
        name = "Normal Coin";     
        symbol = "N";

        /* If you want a divisible token then add the amount of decimals the base unit has  */
        decimals = 1;
    }

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* Send coins */
    function transfer(address _to, uint256 _value) public {
        /* if the sender doenst have enough balance then stop */
        assert (balanceOf[msg.sender] >= _value);
        assert (balanceOf[_to] + _value >= balanceOf[_to]);

        /* Add and subtract new balances */
        balanceOf[_to] += _value;
        balanceOf[msg.sender] -= _value;

        /* Notifiy anyone listening that this transfer took place */
        Transfer(msg.sender, _to, _value);
    }
}

contract RareCoin {
    /* Public variables of the token */
    string public name;
    string public symbol;
    uint8 public decimals;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;

    function RareCoin(uint256 _supply) {
        /* if supply not given then generate 1 million of the smallest unit of the token */
        if (_supply == 0) _supply = 1000000;

        /* Unless you add other functions these variables will never change */
        balanceOf[msg.sender] = _supply;
        name = "Rare Coin";     
        symbol = "R";

        /* If you want a divisible token then add the amount of decimals the base unit has  */
        decimals = 1;
    }

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* Send coins */
    function transfer(address _to, uint256 _value) public {
        /* if the sender doenst have enough balance then stop */
        assert (balanceOf[msg.sender] >= _value);
        assert (balanceOf[_to] + _value >= balanceOf[_to]);

        /* Add and subtract new balances */
        balanceOf[_to] += _value;
        balanceOf[msg.sender] -= _value;

        /* Notifiy anyone listening that this transfer took place */
        Transfer(msg.sender, _to, _value);
    }
}

contract SuperRareCoin {
    /* Public variables of the token */
    string public name;
    string public symbol;
    uint8 public decimals;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;

    function RareCoin(uint256 _supply) {
        /* if supply not given then generate 1 million of the smallest unit of the token */
        if (_supply == 0) _supply = 1000000;

        /* Unless you add other functions these variables will never change */
        balanceOf[msg.sender] = _supply;
        name = "Super Rare Coin";     
        symbol = "SR";

        /* If you want a divisible token then add the amount of decimals the base unit has  */
        decimals = 1;
    }

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* Send coins */
    function transfer(address _to, uint256 _value) public {
        /* if the sender doenst have enough balance then stop */
        assert (balanceOf[msg.sender] >= _value);
        assert (balanceOf[_to] + _value >= balanceOf[_to]);

        /* Add and subtract new balances */
        balanceOf[_to] += _value;
        balanceOf[msg.sender] -= _value;

        /* Notifiy anyone listening that this transfer took place */
        Transfer(msg.sender, _to, _value);
    }
}

こんな感じで一応それぞれのコントラクトを実装できます。しかし、これはプログラムとして美しくないですよね。ほとんど同じコードをコピペで実装して無駄が多いです。トークンとして共通となる部分はなるべくコードも共有したいものです。

コントラクトの継承

これを実現するためにコントラクトの継承が使えます。継承はJavaやC#のクラス継承と同類のものです。Syntaxとしては

contract TradeGachaCoin is CryptoToken 

といった感じで、isという演算子を使います。

当然ですがコンストラクタや、functionも継承されますし、overrideすることもできます。
コンストラクタを継承したい場合は、

function TradeGachaCoin () public CryptoToken("Trade Gacha Coin", "TGC", 100000000, 1) {
   /* ... */
}

といったようにブロックの直前に親のコンストラクタ呼び出しを記述します。functionをoverrideする場合は、そのまま同じ名前のfunctionを定義するだけです。

function hoge () public {
   super.hoge(); /* 親のfunction */
}

というように親コントラクトのfunctionを呼び出すこともできます。

function修飾子

今までも、さらっとfunctionにpublicなど、修飾子をつけてきましたが、ここで少し解説しておきます。

コントラクトにあるfunctionのうち、publicとしているものが、ユーザーが実行できる機能となります。ユーザーに実行させたくないfunctionにはprivateあるいはinternalをつけます。何も書かないとpublicとして扱われます。うっかり何も書かないでおくとユーザーに実行されてセキュリティの穴になる可能性がありますのでユーザーに実行させないfunctionには必ずprivateあるいはinternalをつけるようにしましょう。

その他にもpure、constantといった修飾子があります。詳しくはこちらで解説していますのでご覧ください。

https://qiita.com/kackytw/items/1bc6f86e54794fda3011

プログラムを書き直す

以上を踏まえてプログラムを書き直してみましょう。

contract CryptoToken {
    /* Public variables of the token */
    string public name;
    string public symbol;
    uint8 public decimals;

    /* This creates an array with all balances */
    mapping (address => uint256) public balanceOf;
    /* Initializes contract with initial supply tokens to the creator of the contract */
    function CryptoToken(string _name, string _symbol, uint256 _supply, uint8 _decimals) public {
        /* if supply not given then generate 1 million of the smallest unit of the token */
        if (_supply == 0) _supply = 1000000;

        /* Unless you add other functions these variables will never change */
        balanceOf[msg.sender] = _supply;
        name = _name;
        symbol = _symbol;

        /* If you want a divisible token then add the amount of decimals the base unit has  */
        decimals = _decimals;
    }

    /* This generates a public event on the blockchain that will notify clients */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /* Send coins */
    function transfer(address _to, uint256 _value) public {
        /* if the sender doenst have enough balance then stop */
        assert (balanceOf[msg.sender] >= _value);
        assert (balanceOf[_to] + _value >= balanceOf[_to]);

        /* Add and subtract new balances */
        balanceOf[_to] += _value;
        balanceOf[msg.sender] -= _value;

        /* Notifiy anyone listening that this transfer took place */
        Transfer(msg.sender, _to, _value);
    }
}

contract TradeGachaCoin is CryptoToken { 
    function TradeGachaCoin() public
      CryptoToken("Trade Gacha Coin", "TGC", 100000000, 1) {
    }

    function calcSum(uint8[3] array) pure private returns (uint sum) {
        sum = 0;
        for (uint i = 0; i<array.length; i++) {
            sum += array[i];
        }
    }

    function sample(uint8[3] array) private returns (uint index) {
        uint r = block.timestamp % calcSum(array);
        uint s = 0;
        for (uint i = 0; i < array.length; i++) {
            s += array[i];
            if (s > r) {
                break;
            }
            index++;
        }
    }

    function gacha() public {
        var lotteryWeight = [70, 20, 10];
        uint fee = 100;
        /* 料金を引き算する */
        assert(balanceOf[msg.sender] >= fee);
        balanceOf[msg.sender] -= fee;
        /* ガチャの結果に応じて報酬ゲット */
        uint index = sample(lotteryWeight);

        /* (TODO)報酬 */        
    }

    function pay() public payable {}    
}

contract NormalCoin is CryptoToken {
    function NormalCoin() public
      CryptoToken("Normal Coin", "N", 100000000, 1) {
    }
}

contract RareCoin is CryptoToken {
    function RareCoin() public
      CryptoToken("Rare Coin", "R", 100000000, 1) {
    }
}

contract SuperRareCoin is CryptoToken {
    function SuperRareCoin() public
      CryptoToken("Super Rare Coin", "SR", 100000000, 1) {
    }
}

こんな感じになりました。余計な記述がなくなってすっきりしましたね。

コントラクト間を連携する

ガチャを引いてそれに応じた報酬を送るためには、TradeGachaCoinから、NormalCoin、RareCoin、SuperRareCoinのtransferメソッドを呼び出す必要があります。
これには次の2通りのやり方があります。

  • TradeGachaCoinコンストラクタにパラメータとしてコントラクトアドレスを渡して、NormalCoin、RareCoin、SuperRareCoinとしてインスタンス化する
  • TradeGachaCoinコントラクトからNormalCoin、RareCoin、SuperRareCoinをデプロイする

前者のやり方ですが、address型はコントラクト型にキャストすることができるので、

contract TradeGachaCoin is CryptoToken {
    NormalCoin private normalCoin;
    RareCoin private rareCoin;
    SuperRareCoin private superRareCoin;

    function TradeGachaCoin(address addrNormal, address addrRare, address addrSRare) public
      CryptoToken("Trade Gacha Coin", "TGC", 100000000, 1) {
          normalCoin = NormalCoin(addrNormal);
          rareCoin = RareCoin(addrRare);
          superRareCoin = SuperRareCoin(addrSRare);
    }

このように書くことで実現できます。

後者のやり方は

contract TradeGachaCoin is CryptoToken {
    NormalCoin private normalCoin;
    RareCoin private rareCoin;
    SuperRareCoin private superRareCoin;

    function TradeGachaCoin() public
      CryptoToken("Trade Gacha Coin", "TGC", 100000000, 1) {
          normalCoin = new NormalCoin();
          rareCoin = new RareCoin();
          superRareCoin = SuperRareCoin();
    }

このように書くことで実現できます。今回のケースでは後者の方がパラメータが少なくてすみますし、デプロイも1回ですむので楽そうです。

しかし、デプロイしてみると

スクリーンショット 2018-01-17 19.31.26.png

TradeGachaCoinコントラクトはbrowser-solidity上に表示されますが、そこからさらにデプロイされたNormalCoinやRareCoinは画面に出てきません。こうなると、NormalCoinやRareCoinをいくつ持っているのか確認できませんね。これは不便です。

どうすればいいでしょう。NormalCoinやRareCoinはTradeGachaCoinから見られる形なので、TradeGachaCoinにNormalCoinやRareCoinの残高を返すpublicメソッドを足せば解決できそうです。

    function normalCoinBalance() public constant returns (uint256) {
        return normalCoin.balanceOf(msg.sender);
    }

    function rareCoinBalance() public constant returns (uint256) {
        return rareCoin.balanceOf(msg.sender);
    }

    function superRareCoinBalance() public constant returns (uint256) {
        return superRareCoin.balanceOf(msg.sender);
    }

こんな形でそれぞれのコインの保有残高を確認するメソッドができます。

報酬を配る

ここまででだいぶ調いました。あとは、gachaメソッドの結果に応じて、それぞれのコインを送金するところを実装します。

    function gacha() public {
        var lotteryWeight = [70, 20, 10];
        uint fee = 100;
        /* 料金を引き算する */
        assert(balanceOf[msg.sender] >= fee);
        balanceOf[msg.sender] -= fee;
        /* ガチャの結果に応じて報酬ゲット */
        uint index = sample(lotteryWeight);

        /* 報酬 */
        if (index == 0) {
            normalCoin.transfer(msg.sender, 1);
        } else if (index == 1) {
            rareCoin.transfer(msg.sender, 1);
        } else {
            superRareCoin.transfer(msg.sender, 1);
        }
    }

ガチャの結果を表示させる

ここまででガチャの機能は実装できました。しかし、このガチャは静かに報酬を配るので、Normalが当たったのか、Rareが当たったのか、残高をくまなく調べないとわからないのでこれも不便ですよね。プログラマーとしては何が当たったかをログとして表示させたいです。

これにはeventという仕組みを使うと実現できます。
eventは処理の途中で呼び出せる特殊なメソッドで、引数は自由に指定することができます。そして、トランザクションが完了するとともにノードにブロードキャストされます。こちらはbrowser-solidity上でも確認できるのでデバッグログ的な使い方ができます。

※ただし、トランザクションが失敗した場合は一切出ないので完全にデバッグログと同じにはならないんですよね(´・ω・`)

    event GachaResult(string str);

    function gacha() public {
        var lotteryWeight = [70, 20, 10];
        uint fee = 100;
        /* 料金を引き算する */
        assert(balanceOf[msg.sender] >= fee);
        balanceOf[msg.sender] -= fee;
        /* ガチャの結果に応じて報酬ゲット */
        uint index = sample(lotteryWeight);

        /* 報酬 */
        if (index == 0) {
            GachaResult("normal");
            normalCoin.transfer(msg.sender, 1);
        } else if (index == 1) {
            GachaResult("rare");
            rareCoin.transfer(msg.sender, 1);
        } else {
            GachaResult("super rare");
            superRareCoin.transfer(msg.sender, 1);
        }
    }

動作確認

ガチャの機能ができたので動作確認します。当たれ当たれ~
スクリーンショット 2017-12-08 15.30.51.png
当たりました、normalです(´・ω・`)

スクリーンショット 2017-12-08 15.33.00.png

残高を確認するとNormalCoinだけが1増えていることが確認できます。

まとめ

今回は交換所の実装を通じてコントラクトの継承の仕方、複数のコントラクトにまたがる処理の書き方を学ぶことができました。ちょっと今回は機能が大きいので前半後半に分けさせていただきました。次回はToken同士を交換する処理を書きたいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away