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に詳しい方ならもっと見栄えのするウォレットを作れるはずなので、是非チャレンジしてみてください。

XRP用のウォレットを自作する ー 新規ウォレット作成

前回作った test.html をコピーして名前を wallet.html に変更してから、不要なコードを削除しておきます。このファイルにこれからコードを追加していきます。

[wallet.html]
<!DOCTYPE html>
<html>
<head>
  <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>
    //ここにJavaScriptコードを書いていきます
  </script>
  <style type="text/css">
    //ここにCSSを書いていきます
    td, th { border: 1px solid black; padding: 5px; }
    table { border-collapse: collapse; }
  </style>
</head>
<body>
  //ここにHTMLを書いていきます
</body>
</html>


1.オフライン用のRippleAPIインスタンスを作る


まず、オフラインでRippleAPIを利用するためのインスタンスを作ります。次のコードを<script>~</script>の最初に書いてください。

const apiOffline = new ripple.RippleAPI();


ちなみに、Rippleサーバーに接続してオンラインでRippleAPIを利用するには以下のようにします。

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


2.ウォレットを作る


ウォレットは、アドレスとシークレットキーという二つの要素から成ります。

アドレスとシークレットキーを生成するには generateAddress() というメソッドを使います。このメソッドはオフラインで使えます。

返り値は次のようなオブジェクトになります。

{
    "address": "rGCkuB7PBr5tNy68tPEABEtcdno4hE6Y7f",
    "secret": "sp6JS7f14BuwFY8Mw6bTtLKWauoUs"
}


まず、<body>~</body>にcreateNewWallet()を呼び出すボタンと結果の表を表示する場所を作ります。ボタンをクリックすると、そのボタンの下に注意書きと新しく生成されたアドレスとシークレットキーを表示するようにします。

<body>
    <form>
        <input type="button" class="button" onClick="createNewWallet()">
    </form>
    <div id="new_wallet"></div>
</body>

<style>~</style>にCSSを追加してボタンのデザインや位置を整えます。

    #container {
        width: 50%;
        margin: 0 auto;
    }    
    .button {
        font-size: 1.4em;
        font-weight: bold;
        padding: 10px 30px;
        background-color: #248;
        color: #fff;
        border-style: none;
        border-radius: 5px;
    }


ブラウザで読み込むと、下のようなボタンが表示されるはずです。




では、generateAddress()メソッドを使ってアドレスとシークレットキーを生成して、表形式で表示する createNewWallet() という関数をつくってみましょう。

function createNewWallet() {
    const apiOffline = new ripple.RippleAPI(); // RippleAPIインスタンスを生成
    const pair = apiOffline.generateAddress(); // アドレスとシークレットキーを生成
    const address = pair.address;              // アドレスを取り出す
    const secret = pair.secret;                // シークレットキーを取り出す
    document.getElemetById('new_wallet').innerHTML =
        "<ul><li>下に表示されているアドレスとシークレットキーを紙に書くなどして保存してください</li>" +
        "<li>特にシークレットキーの保管には十分注意してください。</li>" +
        "<li>画像にしたりやテキストファイルにコピーして保存する場合はUSBメモリーなどの外部メモリーで保存するなどして、インターネットから切り離された状態で保管してください。</li>" +
        "<li>シークレットキーを失くすと資金を引き出すことができなくなります。</li></ul>" +
        "<li>シークレットキーを他人に知られると勝手に資金を引き出される危険があります。</li></ul>" +
        "<table><tr><th>Address</th><td>" +
         address +
        "</td></tr><th>Secret</th><td>" +
        secret +
        "</td></tr></table>"
};


「新規にウォレットを作成」ボタンをクリックすると、下の画像のように表示されます。




3.今回のまとめ


ここまでのコードをまとめると次のようになります。

[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 addressAndSecret = apiOffline.generateAddress();
            const address = addressAndSecret.address;
            const secret = addressAndSecret.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>";
        };

    </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;        
    } 
    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>  
    </div>
</body>
</html>


XRP用のウォレットを自作する ー 残高照会

今回は残高を確認する機能を実装していきます。

1.オンライン用のRippleAPIインスタンスを作る


残高を確認するにはリップルのサーバーに接続する必要があるので、オンライン用のRippleAPIインスタンスを作ります。

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

サーバーに接続するには connect() メソッドを使います。オンラインでRippleAPIを使う場合、接続が失敗したときの処理や接続を切断する処理も必要になります。幸いなことにRippleの公式サイトにテンプレートがあるので、それを使わせてもらうことにします。

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) => {
  // code - [close code](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) sent by the server
  // will be 1000 if this was normal closure
  console.log('disconnected, code:', code);
});
apiOnline.connect().then(() => {

  // ここにコードを書いていきます

}).then(() => {
  return apiOnline.disconnect();
}).catch(console.error);


2.getBalances() メソッドによる残高データの取得


残高データを取得するには getBalances() メソッドを使います。このgetBalances()メソッドに残高を調べたいアドレスを文字列として渡すと、オブジェクトを要素として持つ配列を返します。

呼び出し
const address = 'r9cZA1mLK5R5Am25ArfXFmqgNwjZgnfk59';
return apiOnline.getBalances(address).then(balances =>
  {/* ... */});


返り値
[
  {
    "value": "922.913243",
    "currency": "XRP"
  },
  {
    "value": "0",
    "currency": "ASP",
    "counterparty": "r3vi7mWxru9rJCxETCyA1CHvzL96eZWx5z"
  },

  .
  .
  .
  .

  {
    "value": "0",
    "currency": "015841551A748AD2C1F76FF6ECB0CCCD00000000",
    "counterparty": "rs9M85karFkCRjvc6KMWn8Coigm9cbcgcx"
  },
  {
    "value": "0",
    "currency": "USD",
    "counterparty": "rEhDDUUNxpXgEHVJtC2cjXAgyx5VCFxdMF"
  }
]


3.残高データを取得する関数を実装する


それでは最初に、「残高を確認する」ボタン、アドレス入力欄、確認実行ボタン、そして結果を表示する場所を<body>~</body>に追加します。

CSSは 4.今回のまとめ のコードを参照してください。

<form>
  <input type="button" class="button" value="残高を確認する">
  <div id="address_text">Address: <input type="text" class="text" id="address"><input type="button" value="確認する" onClick="checkBalances()"></div>
</form>
<div id="show_balances"></div>


ブラウザで見るとこのように表示されます。



では、残高データを取得する関数を書いていきましょう。

流れとしては、アドレス入力欄の値を取得し、その値を getBalances()メソッドに渡します。そして返り値の配列からオブジェクトを一つずつ取り出して、表形式にまとめていきます。

function checkBalances() {
  
  .
  .    // テンプレート
  .
 
  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>currency</th><td>" + currency + "</td></tr>" +
        "<tr><th>value</th><td>" + value + "</td></tr>;
      if(counterparty) { 
        resultString += "<tr><th>counterparty</th><td>" + counterparty + "</td></tr>"
      };
      resultString += "</table>";

    };

    document.getElementById('show_balances').innerHTML = resultString;
  
  .
  .  // テンプレート
  .

}


試しに東京JPYのアドレスを入力してみると以下のように結果が表示されました。



4.今回のまとめ


では今回作った残高照会のコードを、前回の新規ウォレット作成のコードに追加してみましょう。CSSも追加しています。

[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>

        const apiOffline = new ripple.RippleAPI();

    // 新規ウォレット作成
        function createNewWallet() {
            const addressAndSecret = apiOffline.generateAddress();
            const address = addressAndSecret.address;
            const secret = addressAndSecret.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>currency</th><td>" + currency + "</td></tr>" +
                            "<tr><th>value</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);
            });
        };

        
        
    </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;
        }
    .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 class="button" type="button" value="新規にウォレットを作成" onClick="createNewWallet()">    
        </form>
        <div id="new_wallet"></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>

これで残高照会機能の実装は終わりです。次回はいよいよ送金機能を実装していきます。

2017年5月8日月曜日

XRP用のウォレットを自作する ー 準備編

今年の一月から暗号通貨のXRPを買い始めました。今のところ順調に価格が上昇していて、投資としては大成功です。まだまだ上がると思っているので当分売るつもりはありません。

そこでXRPを長期間保管しておきたいのですが、XRPに対応しているハードウェアウォレットがありません。そのうちTrezorやLedger nanoとかが対応してくれると思いますが、いい機会なので自分でXRPを保管しておくウォレットを作ってみようと思います。


|方針


リップルネットワークを使った送金などのTransactionは、大きく4つの段階に分けられます。

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

このうち第1段階の Prepare(準備)と第2段階の Sign(シークレットキーによる署名)はインターネットに接続しないでオフラインで処理することが可能です。また、ウォレットの新規作成(アドレスとシークレットキーの生成)もオフラインで作成することが可能です。

そこで、より安全を求めるならシークレットキーはインターネットから切り離された環境で取り扱う方がよいので、ウォレットの作成および送金の際の第2段階まではインターネットに接続していないPCやスマートフォンなどで処理することにし、その処理結果をUSBメモリなどでインターネットに接続したPCやスマートフォンに移動してから第3段階以降を処理するという方法もあります。

しかしオフライン処理についての自分の理解が不十分なことと、スマートフォンでの動作に不具合があり、その解決策がわからないため、今回はまず普段使っているPCで使用するウォレットを作ってみます。オフライン処理の理解が進み、スマートフォンでの不具合が解消ができればインターネットに接続していない端末とインターネットに接続している端末の2台を使ったウォレットの作り方も書く予定です(書かないかも...


|ウォレットの仕様


ここで作るウォレットには、以下の基本的な機能のみを実装していきます。
  • 新規にウォレットを作成
  • 残高照会
  • 送金
送金できるのはXRPのみです。また、これらの機能はChromeやFirefoxなどのブラウザ上で操作できるようにします。


|ウォレットを作るための環境を構築


OSはWindows 10を前提にしています。

参考サイト




環境構築の流れ

  1. Node.js をインストール
  2. Git for Windows をインストール
  3. 作業用フォルダを作る
  4. ripple-lib をインストールしてJavaScriptファイルをビルド
  5. ビルドしたJavaScriptファイルとテスト用の test.html を作って動作を確かめる


1.Node.js をインストールする




Node.js (https://nodejs.org/ja/) のサイトから推奨版か最新版をダウンロードしてインストールしてください。

2.Git for Windows をインストールする



Git for Windows (https://git-for-windows.github.io/) のサイトからファイルをダウンロードしてインストールしてください。

3.Git Bash を起動して作業用フォルダを作る


「スタート」→「Git」→「Git Bash」をクリックしてGit Bashを起動します。



ここではプロジェクト用のフォルダとして「wallet」という名前のフォルダを作って、そこで作業をしていきます。フォルダ名は好きな名前で構いません。

Git Bash で次のコマンドを実行します('$'は入力不要)

$ mkdir wallet
$ cd wallet

4.ripple-lib をインストールしてJavaScriptファイルをビルド


Git Bash から次のコマンドを実行してください

$ git clone https://github.com/ripple/ripple-lib.git
$ cd ripple-lib
$ git checkout release
$ npm install  //この処理には時間がかかります。
               //WARN(警告)が出ますが気にしないでいいようです
$ npm run build

すると wallet\ripple-lib\buildフォルダの中に ripple-0.17.14.js (バージョン番号は異なるかもしれません)というJavaScriptファイルができているはずなので、そのファイルを walletフォルダ直下にコピーしてください。このJavaScriptファイルをHTMLファイルへ読み込むとブラウザ上でRippleAPIを使えるようになります。

5.ビルドしたJavaScriptファイルとテスト用のHTMLファイルを作って動作を確かめる


Rippleの公式サイトに サンプルコード があるのですが、そのままではうまく動きません。

まず、サンプルコードをコピーして test.html というファイルをつくり、ripple-0.17.4.js と同じwalletフォルダ直下に置きます。

それから、test.html の上から4行目に次のコードを追加します。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.js"></script>

また、上のコードを追加した後の5行目のコードのバージョン番号を次のように修正します。ここはビルドしたjsファイル名に合わせて変えてください。

<script src="ripple-0.17.4.js"></script>

コード全体としては以下のようになります。

[test.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>
    console.log(ripple);
    var api = new ripple.RippleAPI({server:'wss://s1.ripple.com/'});
    api.connect().then(function() {
        return api.getServerInfo();
    }).then(function(server_info) {
    document.body.innerHTML += "<p>Connected to rippled server!</p>" +
"      <table>" +
"        <tr><th>Version</th>" +
"          <td>" + server_info.buildVersion + "</td></tr>" +
"        <tr><th>Ledgers available</th>" +
"          <td>" + server_info.completeLedgers + "</td></tr>" +
"        <tr><th>hostID</th>" +
"          <td>" + server_info.hostID + "</td></tr>" +
"        <tr><th>Most Recent Validated Ledger Seq.</th>" +
"          <td>" + server_info.validatedLedger.ledgerVersion + "</td></tr>" +
"        <tr><th>Most Recent Validated Ledger Hash</th>" +
"          <td>" + server_info.validatedLedger.hash + "</td></tr>" +
"        <tr><th>Seconds since last ledger validated</th>" +
"          <td>" + server_info.validatedLedger.age + "</td></tr>" +
"      </table>";
    });
  </script>
  <style type="text/css">
    td, th { border: 1px solid black; padding: 5px; }
    table { border-collapse: collapse; }
  </style>
</head>
<body></body>
</html>

test.html をブラウザで開いてみると、下の画像のように表示されるはずです。


以上でウォレット作成のための環境構築は終わりです。

2017年4月20日木曜日

投資で勝つために必要なものとは? 『新しい株式投資論ー「合理的へそ曲がり」のすすめ (PHP新書) 山崎 元(著)』

NISAとかを利用して株式投資を始めてみようかと考えている人もいると思いますが、その前に絶対読んでおいた方がいいオススメの本が山崎元さんが書かれた『新しい株式投資論ー「合理的へそ曲がり」のすすめ』です。


新しい株式投資論―「合理的へそ曲がり」のすすめ (PHP新書)
山崎 元
PHP研究所
売り上げランキング: 124,418

10年も前に発売された本ですが、考え方は今でも通用します。
山崎さんの性格なのかもしれませんが、文章に勿体つけるところが一切無く、「結論→理由」という流れで書かれているので、気持ち良く読み進めることができました。新書版で230ページ程なので、読むのが速い人なら1日あれば読み終えることができるのではないでしょうか。投資経験が長い人なら第4章を読むだけでも著者の言いたいことは分かると思います。

株式投資に努力は不要

 株式投資は不思議な世界だ。
 プロのファンドマネージャーの運用成績の平均は、ならして見ると、株式市場の平均に勝てない。(中略)しかし、公平に見てプロのほうが、知識も情報も豊富だろう。それなのに、プロが勝てない株式投資とは、一体どのような性格のゲームなのか。彼らの努力や勉強が報われないのは、なぜか。
(P.3より引用)
サルにダーツを投げさせて作ったポートフォリオの方が、プロのファンドマネージャーが作ったポートフォリオよりも運用成績が良かった、という話は聞いたことがあるかもしれません。 
いくら勉強しても、どれだけ経験を積んでも、サルに負けてしまう株式投資という不思議な世界で、どうすれば儲けることができるのか。
結論から述べる。株式投資で成功するために必要なものは、一に「運」、二に「センス」だ。
(P.14より引用)
のっけから身も蓋もない結論です(笑)。
経済やチャートの分析方法を勉強したからといって勝てるものではないし、何年も経験を積んだからといって勝てるものでもない。
そして、「運」は自分ではどうしようもないので、残りの「センス」を磨くしかなく、「センス」を磨くにはそれほど時間や経験は必要ないが、先入観や自分に都合の良い期待を捨てなければならない、と山崎さんは言います。

株式投資に必要な「センス」

では「センス」とは何なのでしょうか。
「株式投資は不美人投票である」と主張したい。端的に言って、これが株式投資のコツであり、株式投資に必要な「センス」の半分以上を説明すると思う。
(P.218より引用)
ケインズの「株式投資は美人投票だ」というのは聞いたことがあるかもしれません。自分が美人だと思う人ではなく、他の多くの投票者が美しいと思う人に投票しなければ勝つことはできない、というあれです。
一見正しいようにも思えますが、皆が高評価している銘柄にその時に投資しても、その評価はすでに株価に反映されているので、たいして儲かりません。
感覚的に言うと、現在、注目と人気を集めている美人を少し遠ざけて、不人気なのだけれども長所がある半美人、どう考えても人気面で評価されているとは思えない不美人的な銘柄に投資することが、株式投資ゲームのコツなのだ。
(P.225より引用)
美しい人が更に美しくなる確率より、今は評価の低い人が化粧や服装などの工夫によって以前よりも評価が改善する確率の方が高いということです。
メガネを取ったら意外と美人だったとか、怠け者がちょっと頑張ったらすごく褒められるみたいなことですね。
隠れた魅力に気づいている人は少ないけど、ちょっとしたキッカケでその魅力を皆が知るようになって評価が高まる可能性が高い、そういう銘柄を見抜く能力が山崎さんの言う「センス」の半分の意味。

徹底的に合理的に行動する

では「センス」のもう半分の意味は何でしょうか。
本書の内容から自分なりに解釈すると、
自分の行為や他人の行為が合理的なのか、それとも非合理的なのかを見極める能力のこと
ではないかと思います。
山崎さんは本書の中で、「分割購入」「ドルコスト平均法」「利食い/損切りの目標株価の設定」「テクニカル分析」など、初心者向けの投資指南本には必ずと言っていいほど紹介されている投資手法を、いずれも非合理的だとしてバサッと一刀両断に切り捨てていきます。
これらは一見すると合理的に思えるので、非合理的だと言われてもすぐには納得できないかもしれません。
だが、たとえば分割売買やドルコスト平均法が、投資の方法として合理的ではない、ということについては、意図的に頭を使って注意を向けていないと、気づきにくいし、運用結果には「運」の要素の影響が圧倒的だから、経験を糧にして、正しい原則の理解に至ることが、しばしば難しい。
(P.19より引用)
株式投資は「運」がほとんどを支配するゲームであり、非合理的な行動を取っても偶然儲かってしまうことがあるために、自らの非合理的な行動に気づきにくいのです。
しかし、「運」がほとんどを支配するからこそ、感情的に納得できなくても徹底的に合理的な行動を取り、自分でコントロールできる僅かな勝つ確率を少しづつ積み上げていくよりほかはないのです。
それはサルにはできない、人間だけができることではないでしょうか。

「逆」ではなく「裏」を取れ

 それでは、無意味な不利を背負わないために、合理的な行動原則を尊重することは当然として、それ以外にどうやって、プラスの「差」を作る手立てがあるのだろうか。
 著者の考えるアプローチは、他の市場参加者が陥りやすい誤りを避け、願わくは、これを少々利用して、自らにとっての確率を少しでも有利に改善することだ。
(P.20より引用)
山崎さんはこのアプローチのことを「逆」ではなく「裏」に張る感覚だと説明しています。
手数料などのはっきりしている無駄なコストは省きつつ、他人の非合理的な行動を嗅ぎ分け、先回りして待ち伏せするような感じでしょうか。
前述した「不美人投票」の考え方は、まさに皆が気づく前に買っておいて、評価が高くなるのを待ちぶせするのと同じですね。
ただ実行するのは勇気がいると思います。
先回りして待ち伏せしていても一向に獲物が現れなかったり、裏を取ったつもりが逆に裏を取られているかもしれません。
皆と同じ投資手法を取っていれば安心できるでしょうし、失敗した時も自分のやり方は間違っていなかったと言い訳できるでしょう。
しかし、それではいつまでたってもダーツを投げるサルには勝てないのです。