支払・受取の区別ができるようになった。旧式の改良版。
ブツ
経緯
上記で取引の集計などをしたかったが、取引データから支払・受取の区別が正しく行えず、間違った算出結果となってしまった。正しい方法を探すべく試行錯誤した結果、ようやく実現した。
試行錯誤
大まかにいうと以下のとおり。
- MasteringBitcoin日本語訳PDFを読んでトランザクションの基礎をうっすらと理解した
- ブロックチェーン・エクスプローラなるものを使い、取引の入力となるデータの所持者アドレスを取得することで支払・受取を区別すればいいと理解した
変更点
追加機能
XMPと独自トークン(モナカード)の残高も表示するようにした。
集計
いくつかの集計方法を試したみた結果を表示している。その理由はあるAPI取得による集計値が正しくないように思えるから。詳細が判明するまではこうしておく。
謎
- 受取総額、支払総額が多すぎる
- 受取人数が多すぎる
- 残高がmpurseのときと微妙に違う
受取総額、支払総額が多すぎる
Trezor address
Trezor addressを使い、指定アドレスの受取総額、支払総額らしき値を取得できる。だが、実際の取引額より大きすぎる。
たとえば、ある時点での私のアドレスにおける額は以下。
項目 | 金額 |
---|---|
支払総額 | 11.84094860 MONA |
受取総額 | 29.76631530 MONA |
残高 | 17.92536670 MONA |
でも、こんなに受け取っていないはずだし、支払ってもいない。残高はほぼmpurseのそれと同じなのでOK。(なぜか微妙に違うのが気になるが)
Trezor tx
次は取引データ一件ずつから支払総額などを計算してみた。こちらは私の認識と同じ。正しいのはこちらと思われる。
Trezor addressのtxids
ですべての取引データへのtxid
を取得したら、Trezor txで一件ずつの詳細な取引データを取得する。これで支払・受取を判別し、そのアドレスや金額、手数料を取得する。それをもとに自分で集計したデータが以下。
項目 | 金額 |
---|---|
支払総額 | 0.93718430 MONA |
内手数料 | 0.00024750 MONA |
手数料比 | 0.03% |
受取総額 | 18.86255100 MONA |
残高 | 17.92536670 MONA |
残高だけは同じ。でも支払総額と受取総額がぜんぜん違う。なぜこのような差が出てしまうのか謎。
私の記憶では自分で計算したこちらのほうが実際値だと思っている。
受取人数が多すぎる
ある時点での人数。記憶にあるのより受取人数が多すぎる。
項目 | 人数 |
---|---|
支払人数 | 6人 |
受取人数 | 41人 |
両思人数 | 3人 |
取引データをみて日時や金額などから察するに、もなフォーセットで受け取ったとき、毎回違うアドレスで支払われているのが原因のようだ。なぜそんなことになっているのか原因不明。
BIP0039
トランザクション情報から支払か受取かを判別する方法がわからないでいただいたコメントによると、BIPとやらが関係しているかもしれません。
BIPのどこを採用してるかにもよりますが(多分BIP39)、同じアドレスを使用し続けるのは良くないよねっていうことでおつりは新しいアドレスを作ってそこに返すとかやってるウォレットもあるかもしれませんね。
BIPとは? MasteringBitcoin日本語訳PDFでBIP
と検索すると以下がヒットしました。
bip:Bitcoin改善提案(Bitcoin Improvement Proposals)の略称で、Bitcoinコミュニティのメンバーが Bitcoinを改善するために提出してきた一連の提案のことを指します。例えば、BIP0021はbitcoin uniform resource identifier (URI) スキームを改善するための提案です。
BIP0039
で文字列検索してもあまり情報はなかったので、ググってみると以下がヒット。
BIP39は主にHDウォレット(BIP32)と組み合わせて使うことが想定されたプロトコルで、ウォレットの秘密鍵をユーザーが安全に保管するために考案されました。
セキュリティを強化する案らしい。
秘密鍵は256bitから512bitのバイナリで出来ており、普通の人には覚えることができず、書き写すにしても間違えやすくPCやスマートフォンにデータを保存する以外の選択肢がありませんでした。これを解決したのが、BIP39です。BIP39で標準化されたプロトコルに準拠すると、12〜24ワードの英単語で構成されたニーモニックコードからHDウォレットのマスターシードを生成することができます。これによって、ユーザーは自分のウォレットの秘密鍵をPCやスマートフォンで生成したまま放置せず、紙に書き写し、ハードウェアウォレットに移すことでデバイスから秘密鍵を分離できます。
12〜24ワードの英単語……そういえばmpurseでアドレスを作るとき「パスフレーズ」とかいうそれらしきものが出てきた。それのことかな?
つまり、インターネットを通じたハッキングによる秘密鍵の盗難や、デバイスの故障による紛失からビットコインを守ることができるようになったのです。一方で、ウォレットによってはBIP39に準拠していない独自規格のものもあり、どのウォレットでも同じニーモニックコードが使えるというわけではないことに注意が必要です。
「ハードウェアウォレットに移すことでデバイスから秘密鍵を分離できます」という言葉から察するに、たぶん紙のような電子機器以外のものだけに秘密鍵を書き残すことで、ネットを通じたハッキングから秘密鍵の盗難を防ぐための仕組みなのだろう。
mpurseのパスフレーズとは関係ないのかな? よくわからない。ニーモニックコードという名前でもなかったし、違うのかも?
同じアドレスを使うのが良くない理由がわからなかった。秘密鍵が逆算されるリスクがあるとか? 少なくともアドレスを分散させれば、どれかひとつがハッキングされても全額一気に奪われることはなくなるけど。
たぶん、まだ基礎知識が足りないのだろう。まずはさらっとでもMasteringBitcoin日本語訳PDFを一通り読むべきか。272ページもあるから大変だけど。
対処法なし
いずれにせよもなフォーセットの当選回数がそのまま受取人数として加算されてしまっている状態。
私としては「受取人数=支援してくれた人の数」として認識できる値にしたかった。だが、もなフォーセットは毎回新しいアドレスを作って支払っているせいで「受取人数=支援してくれた人の数」として考えることができなくなってしまった。その原因はセキュリティ対策かもしれない。
まあ、もとから一人で複数アドレスを持つことができるので「人数<アドレス数」になってしまうのは仕方ない。ただ、それにしても支払うたびにアドレスを作成されたら、そのズレが尋常じゃない件数になってしまう。
どうしたものか。対処法は思いつかない。少なくとも、取得できるデータではどうにもならないと思う。
残高がmpurseのときと微妙に違う
mpurseは残高をmpchainで取得していると思われる。
const addr = await window.mpurse.mpchain('address', {'address':'MEHCqJbgiNERCH3bRAtNSSD9uxPViEX1nu'});
addr.mona_balance
これによると取得値は以下。
項目 | 金額 |
---|---|
残高 | 17.92536500 MONA |
2つを比較すると以下。なぜかtrezorのほうが0.00000170
MONAほど高い。
API | 金額 |
---|---|
trezor | 17.92536670 MONA |
mpurse | 17.92536500 MONA |
誤差の範囲とはいえ、残高で誤差があったら困る。しかし原因不明。どうしたものか。
技術
取引データの取得
取引データの取得は主に以下APIを使った。ここが一番のキモ。特にコメントで教えていただいたTrezorのおかげで取引が支払か受取かを区別できるようになった。
データの取得には次のような工夫をしている。
- ページネーションで全件取得する
- 前回の続きから取得するようにしてデータ量を必要最小限に抑える
集計結果に疑問は残るものの、大筋で目論見どおりになった。
取引データの保存
IndexedDBライブラリdexie.jsを使うでやった方法でローカルに取引データを保存した。
- DB(名前=アドレス)
last
テーブルsendPartners
テーブルreceivePartners
テーブルtransactions
テーブル
DBをアドレス単位にすることで、複数アドレスを使ったときにも対応できる余地を残しつつ、テーブル結合を最小化し、外部キーなど不要な項目を作らぬようにした。
テーブル | インデックス | 概要 |
---|---|---|
last |
++id |
最後に取引データを取得した時点での集計結果や最新取引データ特定情報(次回取得時に使う) |
sendPartners |
address |
支払った相手の集計結果 |
receivePartners |
address |
受け取った相手の集計結果 |
transactions |
txid |
全取引データ |
last
テーブルはレコードがゼロ件または一件のみ*Partners
テーブルは送受金したアドレス数だけレコードができるtransactions
テーブルは取引回数だけレコードができる
for await of 構文
Trezor address APIでは一度のリクエストで最大1000件のtxids
要素が取得できる。それは新しい順であり、それより古い取引についてはページネーションする必要がある。
このときAPIの実行は同期すべくawait
にし、さらにAPIを1回実行するたびyield
でデータを返したい。それをクラスのメソッド経由で行いたい。そしてそれを呼び出して受け取る側はループ処理したい。このときのコードは以下のようにfor await of
構文を使う。
API結果受取(ページネーション単位で結果を受けとりループする)
for await (const res of trezor.address(address, options)) {...}
APIリクエスト(ページネーション単位で結果を返す)
class TrezorClient extends RestClient { // https://github.com/trezor/blockbook/blob/master/docs/api.md#get-transaction
async *address(address, options={}) { // 指定アドレスの取引txid一覧を取得する
yield* await this.paginateAddr(address, options)
}
async *paginateAddr(address, options) {
while (still) {
const res = await this.get(`${this.domain}api/v2/address/${address}${(query) ? '?' + query : ''}`, headers)
yield res
}
}
}
以下のような条件でのコーディングは初めてだった。
- ジェネレータと非同期の両方を使う
- ジェネレータ関数を使う
以下のような構文を使うことで実現できた。
for await of
構文*関数名
構文yeld* 関数呼出
構文yeild 値
構文async
,await
async
もyield
もJavaScriptの初期には存在しなかった後付の構文であるためか、関数定義やyield
に*
をつける必要があったり、付けたらダメだったりと、わかりにくいところがあって苦労した。
しかもfor await of
など、ふだんのawait
をつける場所とは違うところにつけるのも、わかりにくい。
正直、ジェネレータも非同期もよく使うのに、コードが書きにくい感じがして残念。慣れればいいのだろうが、記法パターンを覚えねばならないし、その量が多い。こんなところに脳のリソースを使いたくない。
所感
やっと集計やら作図やらが何とかできた。
集計に課題はあるものの、暗号通貨の仕様上どうしようもないと思われる。毎回アドレスが変わるなんて聞いてないよ。仕方なくそういうものとして受け入れるしかない。
IndexedDB
IndexedDBのインデックス列というのが厄介。ふつうにSQL文のように条件検索や集計もSQL文でできるかと思っていたが、そんなことはないらしい。RDBMSとは使い勝手が違って、なんだかモヤる。
とくにDBからデータを取り出すとき、インデックスキーにしていない項目でフィルタリングするとき。現状は全件取得しJavaScriptのfilter関数にかけている。それってパフォーマンス的にどうなの?
かといって全列をインデックスにするとファイルサイズが膨大になるのだろうし。全ユースケースを洗い出してインデックス列をピックアップしておけばよかったのだろうが、集計のほうも手探りだったし。まだまだ改良の余地があるかもしれない。
IndexedDBに限らず、フロントエンドだけでローカルファイルを操作すること全般について、もっと調べたい。その上で改良できるならしたい。
展望
今度はこれの簡略版で、モナコイナーとしてのステータスカードみたいなのを作ってみたい。支払総額とかの集計値を小さなHTMLパーツとして作成する感じ。
でもそれをリアルタイムにやるには膨大なAPIの発行が必要になると今回わかってしまった。対策を考える必要がある。やはりデータ保存がキモになるか。
次の課題は、この取引データをローカルにファイルとして保存すること。インポートやエクスポートがしたい。
もし取引データをローカルに保存できれば、その集計をJSONに変換し、GitHubなどへアップロードもできる。あとはそのデータを使えばモナコイナー・ステータスカードが作れそう。
投げモナボタンを表示するHTML作成もしたいし。どうしようかな。
やりたいときに、やりたいことを、やりたい分だけやろう。気楽に楽しみながらやるのが一番。