mpurse, mpchain, counterParty, counterBlock APIを使えば送金できるはず。その方法を調査してみる。
結論
トランザクション作成APIと思しきcreate_sendをmpurse経由で実行するもエラーに阻まれてしまった。
const cpParams = {
source: 'MEHCqJbgiNERCH3bRAtNSSD9uxPViEX1nu',
destination: 'MEHCqJbgiNERCH3bRAtNSSD9uxPViEX1nu',
asset: 'MONA',
quantity: 0.00000000,
memo: null,
memo_is_hex: false,
fee_per_kb: 0,
allow_unconfirmed_inputs: true,
extended_tx_info: true,
disable_utxo_locks: true,
};
await window.mpurse.counterParty('create_send', cpParams);
//const unspentTxouts = await window.mpurse.counterParty('create_send', cpParams);
実行したら以下のように怒られた。
{
"code": -32000,
"data": {
"args": [
"{\"message\": \"Error composing send transaction via API: Destination output is dust.\", \"code\": -32001}"
],
"message": "{\"message\": \"Error composing send transaction via API: Destination output is dust.\", \"code\": -32001}",
"type": "Exception"
},
"message": "Server error"
}
原因不明。以下URLによると手数料が少なすぎるせいかもしれない。仮にそうだとして、公式サイトなど信頼できる情報源からの情報を見つけられなかった。私がちゃんと読めてないのだろう。
おそらくmpchainなりcounterPartyなりが最低手数料を算出しており、それより低いときのリクエストを弾いているのだと思われる。その値がわかれば解決するかもしれないが、どこにその情報があるかわからない。
送金APIについては実際に所持金が減るので、諸々ちゃんと把握してから実行したい。APIテスト環境でもあれば触りながら理解を深められそうだが、そういったものも見つけられなかった。自分のマシンにフルノードをインストールし、そのコードを解析すれば理解できると踏んでいるが、壮大すぎて読み解けると思えない。MasteringBitcoinも読了していないので、今の私にはまだ早すぎたのかもしれない。
送金する方法
MasteringBitcoin日本語訳PDFを少し読んだことで、送金する仕組みを以下のように理解した。
- 支払先アドレス、支払元アドレス、アセット(MONA)、支払額、手数料、メモを決める
- 支払額+手数料以上の額になるようUTXO(未使用アウトプットトランザクションデータ)を取得する
- トランザクションを作成する
- UXTOをvin(入力)として設定する
- 支払をvout(出力)として設定する
- おつりをvout(出力)として設定する
- トランザクションを送信する
- 未処理トランザクション用プールに入る
- マイナーがそれを見つける
- 手数料が一定値ならマイニングする
- ブロックチェーンに取り込まれる
- 未承認から承認済みトランザクションになり支払確定する
予想
問題は、これをどのように実行するか。とりあえず実際に自分で使い、支払った実績のあるmpurseのソースコードを解析すれば、送金する方法がわかるはず。たぶんその中でmpchain, counterBlock, counterParty APIを実行することで送金を実現していると思われる。つまり本体はWebAPIのはず。
目標
送金するAPIはどれで、どのような引数や戻り値か。HTTPヘッダはどうすればいいか。などの情報を知りたい。最終的にJavaScriptのfetch APIでそれをリクエストし、mpurseなしでモナコインの送金を実現したい。
API 物色
mpurse sendAsset
mpurse sendAsset APIを使えば送金できる。Mpurseを使ったモナコイン送金ボタンを生成するツールを作ったとき、まさにこのAPIを使った。
たしかに送金はできるが、これはUIやトランザクション手数料の計算が固定されている。もっと自分好みにカスタマイズしたい。そこで送金処理だけを抽出したいと思ったのが今回の動機である。
mpurse sendRawTransaction
MasteringBitcoin日本語訳PDFにより、送金とはトランザクション情報の送信であると知った。なら、mpurse sendRawTransaction APIで送金できるのでは?
READMEにはAPI以外の情報がなかったので「sendRawTransaction」でググってみると以下がヒット。別のコイン(イーサリアム)の情報っぽい。
署名済みのトランザクションを送信します
ということらしい。
いまだに署名とやらをちゃんと理解できていないのだが、文脈から察するに、送信する前に署名が必要らしい。
そこでREADMEでAPIをさがしてみるとmpurse signRawTransactionがあった。
const signedTx = await window.mpurse.signRawTransaction(tx);
const txHash = await window.mpurse.sendRawTransaction(signedTx);
mpurse sendRawTransactionで返されたtxHash
をtrezor txのURLの末尾につければ、そのトランザクションの情報がゲットできる、のかな?
問題はmpurse signRawTransactionの引数tx
に何を渡せばいいのか。無情にもREADMEにはtx: string
としか書いていない。ならその文字列に何を渡せばいいの? ググったページには以下のように書いてあった。
...
var rawTx = {
nonce: '0x00',
gasPrice: '0x09184e72a000',
gasLimit: '0x2710',
to: '0x0000000000000000000000000000000000000000',
value: '0x00',
data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057'
}
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();
web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), function(err, hash) {
...
rawTx
というオブジェクトをTx
なるクラスに渡し、sign
メソッドにかけ、さらにserialize
した結果を16進数化している、のかな?
何がなんだかさっぱり。少なくともこのコードをそのまま使うことはできなさそう。たぶんこのうちいくつかはmpurse signRawTransactionとmpurse sendRawTransactionで実現できると思いたい。ただ、トランザクション情報を作成するとき、どんな情報をどのように与えればいいかさっぱりわからない。具体的にいうとmpurse signRawTransactionの引数tx
がわからない。
これならmpurse sendAssetのソースコードを読んだほうがいいかもしれない。
一応ほかにも以下のようなサイトはあった。別のコイン(イーサリアム)の情報だった。やはりgasPrice
などの情報を入れている。
驚くほど情報が少ない。ググり方が悪いのかな?
counterParty API
send
をキーワードに検索してみた。すると以下のようなコード例があった。
def do_send(source, destination, asset, quantity, fee, encoding):
validateaddress = bitcoin_api('validateaddress', [source])
assert validateaddress['ismine']
pubkey = validateaddress['pubkey']
unsigned_tx = counterparty_api('create_send', {'source': source, 'destination': destination, 'asset': asset, 'quantity': quantity, 'pubkey': pubkey, 'allow_unconfirmed_inputs': True})
signed_tx = bitcoin_api('signrawtransaction', [unsigned_tx])['hex']
tx_hash = bitcoin_api('sendrawtransaction', [signed_tx])
return tx_hash
- create_send APIで
signrawtransaction
APIの引数を作成する signrawtransaction
APIでsendrawtransaction
の引数を作成する
ということらしい。つまり、以下のような流れかな?
- create_send APIを実行する
- 1の戻り値をmpurse signRawTransactionの引数として渡して実行する
- 2の戻り値をmpurse sendRawTransactionの引数として渡して実行する
- 以上で送金完了
- 3の戻り値をtrezor txのURL末尾にして実行すると送金トランザクション情報を入手できる
最初のcreate_sendをみてみよう。
create_send(source, destination, asset, quantity)
引数 | 型 | 概要 |
---|---|---|
source | string | The address that will be sending (must have the necessary quantity of the specified asset). |
destination | (string, array[string]) | The address to receive the asset. |
asset | (string, array[string]) | The asset or subasset to send. |
quantity | (integer, array[integer]) | The quantities of the asset to send. |
memo | (string, optional) | The Memo associated with this transaction. |
memo_is_hex | (boolean, optional) | If this is true, interpret the memo as a hexadecimal value. Defaults to false. |
use_enhanced_send | (boolean, optional) | If this is false, the construct a legacy transaction sending bitcoin dust. Defaults to true. |
さらに以下も参照しろとのこと。
するとアホみたいに大量の引数らしきものが。
引数 | 型 | 概要 |
---|---|---|
encoding | (string) | The encoding method to use, see transaction encodings for more info. |
pubkey | (string/list) | The hexadecimal public key of the source address |
allow_unconfirmed_inputs | (boolean) | Set to true to allow this transaction to utilize unconfirmed UTXOs as inputs. Defaults to false. |
fee | (integer) | If you’d like to specify a custom miners’ fee, specify it here |
fee_per_kb | (integer) | The fee per kilobyte of transaction data constant that the server uses when deciding on the dynamic fee to use |
fee_provided | (integer) | If you would like to specify a maximum fee |
custom_inputs | (list) | Use only these specific UTXOs as inputs for the transaction being created. If specified, this parameter is a list of |
unspent_tx_hash | (string) | When compiling the UTXOs to use as inputs for the transaction being created, only consider unspent outputs from this specific transaction hash. Defaults to null to consider all UTXOs for the address. Do not use this parameter if you are specifying custom_inputs. |
regular_dust_size | (integer) | Specify |
multisig_dust_size | (integer) | Specify |
dust_return_pubkey | (string) | The dust return pubkey is used in multi-sig data outputs |
disable_utxo_locks | (boolean) | By default, UTXO’s utilized when creating a transaction are “locked” for a few seconds, to prevent a case where rapidly generating create_ calls reuse UTXOs due to their spent status not being updated in bitcoind yet. Specify true for this parameter to disable this behavior, and not temporarily lock UTXOs. |
op_return_value | (integer) | The value |
extended_tx_info | (boolean) | When this is not specified or false, the create_ calls return only a hex-encoded string. If this is true, the create_ calls return a data object with the following keys: tx_hex, btc_in, btc_out, btc_change, and btc_fee. |
p2sh_pretx_txid | (string) | The previous transaction txid for a two part P2SH message. This txid must be taken from the signed transaction. |
create_send APIにはそれらを受け取る引数がなさそうに見えるけど。どういうこと?
counterBlock APIのほうはsend
でググってもヒットしなかった。サイトから得られる情報はこんなところが限界か。
コードリーディング
mpurseのコードからsendをキーワードに検索してみた。
src/app/components/send/send.component.ts
送金フォームの送金ボタンを押したとき、トランザクションデータを作成する処理と思われる。が、さっぱりわからない。
createSend
のようなそれっぽいキーワードがあるので関係あるはず。
createSendObservable(disableUtxoLocks: boolean): Observable<any> {
return this.backgroundService.getAsset(this.assetControl.value).pipe(
flatMap(assetInfo => {
let amount: number;
if (assetInfo['divisible']) {
amount = new Decimal(this.amountControl.value)
.times(new Decimal(100000000))
.toNumber();
} else {
amount = new Decimal(this.amountControl.value).toNumber();
}
return this.backgroundService.createSend(
this.fromControl.value,
this.toControl.value,
this.assetControl.value,
amount,
this.memoTypeControl.value === 'no'
? ''
: this.memoValueControl.value,
this.memoTypeControl.value === 'hex',
new Decimal(this.feeControl.value)
.times(new Decimal(1000))
.toNumber(),
disableUtxoLocks
);
})
);
}
createSend(): void {
this.unsignedTx = '';
this.calculatedFee = 0;
this.createSendObservable(true).subscribe({
next: result => {
this.zone.run(() => {
this.unsignedTx = result.tx_hex;
this.calculatedFee = result.btc_fee;
});
},
error: error => {
this.zone.run(() => {
this.snackBar.open(this.backgroundService.interpretError(error), '', {
duration: 3000
});
});
}
});
}
send(): void {
if (this.request) {
this.createSendObservable(false)
.pipe(
flatMap(result => this.backgroundService.send(result.tx_hex)),
flatMap(result =>
this.backgroundService.shiftRequest(true, this.id, {
txHash: result.tx_hash
})
)
)
.subscribe({
next: () => this.backgroundService.closeWindow(),
error: error =>
this.zone.run(() =>
this.snackBar.open(error.toString(), '', { duration: 3000 })
)
});
} else {
this.createSendObservable(false)
.pipe(flatMap(result => this.backgroundService.send(result.tx_hex)))
.subscribe({
next: result => {
this.zone.run(() => {
this.snackBar.open(
this.translate.instant('send.sent') + result.tx_hash,
'',
{ duration: 5000, panelClass: 'break-all' }
);
this.router.navigate(['/home']);
});
},
error: error => {
this.zone.run(() => {
this.snackBar.open(
this.backgroundService.interpretError(error),
'',
{ duration: 3000 }
);
});
}
});
}
}
src/app/services/background.service.ts createSend
createSend(
source: string,
destination: string,
asset: string,
quantity: number,
memo: string,
memoIsHex: boolean,
feePerKb: number,
disableUtxoLocks: boolean
): Observable<any> {
return this.getBackground().pipe(
flatMap(bg =>
from<Observable<any>>(
bg.createSend(
source,
destination,
asset,
quantity,
memo,
memoIsHex,
feePerKb,
disableUtxoLocks
)
)
)
);
}
おや、この引数の感じ、どこかで見覚えが。counterPartyのcreate_sendがこんな感じだったような。
extension_scripts/background.ts createSend
createSend(
source: string,
destination: string,
asset: string,
quantity: number,
memo: string,
memoIsHex: boolean,
feePerKb: number,
disableUtxoLocks: boolean
): Promise<any> {
return MpchainUtil.createSend(
source,
destination,
asset,
quantity,
memo,
memoIsHex,
feePerKb,
disableUtxoLocks
);
}
}
mpchain APIで実行しているっぽい。でもドキュメントにはそれっぽいAPIがない。コードを追ってみよう。
extension_scripts/util.mpchain.ts createSend
static createSend(
source: string,
destination: string,
asset: string,
quantity: number,
memo: string,
memoIsHex: boolean,
feePerKb: number,
disableUtxoLocks: boolean
): Promise<any> {
const cpParams = {
source: source,
destination: destination,
asset: asset,
quantity: quantity,
memo: memo,
memo_is_hex: memoIsHex,
fee_per_kb: feePerKb,
allow_unconfirmed_inputs: true,
extended_tx_info: true,
disable_utxo_locks: disableUtxoLocks
};
return this.cp('create_send', cpParams);
}
counterParty APIで実行しているっぽい。これが本体か。
extension_scripts/background.ts send
async send(tx: string): Promise<any> {
if (!this.isUnlocked) {
this.resetPreferences();
this.resetKeyring();
Promise.reject('Not logged in');
} else {
const hex = await this.keyring.signTransaction(
tx,
this.preferences.selectedAddress
);
return MpchainUtil.sendTx(hex);
}
}
mpchain APIのsend_tx
を使っているっぽい。たぶん引数はcreateSend
の戻り値だろう。
どうすればいいのか
counterPartyのcreate_send(mpurseのcreateSend
)してからmpchainのsend_tx
(mpurseのsend
)することで送金を実現しているような気がする。
まずはcreate_sendのほうから試してみよう。mpurse API経由で実行すると楽。以下のようにやってみた。
ytyaruのプロフィールのようなHTTPSページでデベロッパツールを開き、コンソールに以下コードを入力することで実行した。
const cpParams = {
source: 'MEHCqJbgiNERCH3bRAtNSSD9uxPViEX1nu',
destination: 'MEHCqJbgiNERCH3bRAtNSSD9uxPViEX1nu',
asset: 'MONA',
quantity: 0.00000000,
memo: null,
memo_is_hex: false,
fee_per_kb: 0,
allow_unconfirmed_inputs: true,
extended_tx_info: true,
disable_utxo_locks: true,
};
await window.mpurse.counterParty('create_send', cpParams);
//const unspentTxouts = await window.mpurse.counterParty('create_send', cpParams);
実行したら以下のように怒られた。
{
"code": -32000,
"data": {
"args": [
"{\"message\": \"Error composing send transaction via API: Destination output is dust.\", \"code\": -32001}"
],
"message": "{\"message\": \"Error composing send transaction via API: Destination output is dust.\", \"code\": -32001}",
"type": "Exception"
},
"message": "Server error"
}
以下を読むに、手数料が安すぎてゴミだよってことかもしれない。
なら、適切な手数料というのはどうしたら算出できるの? だれが決めているの?
たぶんこの場合はmpchainかcounterPartyが決めているのだろう。せめて弾かれる最低額を知りたい。そもそも、通貨の単位もよくわからない。モナなのかサトシなのかワタナベなのか。
手数料には謎がたくさんあって未だにわからないことが多い。やはり送金の壁は高かった。
APIのテストができる環境とかがあれば触りながら理解を深められそうなのだが。そんな便利なものは見つけられなかった。
先の予想
たぶんこのcreate_sendでトランザクション情報を作成したあと、どうにかしてブロックチェーンに取り込んでくれるよう要求するのだろう。そのときフルノードなサーバのメモリ上にプールされ、未承認状態トランザクションになる。そしてマイナーがそのトランザクションをマイニングし終えた時点でブロックチェーンに取り込まれ、そこで承認済みトランザクションとして取引が確定するのだろう。
今回はトランザクションを作成する時点でエラーになったので、まだブロックチェーンに取り込むよう要求する前の段階だと思われる。仮にcreate_sendが終わっても、まだ先がある。無謀な挑戦だったか。