Reality Keysを使ったWebサービス「BY MY COIN」について(4/4)

f:id:yzono:20141107154603j:plain

はじめに

BY MY COINについての最終回としてclaimの実装を見ます。realitykeysdemo.pyの時はclaim処理ができまで時間がかかりました。JSの場合どのように実装されているか興味あります。

目次

知りたいこと

RK Runkeeper API(結果取得API)

結果取得APIの仕様は以下です。すごいシンプルで分かりやすいです。

リクエスト例

https://www.realitykeys.com/api/v1/runkeeper/376?accept_terms_of_service=current

レスポンス

{
no_pubkey: "02397f0f5cefd6610a6aa722baf0462c5fa6e172b585784ad07644c29ece1fd06a",
user_profile: "edochan",
settlement_date: "2014-09-23",
objection_period_secs: 604800,
human_resolution_scheduled_datetime: null,
measurement: "total_distance",
evaluation_method: "ge",
is_user_authenticated: true,
objection_fee_satoshis_paid: 0,
machine_resolution_scheduled_datetime: "2014-09-23 00:00:00",
user_id: "29908850",
goal: "4000",
created_datetime: "2014-09-05 04:56:45",
winner: "No",
value: "4000",
id: 376,
source: "runkeeper",
yes_pubkey: "0315796f27cc850af4ff89b39b32e571b1e40c005b9943c14ae192e25d0918c5cd",
activity: "running",
objection_fee_satoshis_due: 1000000,
user_name: "edochan",
winner_privkey: "L38R5VMnFyGmAHLY6Cx25ky5QaeNN72QugPJXuCM4DJtTBt4hTuV"
}

Edは4000mたどり着きませんでしたね..w

ソースコードを読む

1.claim実行ボタン表示、claim実行ボタン押下

  1. (ボタン表示)RK結果取得API(RK)
  2. (ボタン表示)P2SHアドレスの残高取得API(blockr.io)
  3. (ボタン押下後)P2SHアドレスのunspentトランザクション取得API(blockr.io)
  4. (ボタン押下後)bootbox.promptを開いて、宛先アドレス入力
  5. (ボタン押下後)execute_claimメソッド実行
function display_single_contract(c) {

(省略)

  data = c;
  
  // 結果取得API実行 /fact は /runkeeperにリダイレクトされます
  url = oracle_api_base + '/fact/' + c['id'] + '/' + oracle_param_string;

  $.ajax({
    url: url, 
    type: 'GET',
    dataType: 'json', 
    success: function(data) {

      data['wins_on'] = wins_on;
      data['charity_display'] = charity_display_for_pubkey(c['no_user_pubkey']);
      data['yes_user_pubkey'] = c['yes_user_pubkey'];
      data['no_user_pubkey'] = c['no_user_pubkey'];
      data['is_testnet'] = c['is_testnet'];
      data['address'] = p2sh_address(data);

      // バランス取得
      var url = c['is_testnet'] ? 'https://tbtc.blockr.io/api/v1/address/balance/'+data['address'] : 'https://btc.blockr.io/api/v1/address/balance/'+data['address'];
      url = url + '?confirmations=0';
      $.ajax({
        url: url, 
        type: 'GET',
        dataType: 'json', 
        success: function(tx_data) {
          var balance = tx_data['data']['balance'];

          var contract_text = {
            'activity_verb': activity_verb(data['activity']),
            'goal_text': data['goal'] + ' meters',
            'settlement_date': data['settlement_date'],
            'charity_display': data['charity_display'],
            'user': data['user']
          }
          $('.view-contract-title-start').text(formatted_title_start(contract_text, true));
          $('.view-contract-title-end').text(formatted_title_end(contract_text, true));

          $('#goal-view-balance').text(balance);
          $('#goal-view-balance-container').show();
          if (balance > 0 && i_won && data['winner_privkey']) {
            $('#single-claim-button').unbind('click').click( function() {
                
              // unspentトランザクション取得
              var url = c['is_testnet'] ? 'https://tbtc.blockr.io/api/v1/address/unspent/'+data['address'] : 'https://btc.blockr.io/api/v1/address/unspent/'+data['address'];
              url = url + '?confirmations=0';
              url = url + '&unconfirmed=1'; // unspent seems to need both of these
              console.log("fetching unspent:");
              $.ajax({
                url: url, 
                type: 'GET',
                dataType: 'json', 
                success: function(tx_data) {
                  var txes = tx_data['data']['unspent'];
                  bootbox.prompt( 'What address to do want to send your winnings to?', function(result) {
                    if (result !== null) {
                        execute_claim(result, c, txes, data['winner_privkey']);
                        return;
                    }
(省略)
}

2.execute_claim実行

  • to_addr = 送信先アドレス(データ元:bootbox.prompt)
  • c = 契約情報(データ元:RK API、画面)
  • txes = unspentトランザクション(データ元:blockr.io)
  • winner_privkey = winnerの秘密鍵(データ元:RK API)
function execute_claim(to_addr, c, txes, winner_privkey) {

  var i;
  var user_privkey = stored_priv_for_pub(c['yes_user_pubkey']);
  
  if (user_privkey == null) {
      user_privkey = stored_priv_for_pub(c['no_user_pubkey']);
  }
  
  if (user_privkey == null) {
      bootbox.alert('You do not appear to have the key you need to claim these bitcoins.');
      return false;
  }

  if (txes.length == 0) {
      bootbox.alert('Could not find any funds to claim. Maybe they have already been claimed?');
      return false;
  }

  for (i=0; i < txes.length; i++) {
      
    var txHex = hex_for_claim_execution(to_addr, user_privkey, winner_privkey, txes[i], c); 
    console.log(txHex);

    // For now our spending transaction is non-standard, so we have to send to eligius.
    // Hopefully this will be fixed in bitcoin core fairly soon, and we can use same the blockr code for testnet.
    // Presumably they do not support CORS, and we have to submit to their web form.
    // We will send our data by putting it in an iframe and submitting it.
    // We will not be able to read the result from the script, although we could make it visible to the user.
    if (!c['is_testnet']) {
      eligius_cross_domain_post(txHex);
    } else {

(省略)

トランザクションでループしているのがよく分からないですね。毎回ブロードキャストしている..? 金額を満たすまでunspent txを取りまとめてブロードキャストするのでは? あー 今回、送金金額を一切入力していない。なので全額を送金先に送るということかな。正しそうだ。

for (i=0; i < txes.length; i++) {

4.claim実行トランザクション作成

function tx_for_claim_execution(to_addr, priv1, priv2, tx, c) {

  var network = c['is_testnet'] ? bitcore.networks['testnet'] : bitcore.networks['livenet'];

  var n = tx['n'];
  var txid = tx['tx'];
  var amount = tx['amount'];

  var utxos2 = [
  {
      address: c['address'],
      txid: tx['tx'],
      vout: tx['n'],
      ts: 1396375187,
      scriptPubKey: tx['script'],
      amount: amount,
      confirmations: 1
  }
  ];

  var pubkeys = [
      [ c['yes_user_pubkey'], c['yes_pubkey'] ],
      [ c['no_user_pubkey'], c['no_pubkey'] ],
      [ c['yes_user_pubkey'], c['no_user_pubkey'] ]
  ];

  var opts = {network: network, nreq:[2,2,2], pubkeys:pubkeys};
  var fee = 10000 / 100000000;

  outs = [{address:to_addr, amount:(amount-fee)}];

  var hashMap = {};
  hashMap[ c['address'] ] = redeem_script(c);

  var b = new bitcore.TransactionBuilder(opts);
  b.setUnspent(utxos2);
  b.setHashToScriptMap(hashMap);
  b.setOutputs(outs);

  var user_wk = new bitcore.WalletKey({ network: network });
  user_wk.fromObj( {
      priv: priv1,
  });
  var user_wk_obj = user_wk.storeObj();
  var user_privkey_wif = user_wk_obj.priv;

  var winner_wk = new bitcore.WalletKey({ network: network });
  winner_wk.fromObj( {
      priv: priv2,
  });
  var winner_wk_obj = winner_wk.storeObj();
  var winner_privkey_wif = winner_wk_obj.priv;

  b.sign([user_privkey_wif, winner_privkey_wif]);

  tx = b.build();
  return tx;
}

5.claim実行トランザクションhex変換

function hex_for_claim_execution(to_addr, priv1, priv2, tx, c) {
  var tx = tx_for_claim_execution(to_addr, priv1, priv2, tx, c); 
  var txHex =  tx.serialize().toString('hex');
  return txHex;
}

6.eligiusに対してブロードキャスト

function eligius_cross_domain_post(data) {

  // Some browsers refuse to do http posts from https pages
  // For now proxy eligius over https to work around this 
  // If helper.bymycoins.com goes away you may need to restore eligius and tinker with browser settings
  //var url = 'http://eligius.st/~wizkid057/newstats/pushtxn.php';
  var url = 'https://helper.bymycoins.com/pushnonstandardtx/pushtxn.php';

  var iframe = document.createElement("iframe");
  document.body.appendChild(iframe);
  iframe.style.display = "none";

  // Just needs a unique name, last 16 characters of tx hex should be ok
  var target_name = 'tx-' + data.substring(data.length-16, data.length); 
  iframe.contentWindow.name = target_name;

  // construct a form with hidden inputs, targeting the iframe
  var form = document.createElement("form");
  form.target = target_name;
  form.action = url;
  form.method = "POST";

  var tx_input = document.createElement("input");
  tx_input.type = "hidden";
  tx_input.name = 'transaction';
  tx_input.value = data
  form.appendChild(tx_input);

  var send_input = document.createElement("input");
  send_input.type = "hidden";
  send_input.name = 'send';
  send_input.value = 'Push' 
  form.appendChild(send_input);

  document.body.appendChild(form);
  form.submit();
}

まとめ

最近blockchain.infoでもnon-standardトランザクションをブロードキャストするようになったので、eligiusを使わなくてもよいかもしれません。

「claim実行トランザクション作成」の部分がよく理解できていませんが、とりあえず今回まででPythonとJSのRKの実装方法についてみてきました。P2SHアドレス作成、claimトランザクション作成の部分は理解が少ないので、別途詳しく調べます。

Python版、JS版どちらともbitcoindが動いていなくても実行できるので便利です。どちらかというとJS版の方がより楽な気がします。

今後について、モバイルアプリ(iOS)などへの移植も面白そうです。次回からはそれをやろうかなと計画してます。

参考

Bootbox.js