フルノードを最新版v1.4.1.6にしてから調子が上がってきたのでAPIを本格的にいじってみた。今までの記事とは違って、実践的な記事にしていきたい。
環境構築
大きく分けて2つ環境構築のステップがある。
- フルノード構築
- ライブラリ用意
このうち一番厄介なのは1のフルノード構築だ。2に関しては、今回はJavascriptのNode.jsを使う。
フルノード構築
フルノードを運用するために必要な手順は以下の記事で簡単にまとめたので参照してほしい。
さて、フルノードを立ち上げてもAPIリクエストするためには、フルノードを同期させるところまで完了する必要がある。フルノードの同期とは、
latestMilestoneIndex
とlatestSolidSubtangleMilestoneIndex
のフィールドの数字が同じである。- この数字がSlackの#botboxチャンネルで更新されるマイルストーン番号と一致している。
この手順が厄介であるがゆえに、多くの人がIOTAに手を付けずにいると筆者は考えている。
注)同期させなくても実行できるAPIリクエストはあるが、送金受金などお金をやり取りするAPIは同期なしには実行できない。本記事はウォレットを模倣することを目的にしているため、あくまでフルノード同期を前提に話を進めていく。
ライブラリの用意
フルノードを走らせているマシンにライブラリをインストールしよう。今回はUbuntuのマシンを前提に話を進めていく。
Node.jsのインストール
sudo apt-get update
sudo apt-get install nodejs
npmのインストール
sudo apt-get install npm
IOTAのライブラリ
npm install iota.lib.js
ここまでペタペタペーストでささっと準備完了だ。フルノード立ち上げまでが環境構築の難所だった。
基本API
まずAPIをマシンが認識して正しく動くかの確認のためにgetNodeInfo
リクエストで、自分のフルノードのステータスを表示させてみよう。
コードの保存ディレクトリだが、~/node_modules/iota.lib.js/examples/
を使っていく。
var IOTA = require('../lib/iota');
// ポート番号はiri.iniで設定したAPIポートを使う。
var iota = new IOTA({'host':'http://localhost','port':14265});
// おなじみのgetNodeInfo API
iota.api.getNodeInfo(function(error, success) {
if (error) {
console.error(error);
} else {
console.log(success);
}
});
これをnode basic.js
コマンドで実行すると下記のような結果が返ってくる。
{ appName: 'IRI',
appVersion: '1.4.1.6',
jreAvailableProcessors: 4,
jreFreeMemory: 781752912,
jreVersion: '1.8.0_151',
jreMaxMemory: 1993867264,
jreTotalMemory: 1993867264,
latestMilestone: 'SOETRTUZATLQHS9KJGPOJQSWBJUUDPNLSZCAGAGASJSMZPJUMAJWMSACWLISMKHTXUZUAQQARBOY99999',
latestMilestoneIndex: 322044,
latestSolidSubtangleMilestone: 'SOETRTUZATLQHS9KJGPOJQSWBJUUDPNLSZCAGAGASJSMZPJUMAJWMSACWLISMKHTXUZUAQQARBOY99999',
latestSolidSubtangleMilestoneIndex: 322044,
neighbors: 4,
packetsQueueSize: 0,
time: 1515351225845,
tips: 4549,
transactionsToRequest: 2,
duration: 1 }
'latestMilestoneIndex = latestSolidSubtangleMilestoneIndex'が**#botbox**のマイルストーン番号322044と一致している。準備完了だ!。
受金
Private Keyとは
81トライトのSeedから生成するのがPrivate Key。ウォレットを作る際はAPIが内部で自動にやってくれるのでこのステップは必要ない。
var IOTA = require('../lib/iota');
var Signing = require('../lib/crypto/signing/signing');
var Converter = require('../lib/crypto/converter/converter');
var iota = new IOTA({'host':'http://localhost','port':14265});
var seed = 'ABMUSHI9999';
var index = 0;
var security = 2;
// トリッツに変換
var key_trits = Signing.key(Converter.trits(seed),index,security);
// トライトに変換
var key = Converter.trytes(key_trits);
// Private Keyの長さを表示
console.log('key length: ',key.length);
// Private Keyの表示
console.log('Private key: ',key);
key length: 4374
Private key: UVJBFCMASCXNLDCUS9IGZSNUHZSZU9RLMLENNSKLNDLIIWNXSFKBPEECY9CYBSHJXACOVOMGW9SXZIHTZTEYCYJXWT9ZJLFPYFMCONZJH9ATMGTKUJBNLOKUYLUTMZJWTVMKOLLY9IIWQUYBFYYMXTCTAIE9VNJZNC9TAQKXXNGEVRPGBWTXXDCTJZSOTGAINIPMSSJTBHMKMURJETYTF9BEMOKKEJWCLTUVILTBQ9HYHSIKYLBBVS9VKFSJWVNY9QQEAZTGRUQU99DAXBYSJJ9RNOLKYWSHPRGFIFSSKDRWV9SWAPURLXMKWTSA9SVTZGNZVTEDBVNKVRROFURKETEAS9KGMOSZZYDFQVBNWOCHGNPBJ9SNM9GJXNMLYTBSXCARFYXCKDLAXTRESCOMCFMZLFIP9BXKOTIGWMOJFIIUOJIFRIJPSQUDTHHOFYBSPTXMHRLLSXVGZLLGXOQXEVYN9LAWBLCEX9SDECBQKRBEOXUHZGBFZZQYLQVOPEMOEADFLAKSFOPJKVCWJH9ATREUFC9NFSYPGMBGTVKPUVX9NVK9OZEKPVBZWMSNICHHNCGXDPJKCTLKJEYUOQDLYXSUFK9HAZKJFYYJBALRNCXQQPVGFYYUBJWQMNPVQCKFHZERQWXDHKBUOVKCHWUYITKMYYRMZQFURMNRVTBPQOPWTDIRDWZL9IQNEDDHVMEIBYYQHBDUTVNFTAGNBINBASTMYPFTJP9ANCUAMMAHVFFBQGQVWPBSQZPZYVYLUBLH9JISTTCYUOYKWTSKDWVJLJGR9TY9ZDAP9UZZRWFSCYDHZHPEVFAOBFWLFPFYKUDCVEEINJT9XLZQZQTRZCPKGBBIDPEFGQGTN9KEWAYVOGNWEZMBUQHLJKDJWTDAMCICBUHSKXIRGJMPZETYOOAER9JAMTPTGGFG9RFZRBZVWHTQYJVHH9OHBXW9PRWYGKWCEGIERLWZEZRAIODDMCCKFKEYWSKTRPLPYXVPIUVJG99Q9UMAISMVD9EHMWMNWNKSUFSCGMBQYPEGFCOZMDFVHOCHMAFOBKHEZPJWUSMLDKDTSQIYUUVJQKPBCJCFVQTKHPABVS9QXQZCYXFLSCKELAAQOBBKZJJSUISEQGEDSODEKCKGBHYNUBPGODVEGVXOODG9HSLJFSNTTWYJY9HKZYNNQSAZXNFINCBFCBS99WTXVZNDPPSBMVRKDNFOJZCMGLWTUHKGXBZUHHH9SHOVACQWVGCLEACVXKZSUJOFNQYFPGTZRTNHPUKNHQFACBOOROHITJGMFTZYZXE9MHSCUHRVAURXIIWPFIUWVQPVYZWCFBHPQOIQRBNQWAPNTLINAPFUQXOKML9WZNTIUSXBIVVPLTWWXNDQWYPKPANVZFSWPGHRDLKTBKM9LVMNW9VXQGHCRYVYKLDABIBUUFGQXDWJPMUJRCCQDUZRZLHDTNQEHDKEHAMMOMX9RBTXNZANIOCZSZPIFIYQGLXCMMYPCVCOY9IENHFXBRKDWYSVSVVVUDBBHUZDDTYWFWPABELGRCYVCBNPLCTSVUGMOEDFOXYASGCNCHJWM9QTEEATNATRBSHPSSR9PNROLOKSZOHJ99VVUFFKZOTIQRKEJEEYRJVFVOJBLDPSAHOERXJYKVWWQTAMJNQ9GQTIMUGSHNJGGLWRNFKGOIMMEBIYCIIVCPVRWHQFRVOOQWKICECNHWYKCECZZNUEPQNMGNPMUECYVMIEVNUFYJZLZB9USRIPDYZKZONICFRCWVLUWDPVFNLUMIZOBYPGAZANDKZEIGWRNSKBPLQSYGVMNKARJW9WGAYSESIVIAFGEBAPVYFBRUSDTRNKOKGHUDLAIJPBVXEIRBPKW9ZMVYTTYZZTAFNRVYQMMJKBTHTQGOSRWTBHG9Z9DS9YILJSKHCDEZJSNYGHIDTO9WYPRGBBPVKMBKGEZCCDPJVLKUFCBTGQNYW9HKZBYWKMBFICNNVDJTDJNJLVWRRDHEFOJDETKDUQE9MXFUDPXYAAFQYIKRFPPAHMBOPBRACFMIRTJVNU9EWXOLRJCETWCDOVRXDGGBZOQHWBXXULMKOJHWOJJSDLQJ9VTWGCABHZCFKSJC9GQW9RVNXZDFUDYJOSUNB9SLMJHPCXRT9EZGNZJUOWMEXWQL9HP9SJOBIHOPJHBXIIHIL99MXIINCBJGASDVZCVZXFOMQALRATKXIOZTLIHYFKNMX9CXZIMFFPIENKMRLYFCHP9ZYCHVZTJKLAP9QRQGZYVFXRH9BKIBR9EZATZTHEWI9LCROAZIJKDQHNUYSMPQVJUQFF9MDKQYZJYYBXSPNTMEPGEWQZPLBAKBXYWSVSNVAFIBVHWIFIRKVNLFGULQTIOOOPVUKGBHXFCMDYMIKUMGQHKSEEQ9WGLYTF99VBFLCWN9CDKUJYYRKINXDKVECSQZZAW9CFVLPSXOZPHNREYWRZZDMTOBXJINVEWKYIQXJOZHEKUFNKLDVDURSPHUULPMAAWVDBXBXXQIMSVKDQVVYHD9AXDHSYSOG9EVOBYVBPOWSPLPEEES9HDJZOPOBGSJJGEDMSWAESIWCWBABEKLFRXLXIJHKXQOVWXR9UBIGPW9TSVVNOKEBLBKTESLIWBTTSJQFNBGJVJHXNRFHQKLRHJOBOQGBNTTVNQVYPSFYVXJKTYLFXDRVMRIZRTKRRQTWALGDRNUAXBTRBYYOX9TGMJSQV9BFZTPYMYXKIMMTHXBMDKYSBGTQAUDTIRPQLDIXEOCEUGRXQREZUIYIEEDWBFPRRKWEYT9BLFUBIBWGPIBHNTVLCQBWMW99IHSXWBDYBWUKFLMHVWSKHUPHRBV9JNHGZTWPMMCEMEBAFPQATVU9BUOPDVSPOTHNWKPZIKPGVY9FTKVSJTQYOKKAJIZEVLSCASTGRILOIKMKWEPVRZGRBSNYHCZZMZAUTNGP9OWPXNMDZNLEVBWZFASFSWOSERMNADAXBRHGIIZKLUVBPUEFVEGMCCVDDWRYLWLBOASTXTUYXXKENRXRHGFTJRBGFIXVBUPNVLRDZJMCKQFMPOEQTKSGFTMDTVIXP9LHME9XBRIDXXZNYVSMPPHBVFDPWVYZMOPSWIBTIOIDSDJSOOKJZBOMZGSJWPIJAIUYXVTGICIU9UBDPBRDSDZDTVQVXTPYTXHLSKTAUXSSQWRMHHQULPAQQNPRAGWURBKMUBYSKDPTIOSPVFFUFQC9SEWKFRKDVANAJQPGOZLKHSMBYQSWLUCODZYYBWQBUAPX9GLTPYSAEKOGBUROOHOGBWHEEGPROUPSEFVUYQSLCMOKLEXM9CQCEOECGRJMKCPYISWDAGMZUEBFYFMBZXUWYOPQSWTFMVD9ABFVCCQYTOKTGEJIFKTXAEKQXI9CELLMOFKIOEZGCQGFKAWVEBRPCEG99KP9LCBMIZFOQCVI9NPMXO9XZXUJPDNACESVMCNPNWUGAOMHDKYCWBITYJOSIPCIMFRZDKEDLDJEVRGEBDGMUTJAE9JKUYSIPUKVIGFEOTDBZTORFASTGNRFLRABOPTLH9UZVBPHSSHJOSEEKDGSBCF99ABNWK9QJSBZWVKJSFXSYNOCWEYUURVBFOBDFCNQLHQQKDOBAG9VFVCJICU9YRXZH9KPPEUBTQGUZMFDWCQWNAYBQHWVLJRTCAMZKFXNMZGDIKLLOHCTTFTBR9YKVINGSCPZNLPKTAFYGKQPMCCY9UJFIHMYHJ9ZZRWXU9AUEDCAKDJGLQ9QTZKRZGQQWYWXLXDUTVITAH9AMUNZCVPMMGQVJAFCRXFVRHB9JZRLILNBYKTFBNNWSMJCHOVMWAVLZHJMOD9ZJPR9HAWOYJJKYWSGDYFBHGMHVZEMOJNQHQKFFWVHF9UMBZ9SGXGAZXDPHUUIBXTTAHRT9LVDUGDAFLMTMI9HPWUOIRKNMLZLERKODBJTNUXFVSTWFAOXUHG9NAIMFRMZAAMNLJMVUOWCCBQBUZJEJZVAYZJEFKFLODHAPQDREMEVNZQIIFEWJVLIWLIXKSHOTAWPLTJXUMXIZRTTNLJS9MYHWGILCRXRGJIOYXOAOV9USEJ9TEKKPANKHOHDILEAGMFYI9NODDYBTPJOXJDMPHETFKDATSUHTCG9HLREDDUYSJKADJABAXQDXWW9SHITXVVANNIPVVJBGJRXQW9CIEXLKPAMUHWLJUMWZUHKOP9SFWCZABNTVRTF9NDIADOFAFXQRHTFTSWICWMXSMPJXIXYPBUNPJEPIV99ESXGBBKAERG9NWSABEGONCYFLUTLFGYFQINDSERGHODZACEXJLVGQTTIUAWQDIJJUEADGR9RVZZNYRN9MIHTLKDZGKKGQSVSJGNNEXKSLLVLFSDTGUVDMECAP9CUO9X
なにやら長い文字列が表示されたと思う。長さは4374トライト。これがアドレス生成と署名に使われるPrivate Keyの正体である。次にこのPrivate Keyのパターンを変えるindex
、長さを変えるsecurity
という引数について見てみよう。
なお今後は長すぎる文字列は...と省略して表示する。
indexとsecurityとは
index
によって変化するPrivate Keyの様子を見てみよう。
for(var i=0;i<10;i++){
// indexをインクリメントしていく。
var key_trits = Signing.key(Converter.trits(seed),i,security);
var key = Converter.trytes(key_trits);
console.log('- - - - - - - - - - - - - - - - - - - ');
console.log('index: ',i);
console.log('key length: ',key.length);
console.log('Private key: ',key);
}
- - - - - - - - - - - - - - - - - - -
index: 0
key length: 4374
Private key: UVJBFCMASCXNLDCUS9IGZSNUHZSZU9RLMLENNS...
- - - - - - - - - - - - - - - - - - -
index: 1
key length: 4374
Private key: UL9H9ZESBSPTHTSVA9KNKPTMJLCCWZVCBAUBMG...
- - - - - - - - - - - - - - - - - - -
index: 2
key length: 4374
Private key: GISSLIRLYKEFOSYQJGKYO9JPRRHJLWOEJEXBAQ...
- - - - - - - - - - - - - - - - - - -
index: 3
key length: 4374
Private key: GYMPGGFTVLZGKWVVVWJCQCQFKTP9FXIPGWAQIB...
- - - - - - - - - - - - - - - - - - -
index: 4
key length: 4374
Private key: CPOHZZUZLQAHTBDOQYNRILSULOXUGHQWACOZBU...
- - - - - - - - - - - - - - - - - - -
index: 5
key length: 4374
Private key: VJOQLHZK9F9ISXSTABBEBMGYGO9OYDURLNYIUA...
- - - - - - - - - - - - - - - - - - -
index: 6
key length: 4374
Private key: SXYIFTNTYDTRFBVVJXGRPHSLYFR9HYADYCWIQX...
- - - - - - - - - - - - - - - - - - -
index: 7
key length: 4374
Private key: H9NAZMOBNADQA9FUL9NRWQPLBLAGBGBNCSNFOV...
- - - - - - - - - - - - - - - - - - -
index: 8
key length: 4374
Private key: SZXMZTUVYGJKQB9POS9ZNLMYYHEOGKCBZHRPLL...
- - - - - - - - - - - - - - - - - - -
index: 9
key length: 4374
Private key: IWXLHGALLBLXWVOTSMCADWTAHJDPZLTZYURFJI...
このように、IOTAのPrivate Keyはindexを変化させることによって毎回違うPrivate Keyを使って署名を行う。
次にsecurity
によって変化するPrivate Keyの様子を見てみよう。デフォルトは**security = 2
**。
for(var i=0;i<3;i++){
for(var s=1;s<4;s++){
var key_trits = Signing.key(Converter.trits(seed),i,s);
var key = Converter.trytes(key_trits);
console.log('- - - - - - - - - - - - - - - - - - - ');
console.log('index: ',i);
console.log('security: ',s);
console.log('key length: ',key.length);
console.log('Private key: ',key);
}
}
- - - - - - - - - - - - - - - - - - -
index: 0
security: 1
key length: 2187
Private key: UVJBFCMASCXNLDCUS9IGZSNUHZSZU9RLMLENNSKLNDLIIWNXSFKBPEECY9CYBSHJXACOVOMGW9...
- - - - - - - - - - - - - - - - - - -
index: 0
security: 2
key length: 4374
Private key: UVJBFCMASCXNLDCUS9IGZSNUHZSZU9RLMLENNSKLNDLIIWNXSFKBPEECY9CYBSHJXACOVOMGW9...
- - - - - - - - - - - - - - - - - - -
index: 0
security: 3
key length: 6561
Private key: UVJBFCMASCXNLDCUS9IGZSNUHZSZU9RLMLENNSKLNDLIIWNXSFKBPEECY9CYBSHJXACOVOMGW9...
- - - - - - - - - - - - - - - - - - -
index: 1
security: 1
key length: 2187
Private key: UL9H9ZESBSPTHTSVA9KNKPTMJLCCWZVCBAUBMGUDBEVVJMFHOWIK9D9MZDZITCIXUEWNSMKLTD...
- - - - - - - - - - - - - - - - - - -
index: 1
security: 2
key length: 4374
Private key: UL9H9ZESBSPTHTSVA9KNKPTMJLCCWZVCBAUBMGUDBEVVJMFHOWIK9D9MZDZITCIXUEWNSMKLTD...
- - - - - - - - - - - - - - - - - - -
index: 1
security: 3
key length: 6561
Private key: UL9H9ZESBSPTHTSVA9KNKPTMJLCCWZVCBAUBMGUDBEVVJMFHOWIK9D9MZDZITCIXUEWNSMKLTD...
- - - - - - - - - - - - - - - - - - -
index: 2
security: 1
key length: 2187
Private key: GISSLIRLYKEFOSYQJGKYO9JPRRHJLWOEJEXBAQSEOPQTIUTCWGCYB9MQEJDHODLNIWRNTWCVTF...
- - - - - - - - - - - - - - - - - - -
index: 2
security: 2
key length: 4374
Private key: GISSLIRLYKEFOSYQJGKYO9JPRRHJLWOEJEXBAQSEOPQTIUTCWGCYB9MQEJDHODLNIWRNTWCVTF...
- - - - - - - - - - - - - - - - - - -
index: 2
security: 3
key length: 6561
Private key: GISSLIRLYKEFOSYQJGKYO9JPRRHJLWOEJEXBAQSEOPQTIUTCWGCYB9MQEJDHODLNIWRNTWCVTF...
このようになる。面白いのはsecurityによって全く違うPrivate Keyが生成されるのではなく、序盤は同じ文字列が生成されることだ。
アドレス生成
それではアドレスを生成してお金を受け取る用意を整えよう。
var IOTA = require('../lib/iota');
var iota = new IOTA({'host':'http://localhost','port':14265});
var seed = 'TESTABMUSHI9...';
var index = 0;
var security = 2;
// checksumを設定しないと公式ウォレットから送金できないため、checksumをtrueにしておく。
iota.api.getNewAddress(seed,{'index':index,'security':security,'checksum':true},function(error,success){
if(error){
console.log(error);
}else{
console.log('address: ',success);
}
});
address: 9JXPVTYVEYUGDACQIOZQUHRKZOVX9ONFYRHKKGRXQOHAIFDITGQXRPMBK9QE9JQSQKJWGAD9QKV9PZDNAMGERSVVSB
承認までにアドレスの残高を取得するコードを書いちゃいましょう。
アドレスの残高
var IOTA = require('../lib/iota');
var iota = new IOTA({'host':'http://localhost','port':14265});
// アドレスは配列で渡すため、複数を指定できる。
var addressArray = ['9JXPVTYVEYUGDACQIOZQUHRKZOVX9ONFYRHKKGRXQOHAIFDITGQXRPMBK9QE9JQSQKJWGAD9QKV9PZDNAMGERSVVSB'];
// 2番目の引数は100を指定。
iota.api.getBalances(addressArray,100,function(error,success){
if(error){
console.error(error);
}else{
console.log(success);
}
});
{ balances: [ '1000' ],
references: [ 'XCGTP9EXPPQDOZKUJWEEPHWUNRIKYBCPKABEMFISFJTTP9NFNM99CXANYHHAAEPRXRADHFJNRXGOZ9999' ],
milestoneIndex: 322346,
duration: 692 }
送金
さあ、受け取ったお金を送金してみよう。今回は先ほど1000iota受け取ったSeedと、これから相手役をするSeedの合計二つのSeedを使ってやり取りをする。
Transferオブジェクト
送金情報をTransferオブジェクトにまとめる。
{
address: String // 81トライトの受け取りアドレス
value: Int // 送金額(単位はiotaなので注意)
message: String // トライトにエンコードされたメッセージ
tag: String // 27トライトのタグ
}
今回は別のSeedから生成したアドレスDEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ9QJLIPGMC
に13
iota送金する。
var transfer = {
address: 'DEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ9QJLIPGMC',
value: 13,
message: '',
tag: 'ABMUSHI9TEST'
}
sendTransfer
送金のリクエスト行う関数はsendTransfer
。上記Transferオブジェクトを配列で指定するので複数の送金も一斉に行える。
var IOTA = require('../lib/iota');
var iota = new IOTA({'host':'http://localhost','port':14265});
// 送金者のSeed
var seed = 'TESTABMUSHI9...';
// 受金者のアドレス
var address = 'DEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ9QJLIPGMC';
// 送金額
var value = 13;
// Transferオブジェクトは配列で渡す。
var transfers = [{
address: address,
value: value,
message: '',
tag: 'ABMUSHI9TEST'
}]
var depth = 5; // 3~12がおすすめ。
var minWeightMagnitude = 14; // 大抵14。
// Send Transfer リクエスト
iota.api.sendTransfer(seed,depth,minWeightMagnitude,transfers,function(error,success){
if(error){
console.error(error);
}else{
console.log(success);
}
});
やはりウォレットの公開ノードより自分のノードの方が空いてるためか、送金自体は30秒もかからなかった。(ネットワークに承認されるのは時間がかかるが。)
結果返されるのは送金Bundle。Bundleの構造についてはこちらで詳しく解説したが今回の例で簡単に説明する。
BundleはTransactionオブジェクトを複数まとめたものだ。今回はmessage
が空っぽでsecurity=2
なので、
- 出力部は
13
iotaの送信先アドレスを設定するのみ。... 1つ
- 入力部は2187*security分の署名を保管する。 ... 2つ
- お釣りの
987
iotaを送金する。 ... 1つ
となる。つまり合計4つのTransactionが生成された。その4つがBundleにまとめられて返される。略図で表すと以下のように見える。
ちなみに今回の取引をIOTA searchで見てみるとこのようになっている。そして、実際返されるBundleオブジェクトは以下のようになる。
[ { hash: 'CYIAIJQJDXGLSCDPFHYWSJQUFGLAESRCJDSHYWXPEVIMJGMQFMQDOBLWN9FMKHUQVSNHTAWXHCMYA9999',
signatureMessageFragment: '999999999999999999999999999999999999999999999999999999999999999999999999999999999999999...',
address: 'DEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ',
value: 13,
obsoleteTag: 'PCMUSHI9TEST999999999999999',
timestamp: 1515388352,
currentIndex: 0,
lastIndex: 3,
bundle: 'LZVFJOUHNJKTUXAYFTILJHEAFKEGCFPEGXYEXRGIDUZT9YPHEDWF9GIOB9DCJCFZLJS9NYWUW9E9NATHC',
trunkTransaction: 'KCSCGLLFBQXXOKHDEALHTMEMLYXLBX9XRVCRQLWZIJWBBGIWYYVQTGXXUVGXYHGDBLB9LSHTDTIN99999',
branchTransaction: 'ILNMHZBRDCTNLVMBIUFMOYMNDUDCUNSLT9EVHTKWSDQBVVJIXI9B9MMCZFKR9KHVIZCBPF9INYHXZ9999',
tag: 'ABMUSHI9TEST999999999999999',
attachmentTimestamp: 1515388392080,
attachmentTimestampLowerBound: 0,
attachmentTimestampUpperBound: 12,
nonce: 'SICXTHAPZYLRFOMNIHUZONYXLCE' },
{ hash: 'KCSCGLLFBQXXOKHDEALHTMEMLYXLBX9XRVCRQLWZIJWBBGIWYYVQTGXXUVGXYHGDBLB9LSHTDTIN99999',
signatureMessageFragment: 'LPKWPRCHVOLRU9HGAMVUDROYDPVTYLAZXRRPUYG9VNEUOT9HDNWLNRQ99HIJERCXLSLXBHNPTERPYXSUXOPSWFN...',
address: '9JXPVTYVEYUGDACQIOZQUHRKZOVX9ONFYRHKKGRXQOHAIFDITGQXRPMBK9QE9JQSQKJWGAD9QKV9PZDNA',
value: -1000,
obsoleteTag: 'ABMUSHI9TEST999999999999999',
timestamp: 1515388357,
currentIndex: 1,
lastIndex: 3,
bundle: 'LZVFJOUHNJKTUXAYFTILJHEAFKEGCFPEGXYEXRGIDUZT9YPHEDWF9GIOB9DCJCFZLJS9NYWUW9E9NATHC',
trunkTransaction: 'FLMLQQBCNE9LTFBVDSLUIERHFYCGCMQDBOPUJGERVAFSRHBPLSTHKMRLHWZOTZTBJFYEGHYJBTKUZ9999',
branchTransaction: 'ILNMHZBRDCTNLVMBIUFMOYMNDUDCUNSLT9EVHTKWSDQBVVJIXI9B9MMCZFKR9KHVIZCBPF9INYHXZ9999',
tag: 'ABMUSHI9TEST999999999999999',
attachmentTimestamp: 1515388391756,
attachmentTimestampLowerBound: 0,
attachmentTimestampUpperBound: 12,
nonce: 'SSWEGXB9NLIMCBQJAIP9OQMXVGE' },
{ hash: 'FLMLQQBCNE9LTFBVDSLUIERHFYCGCMQDBOPUJGERVAFSRHBPLSTHKMRLHWZOTZTBJFYEGHYJBTKUZ9999',
signatureMessageFragment: '999ORQLZJEUZWPOBHUJR9AXLKFZSDFTQJBNJG9NL9SZSMIRZMNYLMEVQDOGQMEUWRNAXMENENFRNTSVYWIHFUPM...',
address: '9JXPVTYVEYUGDACQIOZQUHRKZOVX9ONFYRHKKGRXQOHAIFDITGQXRPMBK9QE9JQSQKJWGAD9QKV9PZDNA',
value: 0,
obsoleteTag: 'ABMUSHI9TEST999999999999999',
timestamp: 1515388357,
currentIndex: 2,
lastIndex: 3,
bundle: 'LZVFJOUHNJKTUXAYFTILJHEAFKEGCFPEGXYEXRGIDUZT9YPHEDWF9GIOB9DCJCFZLJS9NYWUW9E9NATHC',
trunkTransaction: 'OMJNSTWEEKLSQNVTLFGKM9MYIFEALDJPQEKEW9NSIKORLNHGTCIWQFODM9BZNHIKMQDGXIMGWKRPA9999',
branchTransaction: 'ILNMHZBRDCTNLVMBIUFMOYMNDUDCUNSLT9EVHTKWSDQBVVJIXI9B9MMCZFKR9KHVIZCBPF9INYHXZ9999',
tag: 'ABMUSHI9TEST999999999999999',
attachmentTimestamp: 1515388385850,
attachmentTimestampLowerBound: 0,
attachmentTimestampUpperBound: 12,
nonce: 'BVGHTNVKWSHZGLAMXZTPBMIIEFG' },
{ hash: 'OMJNSTWEEKLSQNVTLFGKM9MYIFEALDJPQEKEW9NSIKORLNHGTCIWQFODM9BZNHIKMQDGXIMGWKRPA9999',
signatureMessageFragment: '999999999999999999999999999999999999999999999999999999999999999999999999999999999999999...',
address: 'NCIYBEMWZFBQHVEISSRJUCQDNGJFKZTWP9SLCFSBRWEYY9ESAXNTP9WDVXWJFGIIAVQKRGO9TRZFWVTRA',
value: 987,
obsoleteTag: 'ABMUSHI9TEST999999999999999',
timestamp: 1515388358,
currentIndex: 3,
lastIndex: 3,
bundle: 'LZVFJOUHNJKTUXAYFTILJHEAFKEGCFPEGXYEXRGIDUZT9YPHEDWF9GIOB9DCJCFZLJS9NYWUW9E9NATHC',
trunkTransaction: 'ILNMHZBRDCTNLVMBIUFMOYMNDUDCUNSLT9EVHTKWSDQBVVJIXI9B9MMCZFKR9KHVIZCBPF9INYHXZ9999',
branchTransaction: '9ZFUVLAXCKNVUHLUZJADZWMAIZEKMKVWYTFUUEQJWKSHAFHGKLSYRPMAQXJJIQQIK9TGWBJDAZIKA9999',
tag: 'ABMUSHI9TEST999999999999999',
attachmentTimestamp: 1515388380939,
attachmentTimestampLowerBound: 0,
attachmentTimestampUpperBound: 12,
nonce: 'VGDCYEFCKKSNMRLJFQKDSTFKIKG' } ]
受け取り主の残高は増えたか?
さて、上の13iotaの送金が承認されるまでだいたい30分かかったところで、まず着金を確認してみよう。アドレスをDEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ9QJLIPGMC
に設定してgetBalance
してみよう。
{ balances: [ '13' ],
references: [ 'KVZNXXBQSUVLEOVSKSQLJEVOJWVBIIFTOPUBHFJ9YOTFYZWLEHMPLWRYGKMSTMVCZ9ZXCZWXLNAIZ9999' ],
milestoneIndex: 322415,
duration: 578 }
ちゃんと13
iota分、着金している。
送り主の残高は減ったか?
次に送り主のアドレスの残高を確認してみよう。
{ balances: [ '0' ],
references: [ 'CDKOPNULQTJZYVCNAZEKVPJNMP9YKMKPNQXRELCDMPVIRNAPNHHNBJUQD9MAVPASABNGBJQQXYWOA9999' ],
milestoneIndex: 322384,
duration: 452 }
おや?13
だけしか送っていないのに残高がゼロ。1000-13 = 987
のはずなのにおかしい!と思うかもしれないが実はこれが正しい。というのも、IOTAではお釣り=987
は新しいアドレスに送金されるからだ。新しいアドレスとは、index
を+1した結果生成されるアドレスのことだ。コードで見てみよう。
var IOTA = require('../lib/iota');
var iota = new IOTA({'host':'http://localhost','port':14265});
var seed = 'TESTABMUSHI9...';
// indexを+1。(さっきは0だった)
var index = 1;
// securityは変えない。
var security = 2;
iota.api.getNewAddress(seed,{'index':index,'security':security,'checksum':true},function(error,success){
if(error){
console.log(error);
}else{
console.log('address: ',success);
}
});
address: VLRVBXGUTJCXPTJARJBOE9SRIPHDULMUGITHVDOXYBYCJRRHUKDXSDTLKRJMLISJPBXWCZJYAP9VGOCPAJEZOPAFYY
ここで問題が発生した。このアドレスの残高を調べるとゼロなのだ。ソースコードを調べてみると、どうやらgetNewAddress
のindex
に指定する整数は、今までアドレスが使われたことがあるかを検索し始めるindexの検索開始地点であることが分かった。また、調べると上のアドレスVLRVBXGUTJCXPTJAR...
はindex=2
のアドレスだと判明した。なるほど、確かにindex=2
はまだ使われていない。
ということでもう使われたindex=1
のアドレスを取得するためには、自作でgetAddress
という関数を作らなければならなくなった。アドレスの生成方法はこの記事でも詳しく解説した。以下は略図である。
コードにすると以下のようになる。
// アドレス生成はオフラインでできる。(APIはいらない)
// var IOTA = require('../lib/iota');
var Signing = require('../lib/crypto/signing/signing');
var Converter = require('../lib/crypto/converter/converter');
// アドレス生成はオフラインでできる。(APIはいらない)
// var iota = new IOTA({'host':'http://localhost','port':14265});
// アドレス生成関数。
var getAddress = function(seed,index,security){
// Private Keyを得る。
var key_trits = Signing.key(Converter.trits(seed),index,security);
// digestを得る。
var digest_trits = Signing.digests(key_trits);
// addressを得る。
var address_trits = Signing.address(digest_trits);
var address = Converter.trytes(address_trits);
return address;
}
var seed = 'TESTABMUSHI9...';
var index = 1;
var security = 2;
// 実行
var address = getAddress(seed,index,security);
// 表示
console.log('address: ',address);
address: NCIYBEMWZFBQHVEISSRJUCQDNGJFKZTWP9SLCFSBRWEYY9ESAXNTP9WDVXWJFGIIAVQKRGO9TRZFWVTRA
この新しいアドレスに対してgetBalances
する。
{ balances: [ '987' ],
references: [ 'PXE9OCISTJHDCYPHVMWUBAWCUDQDTHBXSJOCP99IUCONDGS9OQTGOBJIFUXHOAGTKEJZJNBOHOIRA9999' ],
milestoneIndex: 322389,
duration: 478 }
ついに、求めていた987
が入ったアドレスにたどり着いた。実はこのアドレスNCIYBEMWZ...
だが、先ほどsendTranfer
が返したBundle内の最後のTransaction(currentIndex=3)のaddress
を見れば見つかる。というのも、この987
iotaはお釣りなのでBundleの差額出力部でお釣りの送金先を指定しているからだ。(*差額出力部とは?)
何はともあれ送金が上手くいったことが確認できた。
アカウント残高計算
さて、IOTAではアカウント自体の残高はどうやって求めているのだろう?前章で苦戦したように、今まで使ったアドレス全てを調べて合計を求めないとアカウントの残高(=ウォレットに表示される残高)は求められない。
アドレス連続生成し合計計算
ということで、とりあえず5個(index
の0~4)アドレスを生成してそれぞれの残高を合計しよう。
var IOTA = require('../lib/iota');
var Signing = require('../lib/crypto/signing/signing');
var Converter = require('../lib/crypto/converter/converter');
var iota = new IOTA({'host':'http://localhost','port':14265});
var getAddress = function(seed,index,security){
var key_trits = Signing.key(Converter.trits(seed),index,security);
var digest_trits = Signing.digests(key_trits);
var address_trits = Signing.address(digest_trits);
var address = Converter.trytes(address_trits);
return address;
}
// アドレスを配列で受け取って残高を表示する。
var getBalance = function(addressArray){
iota.api.getBalances(addressArray,100,function(error,success){
if(error){
console.error(error);
}else{
var list = success.balances;
var total = 0;
for(i=0;i<list.length;i++){
var balance = parseInt(list[i]);
console.log('- - - - - - - - - - - - - - - - - - - -');
console.log('index: ',i);
console.log('address: ',addressArray[i]);
console.log('balance: ',balance);
total += balance;
}
console.log('- - - - - - - - - - - - - - - - - - - -');
console.log('account balance: ',total);
}
});
}
var seed = 'TESTABMUSHI9...';
var security = 2;
var addressArray = [];
var maxIndex = 5; // 今回は5つ生成する(0~4)。
// アドレス連続生成
for(var i = 0;i<maxIndex;i++){
address = getAddress(seed,i,security); // index = i のアドレス
addressArray.push(address);
}
// 残高
getBalance(addressArray);
- - - - - - - - - - - - - - - - - - - -
index: 0
address: 9JXPVTYVEYUGDACQIOZQUHRKZOVX9ONFYRHKKGRXQOHAIFDITGQXRPMBK9QE9JQSQKJWGAD9QKV9PZDNA
balance: 0
- - - - - - - - - - - - - - - - - - - -
index: 1
address: NCIYBEMWZFBQHVEISSRJUCQDNGJFKZTWP9SLCFSBRWEYY9ESAXNTP9WDVXWJFGIIAVQKRGO9TRZFWVTRA
balance: 987
- - - - - - - - - - - - - - - - - - - -
index: 2
address: VLRVBXGUTJCXPTJARJBOE9SRIPHDULMUGITHVDOXYBYCJRRHUKDXSDTLKRJMLISJPBXWCZJYAP9VGOCPA
balance: 0
- - - - - - - - - - - - - - - - - - - -
index: 3
address: APCMI9ESIIGEUVKLJHHHGWBXEAFHJXFCLLZD9GZBZC9LIXD9RMWUDRYLUAJATQJDJVHOULTBDHVD9DIVD
balance: 0
- - - - - - - - - - - - - - - - - - - -
index: 4
address: 99OEIIKAHOHWSXMBAEGYJFFLDAGMDKHKXTYFJDWJKXIGIEFJZKMFSEZTVMGY9TXGXFWWDXZOWAYKPKBYW
balance: 0
- - - - - - - - - - - - - - - - - - - -
account balance: 987
簡単な方法
さて、ここでアドレスを何個生成すれば良いかという問題が起きる。その数が足りないと、表示されるアカウントの残高も足りなくなる。先ほどの例だと5つ検索にかけたが、もしかするとindex=6
などにも残高が残っているかもしれないのだ。そこで、公式APIにgetAccountData
というものがあるのでこれを使ってみよう。
var IOTA = require('../lib/iota');
var iota = new IOTA({'host':'http://localhost','port':14265});
var seed = 'TESTABMUSHI9...';
// seedを与えるだけ。
iota.api.getAccountData(seed,function(error,success){
if(error){
console.error(error);
}else{
console.log(success);
}
});
{ latestAddress: 'VLRVBXGUTJCXPTJARJBOE9SRIPHDULMUGITHVDOXYBYCJRRHUKDXSDTLKRJMLISJPBXWCZJYAP9VGOCPA',
addresses:
[ '9JXPVTYVEYUGDACQIOZQUHRKZOVX9ONFYRHKKGRXQOHAIFDITGQXRPMBK9QE9JQSQKJWGAD9QKV9PZDNA',
'NCIYBEMWZFBQHVEISSRJUCQDNGJFKZTWP9SLCFSBRWEYY9ESAXNTP9WDVXWJFGIIAVQKRGO9TRZFWVTRA' ],
transfers:
[ [ [Object], [Object], [Object], [Object] ],
[ [Object], [Object], [Object], [Object] ],
[ [Object], [Object], [Object], [Object] ],
[ [Object], [Object], [Object], [Object] ] ],
inputs:
[ { address: 'NCIYBEMWZFBQHVEISSRJUCQDNGJFKZTWP9SLCFSBRWEYY9ESAXNTP9WDVXWJFGIIAVQKRGO9TRZFWVTRA',
keyIndex: 1,
security: 2,
balance: 987 } ],
balance: 987 }
このように最終的なbalance
を自動的に持ってきてくれる便利なAPIだ。
参考文献
公式のREADME
公式APIソース:iota.lib.js/lib/api/api.js
最後に
今回の記事はソースコードだらけになってしまったが、実際のコードをテストしてみるうちに、IOTAのいいところは送金手数料がゼロだと今更気づいた。
特に今回は1000iota(=0.5円)くらいを移動させた。メインネットでテストしたいけど、怖いから少額取引でテストをしたいと考えたときに手数料がかからないというのは大変都合が良い。もし、これがビットコインやEthereumなら手数料が高すぎてこんな記事のために実際のお金を動かせない。
こんなことを思ったのも、**暗号通貨バブルはイノベーションを絞め殺している**という記事を読んでからだ。引用すると、
Ethereumがトークン化のコストを劇的に下げる方法を考え出せば別だ。もちろん実験的サービスは多数作られてはいる。しかしほどんど誰も利用しない。こうしたサービスには好奇心の強いユーザーが近づいてみるものの、一回限りの実験にしても高すぎる手数料に驚かされている。まして日常利用するようなことにはならない。結果として、暗号通貨テクノロジーを利用する実験もイノベーションも投機バブルが破裂するまでは一時停止状態だ。
とあるように、今バブル状態にある仮想通貨は、そんな異常から離れた日常と相容れない。しかし、仮想通貨が目指すのは、日常のなかの支払いである。この矛盾をどう解消していくか。
仮想通貨という壮大になりつつある社会実験の行く末はいかに...
ではまた。