Reality Keysを使ったWebサービス「BY MY COIN」について(4/4)
はじめに
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実行ボタン押下
- (ボタン表示)RK結果取得API(RK)
- (ボタン表示)P2SHアドレスの残高取得API(blockr.io)
- (ボタン押下後)P2SHアドレスのunspentトランザクション取得API(blockr.io)
- (ボタン押下後)bootbox.promptを開いて、宛先アドレス入力
- (ボタン押下後)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)などへの移植も面白そうです。次回からはそれをやろうかなと計画してます。