JavaScript
Node.js
IoT
仮想通貨
iota

IOTA:【実践】APIでウォレットを実装。受送金と残高計算。

 フルノードを最新版v1.4.1.6にしてから調子が上がってきたのでAPIを本格的にいじってみた。今までの記事とは違って、実践的な記事にしていきたい。

環境構築

 大きく分けて2つ環境構築のステップがある。

  1. フルノード構築
  2. ライブラリ用意

 このうち一番厄介なのは1のフルノード構築だ。2に関しては、今回はJavascriptのNode.jsを使う。

フルノード構築

 フルノードを運用するために必要な手順は以下の記事で簡単にまとめたので参照してほしい。

IOTA:【随時更新】最速でフルノードを立ち上げる。

 さて、フルノードを立ち上げてもAPIリクエストするためには、フルノードを同期させるところまで完了する必要がある。フルノードの同期とは、

  1. latestMilestoneIndexlatestSolidSubtangleMilestoneIndexのフィールドの数字が同じである。
  2. この数字がSlackの#botboxチャンネルで更新されるマイルストーン番号と一致している。

 この手順が厄介であるがゆえに、多くの人がIOTAに手を付けずにいると筆者は考えている。

注)同期させなくても実行できるAPIリクエストはあるが、送金受金などお金をやり取りするAPIは同期なしには実行できない。本記事はウォレットを模倣することを目的にしているため、あくまでフルノード同期を前提に話を進めていく。

ライブラリの用意

 フルノードを走らせているマシンにライブラリをインストールしよう。今回はUbuntuのマシンを前提に話を進めていく。

Node.jsのインストール

Node.jsのインストール
sudo apt-get update
sudo apt-get install nodejs

npmのインストール

npmのインストール
sudo apt-get install npm

IOTAのライブラリ

https://github.com/iotaledger/iota.lib.js

iota.lib.jsのインストール
npm install iota.lib.js

 ここまでペタペタペーストでささっと準備完了だ。フルノード立ち上げまでが環境構築の難所だった。

基本API

 まずAPIをマシンが認識して正しく動くかの確認のためにgetNodeInfoリクエストで、自分のフルノードのステータスを表示させてみよう。
 コードの保存ディレクトリだが、~/node_modules/iota.lib.js/examples/を使っていく。

basic.js
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が内部で自動にやってくれるのでこのステップは必要ない。

privateKey.js
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の様子を見てみよう。

indexテスト
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

securityテスト
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

とりあえず今は公式ウォレットから送金します。
send.png

承認までにアドレスの残高を取得するコードを書いちゃいましょう。

アドレスの残高

残高
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オブジェクトにまとめる。

Transferオブジェクト
{
    address: String // 81トライトの受け取りアドレス
    value: Int      // 送金額(単位はiotaなので注意)
    message: String // トライトにエンコードされたメッセージ
    tag: String     // 27トライトのタグ
}

今回は別のSeedから生成したアドレスDEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ9QJLIPGMC13iota送金する。

var transfer = {
    address: 'DEFBQVKHMDUDBDZBPBPM9ZUPPDSPEXMCQJWQTABHQUHMVXXMDBPTNBIZNLIQEEYOBMUKENQRUOSZZJZOZ9QJLIPGMC',
    value: 13,
    message: '',
    tag: 'ABMUSHI9TEST'
}

sendTransfer

 送金のリクエスト行う関数はsendTransfer。上記Transferオブジェクトを配列で指定するので複数の送金も一斉に行える。

send.js
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なので、

  1. 出力部は13iotaの送信先アドレスを設定するのみ。... 1つ
  2. 入力部は2187*security分の署名を保管する。 ... 2つ
  3. お釣りの987iotaを送金する。 ... 1つ

となる。つまり合計4つのTransactionが生成された。その4つがBundleにまとめられて返される。略図で表すと以下のように見える。
bundle.png
 ちなみに今回の取引をIOTA searchで見てみるとこのようになっている。そして、実際返されるBundleオブジェクトは以下のようになる。

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してみよう。

getBalanceの結果
{ balances: [ '13' ],
  references: [ 'KVZNXXBQSUVLEOVSKSQLJEVOJWVBIIFTOPUBHFJ9YOTFYZWLEHMPLWRYGKMSTMVCZ9ZXCZWXLNAIZ9999' ],
  milestoneIndex: 322415,
  duration: 578 }

ちゃんと13iota分、着金している。

送り主の残高は減ったか?

 次に送り主のアドレスの残高を確認してみよう。

アドレス残高
{ balances: [ '0' ],
  references: [ 'CDKOPNULQTJZYVCNAZEKVPJNMP9YKMKPNQXRELCDMPVIRNAPNHHNBJUQD9MAVPASABNGBJQQXYWOA9999' ],
  milestoneIndex: 322384,
  duration: 452 }

 おや?13だけしか送っていないのに残高がゼロ。1000-13 = 987のはずなのにおかしい!と思うかもしれないが実はこれが正しい。というのも、IOTAではお釣り=987は新しいアドレスに送金されるからだ。新しいアドレスとは、index+1した結果生成されるアドレスのことだ。コードで見てみよう。

nextAddress.js
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

 ここで問題が発生した。このアドレスの残高を調べるとゼロなのだ。ソースコードを調べてみると、どうやらgetNewAddressindexに指定する整数は、今までアドレスが使われたことがあるかを検索し始めるindexの検索開始地点であることが分かった。また、調べると上のアドレスVLRVBXGUTJCXPTJAR...index=2のアドレスだと判明した。なるほど、確かにindex=2はまだ使われていない。
 ということでもう使われたindex=1のアドレスを取得するためには、自作でgetAddressという関数を作らなければならなくなった。アドレスの生成方法はこの記事でも詳しく解説した。以下は略図である。
address_gen.png
 コードにすると以下のようになる。

getAddress.js
// アドレス生成はオフラインでできる。(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を見れば見つかる。というのも、この987iotaはお釣りなのでBundleの差額出力部でお釣りの送金先を指定しているからだ。(*差額出力部とは?
 何はともあれ送金が上手くいったことが確認できた。

アカウント残高計算

 さて、IOTAではアカウント自体の残高はどうやって求めているのだろう?前章で苦戦したように、今まで使ったアドレス全てを調べて合計を求めないとアカウントの残高(=ウォレットに表示される残高)は求められない。

アドレス連続生成し合計計算

 ということで、とりあえず5個(indexの0~4)アドレスを生成してそれぞれの残高を合計しよう。

balance.js
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というものがあるのでこれを使ってみよう。

account.js
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がトークン化のコストを劇的に下げる方法を考え出せば別だ。もちろん実験的サービスは多数作られてはいる。しかしほどんど誰も利用しない。こうしたサービスには好奇心の強いユーザーが近づいてみるものの、一回限りの実験にしても高すぎる手数料に驚かされている。まして日常利用するようなことにはならない。結果として、暗号通貨テクノロジーを利用する実験もイノベーションも投機バブルが破裂するまでは一時停止状態だ。

とあるように、今バブル状態にある仮想通貨は、そんな異常から離れた日常と相容れない。しかし、仮想通貨が目指すのは、日常のなかの支払いである。この矛盾をどう解消していくか。
 仮想通貨という壮大になりつつある社会実験の行く末はいかに...

 ではまた。