2017年5月10日水曜日

XRP用のウォレットを自作する ー 送金処理と全体のまとめ

準備編でも述べたように、送金は4つの段階で構成されています。

第1段階:Prepare(準備)
第2段階:Sign(シークレットキーによる署名)
第3段階:Submit(送信)
第4段階:Velify(検証)

それぞれの段階を実行するコードを書いて行く前に、送金に必要な情報を入力するためのインターフェイスと送金結果を表示する場所を作っておきましょう。

0.インターフェイスの実装


相手先アドレスの入力欄、送金額、自分のアドレス入力欄、シークレットキーの入力欄、「送金実行」ボタンを表示します。

「送金実行」ボタンが押されると、remitXrp() 関数が呼び出されて送金が実行されます。

<body>~</body>の残高確認コードの上に追加します。残高確認は表示が長くなることがあるので、使いやすさを考えて送金ボタンを上に持ってきました。

<!-- XRPを送金する -->
<form>
    <input type="button" class="button" value="XRPを送金する">
    <div id="remit_form">
        <table>
            <tr><th>送金先アドレス</th><td><input type="text" class="text" id="dest_address"></td></tr>
            <tr><th>送金額(XRP)</th><td><input type="text" class="text" id="amount"></td></tr>
            <tr><th>自分のアドレス</th><td><input type="text" class="text" id="source_address"></td></tr>
            <tr><th>シークレット</th><td><input type="text" class=text id="secret"></td></tr>
        </table>
        <input type="button" value="送金実行" onClick="remitXrp()">
    </div>     
</form>
<div id="show_result"></div>


ブラウザで表示すると、とこんな風に見えると思います。



 1.Prepare(送金準備)


送金準備には preparePayment() というメソッドを使います。

このメソッドに引数として、送金元アドレスおよび送金先アドレス、送金量、通貨の種類、counterpartyアドレスなどをプロパティとして持つ paymentオブジェクト を渡してやると、オブジェクトが返されます。詳しくはRippleの公式サイトにあるサンプルコードを確認してください。

このウォレットではXRPの送金のみに対応するので、counterpartyアドレスは不要です。


関数名は remitXrp() にして、まず preparedPayment()メソッドに渡す二つの引数(sourceAddressとpaymentオブジェクト)を用意します。

入力フォームからそれぞれの値を取得して変数に格納していきます。自分のアドレスであるsourceAddressはシークレットキーから復元できると入力する手間が省けると思ったのですが、調べても分からなかったので諦めました。ご存知の方教えてくださいm(_ _)m。

paymentオブジェクトにはアドレスの他、通貨名や送金量を設定します。

function remitXrp() {
            
    const secret = document.getElementById('secret').value;
    const sourceAddress = document.getElementById('source_address').value;
    const destAddress = document.getElementById('dest_address').value;
    const amount = document.getElementById('amount').value;
    const payment = {
        "source": {
            "address": sourceAddress,
            "maxAmount": {
                "currency": "XRP",
                "value": amount
            }
        },
        "destination": {
            "address": destAddress,
            "amount": {
                "currency": "XRP",
                "value": amount
            }
        }
    };

引数が用意できたらpreparePayment()メソッドに渡します。

preparePayment()メソッドはオフラインでも使えるのですが、その場合は paymentオブジェクトにfee、maxLedgerVersion、sequnceをメンバーとして持つinstructionsプロパティを追加する必要があります。ただこれらの引数が何なのかが自分でもよくわかっていないので今回はオンラインで使うことにしました。

RippleAPIインスタンスを生成してからconnect()メソッドでサーバーに接続します。途中の3つの apiOnline.onメソッドは、エラー時、接続成功時、切断時の処理です。

    const apiOnline = new ripple.RippleAPI({ server: 'wss://s1.ripple.com' });

    apiOnline.on('error', (errorCode, errorMessage) => {
    console.log(errorCode + ': ' + errorMessage);
    });
    apiOnline.on('connected', () => {
    console.log('connected');
    });
    apiOnline.on('disconnected', (code) => {
    console.log('disconnected, code:', code);
    });

    apiOnline.connect().then(() => {
        return apiOnline.preparePayment(sourceAddress, payment);
    }).then(prepared => { /* ここに sign() のコードを書く */ }


preparePayment()の返り値は次のようなオブジェクトです。


{
  "txJSON": "{\"Flags\":2147483648,\"TransactionType\":\"Payment\",\"Account\":\"r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59\",\"Destination\":\"rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo\",\"Amount\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"SendMax\":{\"value\":\"0.01\",\"currency\":\"USD\",\"issuer\":\"rMH4UxPrbuMa1spCBR98hLLyNJp4d8p4tM\"},\"LastLedgerSequence\":8820051,\"Fee\":\"12\",\"Sequence\":23}",
  "instructions": {
    "fee": "0.000012",
    "sequence": 23,
    "maxLedgerVersion": 8820051
  }
}


2.Sign(署名)


次はpreparePayment()メソッドの返り値のうち txJSONプロパティの値にシークレットキーを使って署名します。署名するには sign()メソッドに txJSON と シークレットキーを引数として渡します。


     .
     .
     .

    apiOnline.connect().then(() => {
        return apiOnline.preparePayment(sourceAddress, payment);
    }).then(prepared => {
        return apiOnline.sign(prepared.txJSON, secret);
    }).then(signed => { /* ここに submit() のコードを書く */ }


sign()メソッドの返り値は次のようなオブジェクトになります。

{
  "signedTransaction": "12000322800000002400000017201B0086955368400000000000000C732102F89EAEC7667B30F33D0687BBA86C3FE2A08CCA40A9186C5BDE2DAA6FA97A37D874473045022100BDE09A1F6670403F341C21A77CF35BA47E45CDE974096E1AA5FC39811D8269E702203D60291B9A27F1DCABA9CF5DED307B4F23223E0B6F156991DB601DFB9C41CE1C770A726970706C652E636F6D81145E7B112523F68D2F5E879DB4EAC51C6698A69304",
  "id": "02ACE87F1996E3A23690A5BB7F1774BF71CCBA68F79805831B42ABAD5913D6F4"
}


3.Submit(送信)



最後に、署名したTransactionを submit()メソッドを使ってサーバーに送信します。送信結果を表示する処理も追加しています。

     .
     .
     .

    apiOnline.connect().then(() => {
        return apiOnline.preparePayment(sourceAddress, payment);
    }).then(prepared => {
        return apiOnline.sign(prepared.txJSON, secret);
    }).then(signed => {
        return apiOnline.submit(signed.signedTransaction);
    }).then(result => {
        document.getElementById('show_result').innerHTML = "<table>" +
        "<tr><th>resultCode</th><td>" + result.resultCode + "</td></tr>" +
        "<tr><th>resultMessage</th><td>" + result.resultMessage + "</td></tr>" +
        "</table>";
    }).then(() => {
        return apiOnline.disconnect();
    }).catch(console.error);
};


送金に成功するとこのように表示されます。


送金テスト時に「送金する相手がいない!どうしよう!」という方は rBeWNJNJCoDgt1NCMfsbC9YkkuNcuratWK 宛に送っていただいてもかまいません(笑)このアドレスにXRPを送ると幸せになれるという噂があるとかないとか...

4.今までのまとめ


新規ウォレット作成、残高確認、送金をすべてまとめた最終的なコードは次のようになります。この wallet.html と準備編でビルドした ripple-0.17.4.js を同じフォルダに置き、ブラウザで wallet.html を開くだけですぐに使えます。

【注意】このウォレットを自分のAndoidスマートフォンで試してみたのですが、ボタンを押しても動きませんでした。いろいろコードを変えたり削除したりして調べてみると、どうやら RippleAPIインスタンスを生成するところで不具合が生じているようです。PCのブラウザでは問題なく動作します。

[wallet.html]
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>XRP Wallet</title>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.js"></script>
    <script src="ripple-0.17.4.js"></script>

    <script>        
        
        // 新規ウォレット作成
        function createNewWallet() {
            const apiOffline = new ripple.RippleAPI();
            const pair = apiOffline.generateAddress();
            const address = pair.address;
            const secret = pair.secret;
            
            document.getElementById("new_wallet").innerHTML = "<ul>" +
            "<li>下に表示されているアドレスとシークレットキーを紙に書くなどして保存してください</li>" +
            "<li>特にシークレットキーの保管には十分注意してください。</li>" +
            "<li>画像にしたりやテキストファイルにコピーして保存する場合はUSBメモリーなどの外部メモリーで保存するなどして、インターネットから切り離された状態で保管してください。</li>" +
            "<li>シークレットキーを失くすと資金を引き出すことができなくなります。</li>" +
            "<li>シークレットキーを他人に知られると勝手に資金を引き出される危険があります。</li></ul>" +
            "<table><tr><th>Address</th><td>" +
            address +
            "</td></tr><tr><th>Secret</th><td>" +
            secret +
            "</td></tr></table>";
        };
        
        
        // 残高照会
        function checkBalances() {
            const apiOnline = new ripple.RippleAPI({ server: 'wss://s1.ripple.com' });
            apiOnline.on('error', (errorCode, errorMessage) => {
                console.log(errorCode + ': ' + errorMessage);
            });
            apiOnline.on('connected', () => {
                console.log('connected');
            });
            apiOnline.on('disconnected', (code) => {
                console.log('disconnected, code:', code);
            });
            apiOnline.connect().then(() => {
                const address = document.getElementById('address').value;                
                return apiOnline.getBalances(address).then((balances) => {
                    let resultString = "";
                    for(i=0; i<balances.length; i++) {
                        const value = balances[i].value;
                        const currency = balances[i].currency;
                        const counterparty = balances[i].counterparty;
                        resultString += "<table>" +
                            "<tr><th>通貨</th><td>" + currency + "</td></tr>" +
                            "<tr><th>残高</th><td>" + value + "</td></tr>";
                            if(counterparty) {
                                resultString += "<tr><th>Counterparty</th><td>" + counterparty + "</td></tr>";
                            };
                            resultString += "</table>";
                    };
                    document.getElementById('show_balances').innerHTML = resultString;
                }).then(() => {
                    return apiOnline.disconnect();
                }).catch(console.error);
            });
        };

                
        // 送金する
        function remitXrp() {
            
            const secret = document.getElementById('secret').value;
            const sourceAddress = document.getElementById('source_address').value;
            const destAddress = document.getElementById('dest_address').value;
            const amount = document.getElementById('amount').value;
            const payment = {
                "source": {
                    "address": sourceAddress,
                    "maxAmount": {
                        "currency": "XRP",
                        "value": amount
                    }
                },
                "destination": {
                    "address": destAddress,
                    "amount": {
                        "currency": "XRP",
                        "value": amount
                    }
                }
            };

            const apiOnline = new ripple.RippleAPI({ server: 'wss://s1.ripple.com' });
            apiOnline.on('error', (errorCode, errorMessage) => {
                console.log(errorCode + ': ' + errorMessage);
            });
            apiOnline.on('connected', () => {
                console.log('connected');
            });
            apiOnline.on('disconnected', (code) => {
                console.log('disconnected, code:', code);
            });
            apiOnline.connect().then(() => {
                return apiOnline.preparePayment(sourceAddress, payment);
            }).then(prepared => {
                return apiOnline.sign(prepared.txJSON, secret);
            }).then(signed => {
                return apiOnline.submit(signed.signedTransaction);
            }).then(result => {
                document.getElementById('show_result').innerHTML = "<table>" +
                "<tr><th>resultCode</th><td>" + result.resultCode + "</td></tr>" +
                "<tr><th>resultMessage</th><td>" + result.resultMessage + "</td></tr>" +
                "</table>";
            }).then(() => {
                return apiOnline.disconnect();
            }).catch(console.error);
        };
           
    </script>

    <style type="text/css">
    #container {
        max-width: 600px;
        margin: 0 auto;
        
    }
    .button {
        font-size: 1.4em;
        font-weight: bold;
        padding: 10px 30px;
        width: 100%;
        margin: 40px 0px 0px 0px;
        background-color: #248;
        color: #fff;
        border-style: none;
        border-radius: 5px;        
    }
    #addressText {
        margin: 20px 0;
        text-align: center;
    }
    #remit_form {
        text-align: center;
    }
    .text {
        width: 300px;
        margin-right: 10px;
    } 
    table {
        margin: 10px auto;
        width: 500px;
    }
    td, th { border: 1px solid black; padding: 5px; }
    table { border-collapse: collapse; }
    </style>
</head>

<body>
    <div id="container">
        
        <!-- 新規にウォレットを作成 -->
        <form>
            <input type="button" class="button" value="新規にウォレットを作成" onClick="createNewWallet()">    
        </form>
        <div id="new_wallet"></div>

        <!-- XRPを送金する -->
        <form>
            <input type="button" class="button" value="XRPを送金する">
            <div id="remit_form">
                <table>
                    <tr><th>送金先アドレス</th><td><input type="text" class="text" id="dest_address"></td></tr>
                    <tr><th>送金額(XRP)</th><td><input type="text" class="text" id="amount"></td></tr>
                    <tr><th>自分のアドレス</th><td><input type="text" class="text" id="source_address"></td></tr>
                    <tr><th>シークレット</th><td><input type="text" class=text id="secret"></td></tr>
                </table>
                    <input type="button" value="送金実行" onClick="remitXrp()">
            </div>     
        </form>
        <div id="show_result"></div>

        <!-- 残高を確認する -->
        <form>
            <input type="button" class="button" value="残高を確認する">
            <div id="addressText">アドレス: <input type="text" class="text" id="address"><input type="button" value="確認する" onClick="checkBalances()"></div>
        </form>
        <div id="show_balances"></div>        

    </div>
</body>
</html>


いかかだったでしょうか?JavaScriptやHTMLの説明は省いたのでコードの理解が難しいと感じるかもしれませんが、JavaScriptもHTMLもネット上の情報が豊富にあるので、この機会に勉強してみるのもいいかもしれません。

CSSやjQueryに詳しい方ならもっと見栄えのするウォレットを作れるはずなので、是非チャレンジしてみてください。

0 件のコメント:

コメントを投稿