🐷

ユーザー毎のプール作成・送金処理を Solidity で書いて Web アプリと繋げる

2022/05/11に公開

はじめに

どうも、 Yuuuu_改めちゃりです。日本円ハッカソンをきっかけにはじめて Solidity にふれて、JPYC を使ったライブ配信連動型投げ銭サービス「推しサポ」を @Anman くんと開発しました。幸運にも賞を頂いたので、開発についてまとめていきたいと思います。

ソースコードはこちら
前回の記事はこちら

今回は「推しサポ」の送金処理部分に関して解説していきたいと思います。

前提

視聴者のウォレットからの配信者のウォレットへの送金の流れは以下です。
(視聴者からの送金)

  1. 各ユーザー(視聴者)毎に専用プールを作成
  2. ユーザーウォレットから視聴者プールへお金を送金
  3. 各ユーザー(配信者)毎に専用プールを作成
  4. 視聴者プールから配信者プールへお金を送金
  5. 配信者プールから配信者のウォレットへお金を送金

ここで疑問に思った方がいるかもしれません。プールとは??なぜ直接視聴者のウォレットから配信者のウォレットに送金しないの??

プールとは

プールとは暗号資産を保存しておく場所です。ユーザー毎に個別のプールがないと全てのユーザーの JPYC が同一のプールから出金 / 入金されてしまいます。(日本円ハッカソン入門ラボ第 4 回で紹介された貯金箱の例参照)このままだとユーザーAとユーザーBのJPYCが一緒に保存されてしまうので、利用者の資産保護やセキュリティの観点からプールを各ユーザー毎に設けることは絶対必要な仕組みになります。プール作成はどうやら solidity 開発においては一般的らしく、お手本となるソースコートも沢山出てきます。僕らはこの方のソースコードを参考にしました。

なので、少し遠回りに見えるかもしれませんが、視聴者、配信者毎にプールを作る方向で開発しています。


図にするとこんなイメージ

実装

プール作成 (Solidity)

インターフェースを準備。プールアドレスを取得できる関数を用意します。プールは (視聴者のウォレットアドレス) ⇒ (プールアドレス) を対応させる mapping形式で保存します。

ThrowMoney.sol
    interface IThrowMoneyFactory {
    function getPool(address sender) external view returns(address);
    function newThrowMoneyPool() external returns(address);
}
 
contract ThrowMoneyFactory is IThrowMoneyFactory {
    mapping(address => address) pools;
    address withdrawCAAddress;
    constructor(address _withdrawCAAddress){
        withdrawCAAddress = _withdrawCAAddress;
    }

ユーザーのウォレットアドレスを受け取った際に、当ユーザーのプール作成の有無を確認できるよう、 Sender_Address(視聴者)と Pool_Address をイベントで定義します

ThrowMoney.sol
    event ErrorLog(string __error_message);
    event PoolCreated(address indexed __sender_address, address __pool_address);
    function getPool(address _sender) public view returns(address) {
        return pools[_sender];
    }

新たにプールを作成する関数。プール作成済みのウォレットアドレスを受け取った際にはエラーが返されるようになっています。

ThrowMoney.sol
    function newThrowMoneyPool() public returns(address) {
        require(address(pools[msg.sender]) == address(0), "Pool already created for this wallet address");
        // 新しいプールを作成
        ThrowMoneyPool pool = new ThrowMoneyPool(msg.sender, address(this), withdrawCAAddress);
        emit PoolCreated(msg.sender, address(pool));
        pools[msg.sender] = address(pool);
        return pools[msg.sender];
    }
}

プールとWebアプリとの接続(Ether.js)

Solidity で定義したイベントをfilterで参照できるようにします。filter 関数で視聴者のウォレットアドレスを引数に入れて、ウォレットアドレスに対応するプールアドレスを取得します。同時にPool に入っている残高(pool_balance)を定義し、HTML側に表示できるようにします。

index.js
async function createPool(){
    filter = throwMoneyFactoryContract.filters.PoolCreated(useraddress, null);
    throwMoneyFactoryContract.on(filter, (_signer_address, _pool_address) => {
        signerPool = _pool_address;

        _filter = JPYCContract.filters.Transfer(signerPool, null, null);
        JPYCContract.on(_filter, async () => {
            pool_balance = Math.round(ethers.utils.formatEther(await JPYCContract.balanceOf(signerPool)));
            document.getElementById("pool_balance").innerHTML = pool_balance + " JPYC";
        });
}

もし、プール未作成のユーザーである場合(POOL Address === Null)、 新しいプールを作成する Solidity 側の関数(newThrowMoneyPool)が呼ばれます。

index.js
 _filter = JPYCContract.filters.Transfer(null, signerPool, null);
        JPYCContract.on(_filter, async () => {
            pool_balance = Math.round(ethers.utils.formatEther(await JPYCContract.balanceOf(signerPool)));
            document.getElementById("pool_balance").innerHTML = pool_balance + " JPYC";
        });
    });
    throwMoneyFactoryContract.newThrowMoneyPool();

配信者への送金 (Solidity + Ether.js)

  • ThrowmoneyPool:視聴者が記入した値分の JPYC をプールから配信者へ送金する
    スマートコントラクトに送金を代行してもらう形になるので、Approve + TransferFrom を組み合わせて送金処理を実装しましょう。(日本円ハッカソン入門ラボ第4回参照)
ThrowMoney.sol
        try jpyc.approve(address(this), _amount) {
        } catch Error(string memory reason) {
            emit ErrorLog(reason);
        }

        senderPoolAddress = throwMoneyFactory.getPool(_reciveAddr);
        senderPool = IThrowMoneyPool(senderPoolAddress);

        try jpyc.transferFrom(address(this), senderPoolAddress, _amount) {
            senderPool.emitMoneySent(owner, _message, _senderAlias, _amount);
        } catch Error(string memory reason) {
            emit ErrorLog(reason);
        }

Ether.js 側はシンプルです。solidity側の関数を呼び出して、適切な引数を渡してあげましょう。

index.js
    PoolContract.sendJpyc(streamerAddress, message, nickname, amountWei, options).catch((error) => {
        errorMessage = error;   
    });

気を付けるポイント

  • Filter で参照するイベント情報は可能な限り絞りましょう。絞らないと参照するデータ量が多くなり、クライアントに余計な負荷がかかる可能性があります。
  • Wei なのか、 Ether (今回は JPYC) なのか、Javascript においても、Solidity においても関数を書く時の数字の単位は注意しましょう。JPYC のような ERC20 に沿ったトークンでは、内部保持の数値金額から桁数処理を行った上で、○○ JPYC といった金額を表示しています。 例えば、表示金額である 1 JPYC は内部保持の数値金額では 1 * 10 ^ 18 の整数型で表されます。 扱っている金額は表示金額なのか、内部保持の数値金額なのか気を付けましょう。
  • 動かなかったらまずは Remix に戻ってスマコンを動かしてみましょう。動いたら悪さしているのは ether.js です。Walletのガス代が足りない、ネットワークが違う、スマコン側にJPYC入れてない、などコード以外の設定で自分は沢山バグを起こしたので、コード以外も確認を!

次回は配信者側アプリについて Anmanくんが記事を書いてくれます

Discussion