bitcoinfiles-node
Version:
Upload and Download files to Bitcoin Cash blockchain with Node and web
1,091 lines (919 loc) • 662 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.bitcoinfiles = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
let bfp = require('./lib/bfp');
let utils = require('./lib/utils');
let network = require('./lib/network');
module.exports = {
bfp: bfp,
utils: utils,
network: network
}
},{"./lib/bfp":2,"./lib/network":4,"./lib/utils":5}],2:[function(require,module,exports){
(function (Buffer){
let utils = require('./utils');
let Network = require('./network');
let Bitdb = require('./bitdb');
// const BITBOXSDK = require('bitbox-sdk/lib/bitbox-sdk').default
// , BITBOX = new BITBOXSDK()
let bchrpc = require('grpc-bchrpc-web');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
class Bfp {
constructor(BITBOX, network = 'mainnet', grpcUrl=null) {
this.BITBOX = BITBOX;
this.networkstring = network;
this.network = new Network(this.BITBOX, grpcUrl);
this.bitdb = new Bitdb(network);
if(grpcUrl)
this.client = new bchrpc.Client(grpcUrl);
else
this.client = new bchrpc.Client();
}
static get lokadIdHex() { return "42465000" }
async uploadHashOnlyObject(type, // file = 1, folder = 3
fundingUtxo, // object in form: { txid:'', satoshis:#, vout:# }
fundingAddress, // string
fundingWif, // hex string?
objectDataArrayBuffer, // ArrayBuffer
objectName=null, // string
objectExt=null, // string
prevObjectSha256Hex=null, // hex string
objectExternalUri=null, // utf8 string
objectReceiverAddress=null, // string
signProgressCallback=null,
signFinishedCallback=null,
uploadProgressCallback=null,
uploadFinishedCallback=null){
let fileSize = objectDataArrayBuffer.byteLength;
let hash = this.BITBOX.Crypto.sha256(new Buffer(objectDataArrayBuffer)).toString('hex');
// chunks
let chunkCount = 0; //Math.floor(fileSize / 220);
// estimate cost
// build empty meta data OpReturn
let configEmptyMetaOpReturn = {
msgType: type,
chunkCount: chunkCount,
fileName: objectName,
fileExt: objectExt,
fileSize: fileSize,
fileSha256Hex: hash,
prevFileSha256Hex: prevObjectSha256Hex,
fileUri: objectExternalUri,
chunkData: null
};
//* ** building transaction
let transactions = [];
let txid = fundingUtxo.txid;
let satoshis = fundingUtxo.satoshis;
let vout = fundingUtxo.vout;
let metaOpReturn = Bfp.buildMetadataOpReturn(configEmptyMetaOpReturn);
// build meta data transaction
let configMetaTx = {
bfpMetadataOpReturn: metaOpReturn,
input_utxo: {
txid: txid,//chunksTx.getId(),
vout: vout,
satoshis: satoshis,//chunksTx.outs[1].value,
wif: fundingWif
},
fileReceiverAddress: objectReceiverAddress != null ? objectReceiverAddress : fundingAddress
};
let metaTx = this.buildMetadataTx(configMetaTx);
transactions.push(metaTx);
// sign progress
if(signProgressCallback != null){
signProgressCallback(100);
}
// progress : signing finished
if(signFinishedCallback != null){
signFinishedCallback();
}
//* ** sending transaction
if(uploadProgressCallback != null){
uploadProgressCallback(0);
}
console.log('transaction: ', transactions[0].toHex());
var bfTxId = await this.network.sendTxWithRetry(transactions[0].toHex());
// progress
if(uploadProgressCallback != null){
uploadProgressCallback(100);
}
bfTxId = 'bitcoinfile:' + bfTxId;
if(uploadFinishedCallback != null){
uploadFinishedCallback(bfTxId);
}
return bfTxId;
}
async uploadFolderHashOnly(fundingUtxo, // object in form: { txid:'', satoshis:#, vout:# }
fundingAddress, // string
fundingWif, // hex string?
folderDataArrayBuffer, // ArrayBuffer
folderName=null, // string
folderExt=null, // string
prevFolderSha256Hex=null, // hex string
folderExternalUri=null, // utf8 string
folderReceiverAddress=null, // string
signProgressCallback=null,
signFinishedCallback=null,
uploadProgressCallback=null,
uploadFinishedCallback=null){
return await this.uploadHashOnlyObject(3,
fundingUtxo, // object in form: { txid:'', satoshis:#, vout:# }
fundingAddress, // string
fundingWif, // hex string?
folderDataArrayBuffer, // ArrayBuffer
folderName, // string
folderExt, // string
prevFolderSha256Hex, // hex string
folderExternalUri, // utf8 string
folderReceiverAddress, // string
signProgressCallback,
signFinishedCallback,
uploadProgressCallback,
uploadFinishedCallback
)
}
async uploadFileHashOnly(fundingUtxo, // object in form: { txid:'', satoshis:#, vout:# }
fundingAddress, // string
fundingWif, // hex string?
fileDataArrayBuffer, // ArrayBuffer
fileName=null, // string
fileExt=null, // string
prevFileSha256Hex=null, // hex string
fileExternalUri=null, // utf8 string
fileReceiverAddress=null, // string
signProgressCallback=null,
signFinishedCallback=null,
uploadProgressCallback=null,
uploadFinishedCallback=null){
return await this.uploadHashOnlyObject(1,
fundingUtxo, // object in form: { txid:'', satoshis:#, vout:# }
fundingAddress, // string
fundingWif, // hex string?
fileDataArrayBuffer, // ArrayBuffer
fileName, // string
fileExt, // string
prevFileSha256Hex, // hex string
fileExternalUri, // utf8 string
fileReceiverAddress, // string
signProgressCallback,
signFinishedCallback,
uploadProgressCallback,
uploadFinishedCallback
)
}
async uploadFile(fundingUtxo, // object in form: { txid:'', satoshis:#, vout:# }
fundingAddress, // string
fundingWif, // hex string?
fileDataArrayBuffer, // ArrayBuffer
fileName=null, // string
fileExt=null, // string
prevFileSha256Hex=null, // hex string
fileExternalUri=null, // utf8 string
fileReceiverAddress=null, // string
signProgressCallback=null,
signFinishedCallback=null,
uploadProgressCallback=null,
uploadFinishedCallback=null,
delay_ms=500) {
let fileSize = fileDataArrayBuffer.byteLength;
let hash = this.BITBOX.Crypto.sha256(new Buffer(fileDataArrayBuffer)).toString('hex');
// chunks
let chunks = [];
let chunkCount = Math.floor(fileSize / 220);
for (let nId = 0; nId < chunkCount; nId++) {
chunks.push(fileDataArrayBuffer.slice(nId * 220, (nId + 1) * 220));
}
// meta
if (fileSize % 220) {
chunks[chunkCount] = fileDataArrayBuffer.slice(chunkCount * 220, fileSize);
chunkCount++;
}
// estimate cost
// build empty meta data OpReturn
let configEmptyMetaOpReturn = {
msgType: 1,
chunkCount: chunkCount,
fileName: fileName,
fileExt: fileExt,
fileSize: fileSize,
fileSha256Hex: hash,
prevFileSha256Hex: prevFileSha256Hex,
fileUri: fileExternalUri,
chunkData: null
};
//* ** building transaction
let transactions = [];
// show progress
let nDiff = 100 / chunkCount;
let nCurPos = 0;
for (let nId = 0; nId < chunkCount; nId++) {
// build chunk data OpReturn
let chunkOpReturn = Bfp.buildDataChunkOpReturn(chunks[nId]);
let txid = '';
let satoshis = 0;
let vout = 1;
if (nId === 0) {
txid = fundingUtxo.txid;
satoshis = fundingUtxo.satoshis;
vout = fundingUtxo.vout;
} else {
txid = transactions[nId - 1].getId();
satoshis = transactions[nId - 1].outs[1].value;
}
// build chunk data transaction
let configChunkTx = {
bfpChunkOpReturn: chunkOpReturn,
input_utxo: {
address: fundingAddress,
txid: txid,
vout: vout,
satoshis: satoshis,
wif: fundingWif
}
};
let chunksTx = this.buildChunkTx(configChunkTx);
if (nId === chunkCount - 1) {
let emptyOpReturn = Bfp.buildMetadataOpReturn(configEmptyMetaOpReturn);
let capacity = 223 - emptyOpReturn.length;
if (capacity >= chunks[nId].byteLength) {
// finish with just a single metadata txn
// build meta data OpReturn
let configMetaOpReturn = {
msgType: 1,
chunkCount: chunkCount,
fileName: fileName,
fileExt: fileExt,
fileSize: fileSize,
fileSha256Hex: hash,
prevFileSha256Hex: prevFileSha256Hex,
fileUri: fileExternalUri,
chunkData: chunks[nId]
};
let metaOpReturn = Bfp.buildMetadataOpReturn(configMetaOpReturn);
// build meta data transaction
let configMetaTx = {
bfpMetadataOpReturn: metaOpReturn,
input_utxo: {
txid: txid,
vout: vout,
satoshis: satoshis,
wif: fundingWif
},
fileReceiverAddress: fileReceiverAddress != null ? fileReceiverAddress : fundingAddress
};
let metaTx = this.buildMetadataTx(configMetaTx);
transactions.push(metaTx);
} else {
// finish with both chunk txn and then final empty metadata txn
transactions.push(chunksTx);
let metaOpReturn = Bfp.buildMetadataOpReturn(configEmptyMetaOpReturn);
// build meta data transaction
let configMetaTx = {
bfpMetadataOpReturn: metaOpReturn,
input_utxo: {
txid: chunksTx.getId(),
vout: vout,
satoshis: chunksTx.outs[1].value,
wif: fundingWif
},
fileReceiverAddress: fileReceiverAddress != null ? fileReceiverAddress : fundingAddress
};
let metaTx = this.buildMetadataTx(configMetaTx);
transactions.push(metaTx);
}
} else { // not last transaction
transactions.push(chunksTx);
}
// sign progress
if(signProgressCallback != null){
signProgressCallback(nCurPos)
}
nCurPos += nDiff;
}
// progress : signing finished
if(signFinishedCallback != null){
signFinishedCallback();
}
//* ** sending transaction
nDiff = 100 / transactions.length;
nCurPos = 0;
if(uploadProgressCallback != null){
uploadProgressCallback(0);
}
for (let nId = 0; nId < transactions.length; nId++) {
console.log('transaction: ', transactions[nId].toHex());
var bfTxId = await this.network.sendTxWithRetry(transactions[nId].toHex());
// progress
if(uploadProgressCallback != null){
uploadProgressCallback(nCurPos);
}
nCurPos += nDiff;
// delay between transactions
await sleep(delay_ms);
}
bfTxId = 'bitcoinfile:' + bfTxId;
if(uploadFinishedCallback != null){
uploadFinishedCallback(bfTxId);
}
return bfTxId;
}
async downloadFile(bfpUri, progressCallback=null) {
let chunks = [];
let size = 0;
let txid = bfpUri.replace('bitcoinfile:', '');
txid = txid.replace('bitcoinfiles:', '');
let tx = await this.client.getTransaction(txid, true);
let prevHash = Buffer.from(tx.getTransaction().getInputsList()[0].getOutpoint().getHash_asU8()).toString('hex');
let metadata_opreturn_hex = Buffer.from(tx.getTransaction().getOutputsList()[0].getPubkeyScript_asU8()).toString('hex')
let bfpMsg = this.parsebfpDataOpReturn(metadata_opreturn_hex);
let downloadCount = bfpMsg.chunk_count;
if(bfpMsg.chunk_count > 0 && bfpMsg.chunk != null) {
downloadCount = bfpMsg.chunk_count - 1;
chunks.push(bfpMsg.chunk)
size += bfpMsg.chunk.length;
}
// Loop through raw transactions, parse out data
for (let index = 0; index < downloadCount; index++) {
// download prev txn
let tx = await this.client.getTransaction(prevHash);
prevHash = Buffer.from(tx.getTransaction().getInputsList()[0].getOutpoint().getHash_asU8()).toString('hex');
let op_return_hex = Buffer.from(tx.getTransaction().getOutputsList()[0].getPubkeyScript_asU8()).toString('hex');
// parse vout 0 for data, push onto chunks array
let bfpMsg = this.parsebfpDataOpReturn(op_return_hex);
chunks.push(bfpMsg.chunk);
size += bfpMsg.chunk.length;
if(progressCallback != null) {
progressCallback(index/(downloadCount-1));
}
}
// reverse order of chunks
chunks = chunks.reverse()
let fileBuf = new Buffer.alloc(size);
let index = 0;
chunks.forEach(chunk => {
chunk.copy(fileBuf, index)
index += chunk.length;
});
// TODO: check that metadata hash matches if one was provided.
let passesHashCheck = false
if(bfpMsg.sha256 != null){
let fileSha256 = this.BITBOX.Crypto.sha256(fileBuf);
let res = Buffer.compare(fileSha256, bfpMsg.sha256);
if(res === 0){
passesHashCheck = true;
}
}
return { passesHashCheck, fileBuf };
}
static buildMetadataOpReturn(config) {
let script = [];
// OP Return Prefix
script.push(0x6a);
// Lokad Id
let lokadId = Buffer.from(Bfp.lokadIdHex, 'hex');
script.push(utils.getPushDataOpcode(lokadId));
lokadId.forEach((item) => script.push(item));
// Message Type
script.push(utils.getPushDataOpcode([config.msgType]));
script.push(config.msgType);
// Chunk Count
let chunkCount = utils.int2FixedBuffer(config.chunkCount, 1)
script.push(utils.getPushDataOpcode(chunkCount))
chunkCount.forEach((item) => script.push(item))
// File Name
if (config.fileName == null || config.fileName.length === 0 || config.fileName == '') {
[0x4c, 0x00].forEach((item) => script.push(item));
} else {
let fileName = Buffer.from(config.fileName, 'utf8')
script.push(utils.getPushDataOpcode(fileName));
fileName.forEach((item) => script.push(item));
}
// File Ext
if (config.fileExt == null || config.fileExt.length === 0 || config.fileExt == '') {
[0x4c, 0x00].forEach((item) => script.push(item));
} else {
let fileExt = Buffer.from(config.fileExt, 'utf8');
script.push(utils.getPushDataOpcode(fileExt));
fileExt.forEach((item) => script.push(item));
}
let fileSize = utils.int2FixedBuffer(config.fileSize, 2)
script.push(utils.getPushDataOpcode(fileSize))
fileSize.forEach((item) => script.push(item))
// File SHA256
var re = /^[0-9a-fA-F]+$/;
if (config.fileSha256Hex == null || config.fileSha256Hex.length === 0 || config.fileSha256Hex == '') {
[0x4c, 0x00].forEach((item) => script.push(item));
} else if (config.fileSha256Hex.length === 64 && re.test(config.fileSha256Hex)) {
let fileSha256Buf = Buffer.from(config.fileSha256Hex, 'hex');
script.push(utils.getPushDataOpcode(fileSha256Buf));
fileSha256Buf.forEach((item) => script.push(item));
} else {
throw Error("File hash must be provided as a 64 character hex string");
}
// Previous File Version SHA256
if (config.prevFileSha256Hex == null || config.prevFileSha256Hex.length === 0 || config.prevFileSha256Hex == '') {
[0x4c, 0x00].forEach((item) => script.push(item));
} else if (config.prevFileSha256Hex.length === 64 && re.test(config.prevFileSha256Hex)) {
let prevFileSha256Buf = Buffer.from(config.prevFileSha256Hex, 'hex');
script.push(utils.getPushDataOpcode(prevFileSha256Buf));
prevFileSha256Buf.forEach((item) => script.push(item));
} else {
throw Error("Previous File hash must be provided as a 64 character hex string")
}
// File URI
if (config.fileUri == null || config.fileUri.length === 0 || config.fileUri == '') {
[0x4c, 0x00].forEach((item) => script.push(item));
} else {
let fileUri = Buffer.from(config.fileUri, 'utf8');
script.push(utils.getPushDataOpcode(fileUri));
fileUri.forEach((item) => script.push(item));
}
// Chunk Data
if (config.chunkData == null || config.chunkData.length === 0) {
[0x4c, 0x00].forEach((item) => script.push(item));
} else {
let chunkData = Buffer.from(config.chunkData);
script.push(utils.getPushDataOpcode(chunkData));
chunkData.forEach((item) => script.push(item));
}
//console.log('script: ', script);
let encodedScript = utils.encodeScript(script);
if (encodedScript.length > 223) {
throw Error("Script too long, must be less than 223 bytes.")
}
return encodedScript;
}
static buildDataChunkOpReturn(chunkData) {
let script = []
// OP Return Prefix
script.push(0x6a)
// Chunk Data
if (chunkData === undefined || chunkData === null || chunkData.length === 0) {
[0x4c, 0x00].forEach((item) => script.push(item));
} else {
let chunkDataBuf = Buffer.from(chunkData);
script.push(utils.getPushDataOpcode(chunkDataBuf));
chunkDataBuf.forEach((item) => script.push(item));
}
let encodedScript = utils.encodeScript(script);
if (encodedScript.length > 223) {
throw Error("Script too long, must be less than 223 bytes.");
}
return encodedScript;
}
// We may not need this function since the web browser wallet will be receiving funds in a single txn.
buildFundingTx(config) {
// Example config:
// let config = {
// outputAddress: this.bfpAddress,
// fundingAmountSatoshis: ____,
// input_utxos: [{
// txid: utxo.txid,
// vout: utxo.vout,
// satoshis: utxo.satoshis,
// wif: wif
// }]
// }
let transactionBuilder;
if(this.networkstring === 'mainnet')
transactionBuilder = new this.BITBOX.TransactionBuilder('bitcoincash');
else
transactionBuilder = new this.BITBOX.TransactionBuilder('bchtest');
let satoshis = 0;
config.input_utxos.forEach(token_utxo => {
transactionBuilder.addInput(token_utxo.txid, token_utxo.vout);
satoshis += token_utxo.satoshis;
});
let fundingMinerFee = this.BITBOX.BitcoinCash.getByteCount({ P2PKH: config.input_utxos.length }, { P2PKH: 1 })
let outputAmount = satoshis - fundingMinerFee;
//assert config.fundingAmountSatoshis == outputAmount //TODO: Use JS syntax and throw on error
// Output exact funding amount
transactionBuilder.addOutput(config.outputAddress, outputAmount);
// sign inputs
let i = 0;
for (const txo of config.input_utxos) {
let paymentKeyPair = this.BITBOX.ECPair.fromWIF(txo.wif);
transactionBuilder.sign(i, paymentKeyPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, txo.satoshis);
i++;
}
return transactionBuilder.build();
}
buildChunkTx(config) {
// Example config:
// let config = {
// bfpChunkOpReturn: chunkOpReturn,
// input_utxo: {
// address: utxo.address??
// txid: utxo.txid,
// vout: utxo.vout,
// satoshis: utxo.satoshis,
// wif: wif
// }
// }
let transactionBuilder
if(this.networkstring === 'mainnet')
transactionBuilder = new this.BITBOX.TransactionBuilder('bitcoincash');
else
transactionBuilder = new this.BITBOX.TransactionBuilder('bchtest');
transactionBuilder.addInput(config.input_utxo.txid, config.input_utxo.vout);
let chunkTxFee = this.calculateDataChunkMinerFee(config.bfpChunkOpReturn.length);
let outputAmount = config.input_utxo.satoshis - chunkTxFee;
// Chunk OpReturn
transactionBuilder.addOutput(config.bfpChunkOpReturn, 0);
// Genesis token mint
transactionBuilder.addOutput(config.input_utxo.address, outputAmount);
// sign inputs
let paymentKeyPair = this.BITBOX.ECPair.fromWIF(config.input_utxo.wif);
transactionBuilder.sign(0, paymentKeyPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, config.input_utxo.satoshis);
return transactionBuilder.build();
}
buildMetadataTx(config) {
// Example config:
// let config = {
// bfpMetadataOpReturn: metadataOpReturn,
// input_utxo:
// {
// txid: previousChunkTxid,
// vout: 1,
// satoshis: previousChunkTxData.satoshis,
// wif: fundingWif
// },
// fileReceiverAddress: outputAddress
// }
let transactionBuilder
if(this.networkstring === 'mainnet')
transactionBuilder = new this.BITBOX.TransactionBuilder('bitcoincash');
else
transactionBuilder = new this.BITBOX.TransactionBuilder('bchtest');
let inputSatoshis = 0;
transactionBuilder.addInput(config.input_utxo.txid, config.input_utxo.vout);
inputSatoshis += config.input_utxo.satoshis;
let metadataFee = this.calculateMetadataMinerFee(config.bfpMetadataOpReturn.length); //TODO: create method for calculating miner fee
let output = inputSatoshis - metadataFee;
// Metadata OpReturn
transactionBuilder.addOutput(config.bfpMetadataOpReturn, 0);
// outputs
let outputAddress = this.BITBOX.Address.toCashAddress(config.fileReceiverAddress);
transactionBuilder.addOutput(outputAddress, output);
// sign inputs
let paymentKeyPair = this.BITBOX.ECPair.fromWIF(config.input_utxo.wif);
transactionBuilder.sign(0, paymentKeyPair, null, transactionBuilder.hashTypes.SIGHASH_ALL, config.input_utxo.satoshis);
return transactionBuilder.build();
}
calculateMetadataMinerFee(genesisOpReturnLength, feeRate = 1) {
let fee = this.BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2PKH: 1 })
fee += genesisOpReturnLength
fee += 10 // added to account for OP_RETURN ammount of 0000000000000000
fee *= feeRate
return fee
}
calculateDataChunkMinerFee(sendOpReturnLength, feeRate = 1) {
let fee = this.BITBOX.BitcoinCash.getByteCount({ P2PKH: 1 }, { P2PKH: 1 })
fee += sendOpReturnLength
fee += 10 // added to account for OP_RETURN ammount of 0000000000000000
fee *= feeRate
return fee
}
static calculateFileUploadCost(fileSizeBytes, configMetadataOpReturn, fee_rate = 1){
let byte_count = fileSizeBytes;
let whole_chunks_count = Math.floor(fileSizeBytes / 220);
let last_chunk_size = fileSizeBytes % 220;
// cost of final transaction's op_return w/o any chunkdata
let final_op_return_no_chunk = Bfp.buildMetadataOpReturn(configMetadataOpReturn);
byte_count += final_op_return_no_chunk.length;
// cost of final transaction's input/outputs
byte_count += 35;
byte_count += 148 + 1;
// cost of chunk trasnsaction op_returns
byte_count += (whole_chunks_count + 1) * 3;
if (!Bfp.chunk_can_fit_in_final_opreturn(final_op_return_no_chunk.length, last_chunk_size))
{
// add fees for an extra chunk transaction input/output
byte_count += 149 + 35;
// opcode cost for chunk op_return
byte_count += 16;
}
// output p2pkh
byte_count += 35 * (whole_chunks_count);
// dust input bytes (this is the initial payment for the file upload)
byte_count += (148 + 1) * whole_chunks_count;
// other unaccounted per txn
byte_count += 22 * (whole_chunks_count + 1);
// dust output to be passed along each txn
let dust_amount = 546;
return byte_count * fee_rate + dust_amount;
}
static chunk_can_fit_in_final_opreturn (script_length, chunk_data_length) {
if (chunk_data_length === 0) {
return true;
}
let op_return_capacity = 223 - script_length;
if (op_return_capacity >= chunk_data_length) {
return true;
}
return false;
}
// static getFileUploadPaymentInfoFromHdNode(masterHdNode) {
// let hdNode = this.BITBOX.HDNode.derivePath(masterHdNode, "m/44'/145'/1'");
// let node0 = this.BITBOX.HDNode.derivePath(hdNode, '0/0');
// let keyPair = this.BITBOX.HDNode.toKeyPair(node0);
// let wif = this.BITBOX.ECPair.toWIF(keyPair);
// let ecPair = this.BITBOX.ECPair.fromWIF(wif);
// let address = this.BITBOX.ECPair.toLegacyAddress(ecPair);
// let cashAddress = this.BITBOX.Address.toCashAddress(address);
// return {address: cashAddress, wif: wif};
// }
// getFileUploadPaymentInfoFromSeedPhrase(seedPhrase) {
// let phrase = seedPhrase;
// let seedBuffer = this.BITBOX.Mnemonic.toSeed(phrase);
// // create HDNode from seed buffer
// let hdNode = this.BITBOX.HDNode.fromSeed(seedBuffer);
// let hdNode2 = this.BITBOX.HDNode.derivePath(hdNode, "m/44'/145'/1'");
// let node0 = this.BITBOX.HDNode.derivePath(hdNode2, '0/0');
// let keyPair = this.BITBOX.HDNode.toKeyPair(node0);
// let wif = this.BITBOX.ECPair.toWIF(keyPair);
// let ecPair = this.BITBOX.ECPair.fromWIF(wif);
// let address = this.BITBOX.ECPair.toLegacyAddress(ecPair);
// let cashAddress = this.BITBOX.Address.toCashAddress(address);
// return {address: cashAddress, wif: wif};
// }
parsebfpDataOpReturn(hex) {
const script = this.BITBOX.Script.toASM(Buffer.from(hex, 'hex')).split(' ');
let bfpData = {}
bfpData.type = 'metadata'
if(script.length == 2) {
bfpData.type = 'chunk';
try {
bfpData.chunk = Buffer.from(script[1], 'hex');
} catch(e) {
bfpData.chunk = null;
}
return bfpData;
}
if (script[0] != 'OP_RETURN') {
throw new Error('Not an OP_RETURN');
}
if (script[1] !== Bfp.lokadIdHex) {
throw new Error('Not a BFP OP_RETURN');
}
// 01 = On-chain File
if (script[2] != 'OP_1') { // NOTE: bitcoincashlib-js converts hex 01 to OP_1 due to BIP62.3 enforcement
throw new Error('Not a BFP file (type 0x01)');
}
// chunk count
bfpData.chunk_count = parseInt(script[3], 16);
if(script[3].includes('OP_')){
let val = script[3].replace('OP_', '');
bfpData.chunk_count = parseInt(val);
}
// filename
if(script[4] == 'OP_0'){
bfpData.filename = null
} else {
bfpData.filename = Buffer.from(script[4], 'hex').toString('utf8');
}
// fileext
if(script[5] == 'OP_0'){
bfpData.fileext = null
} else {
bfpData.fileext = Buffer.from(script[5], 'hex').toString('utf8');
}
// filesize
if(script[6] == 'OP_0'){
bfpData.filesize = null
} else {
bfpData.filesize = parseInt(script[6], 16);
}
// file_sha256
if(script[7] == 'OP_0'){
bfpData.sha256 = null
} else {
bfpData.sha256 = Buffer.from(script[7], 'hex');
}
// prev_file_sha256
if(script[8] == 'OP_0'){
bfpData.prevsha256 = null
} else {
bfpData.prevsha256 = Buffer.from(script[8], 'hex');
}
// uri
if(script[9] == 'OP_0'){
bfpData.uri = null
} else {
bfpData.uri = Buffer.from(script[9], 'hex').toString('utf8');
}
// chunk_data
if(script[10] == 'OP_0'){
bfpData.chunk = null
} else {
try {
bfpData.chunk = Buffer.from(script[10], 'hex');
} catch(e) {
bfpData.chunk = null
}
}
return bfpData;
}
}
module.exports = Bfp;
}).call(this,require("buffer").Buffer)
},{"./bitdb":3,"./network":4,"./utils":5,"buffer":34,"grpc-bchrpc-web":36}],3:[function(require,module,exports){
(function (Buffer){
const axios = require('axios');
module.exports = class BfpBitdb {
constructor(network) {
this.bitDbUrl = network === 'mainnet' ? 'https://bitdb.bitcoin.com/q/' : 'https://tbitdb.bitcoin.com/q/';
}
async getFileMetadata(txid, apiKey=null) {
txid = txid.replace('bitcoinfile:', '');
txid = txid.replace('bitcoinfiles:', '');
// if(!apiKey)
// throw new Error('Missing BitDB key');
let query = {
"v": 3,
"q": {
"find": { "tx.h": txid, "out.h1": "42465000", "out.h2": "01" }
},
"r": { "f": "[ .[] | { timestamp: (if .blk? then (.blk.t | strftime(\"%Y-%m-%d %H:%M\")) else null end), chunks: .out[0].h3, filename: .out[0].s4, fileext: .out[0].s5, size: .out[0].h6, sha256: .out[0].h7, prev_sha256: .out[0].h8, ext_uri: .out[0].s9, URI: \"bitcoinfile:\\(.tx.h)\" } ]" }
};
// example response format:
// { filename: 'tes158',
// fileext: '.json',
// size: '017a',
// sha256: '018321383bf2672befe28629d1e159af812260268a8aa77bbd4ec27489d65b58',
// prev_sha256: '',
// ext_uri: '' }
const json_str = JSON.stringify(query);
const data = Buffer.from(json_str).toString('base64');
const response = (await axios({
method: 'GET',
url: this.bitDbUrl + data,
headers: null,
// {
// 'key': apiKey,
// },
json: true,
})).data;
if(response.status === 'error'){
throw new Error(response.message || 'API error message missing');
}
const list = [];
// c = confirmed
if(response.c){
list.push(...response.c);
}
// u = unconfirmed
if(response.u){
list.push(...response.u);
}
if(list.length === 0){
throw new Error('File not found');
}
console.log('bitdb response: ', list[0]);
return list[0];
}
}
}).call(this,require("buffer").Buffer)
},{"axios":7,"buffer":34}],4:[function(require,module,exports){
//const BITBOXSDK = require('bitbox-sdk/lib/bitbox-sdk').default
let bchrpc = require('grpc-bchrpc-web');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
class BfpNetwork {
constructor(BITBOX, grpcUrl="https://bchd.greyh.at:8335") {
this.BITBOX = BITBOX;
this.stopPayMonitor = false;
this.isMonitoringPayment = false;
if(grpcUrl)
this.client = new bchrpc.Client(grpcUrl)
else
this.client = new bchrpc.Client()
}
async getLastUtxoWithRetry(address, retries = 40) {
let result;
let count = 0;
while(result == undefined){
result = await this.getLastUtxo(address)
console.log(result);
count++;
if(count > retries)
throw new Error("BITBOX.Address.utxo endpoint experienced a problem");
await sleep(250);
}
return result;
}
async getTransactionDetailsWithRetry(txid, retries = 40){
let result;
let count = 0;
while(result == undefined){
result = await this.BITBOX.Transaction.details(txid);
count++;
if(count > retries)
throw new Error("BITBOX.Address.details endpoint experienced a problem");
await sleep(250);
}
return result;
}
async getLastUtxo(address) {
// must be a cash or legacy addr
if(!this.BITBOX.Address.isCashAddress(address) && !this.BITBOX.Address.isLegacyAddress(address))
throw new Error("Not an a valid address format, must be cashAddr or Legacy address format.");
let res = (await this.BITBOX.Address.utxo([ address ]))[0];
if(res && res.utxos && res.utxos.length > 0)
return res.utxos[0];
return res;
}
async sendTx(hex, log=true) {
let res = await this.BITBOX.RawTransactions.sendRawTransaction(hex);
if(res && res.error)
return undefined;
if(res === "64: too-long-mempool-chain")
throw new Error("Mempool chain too long");
if(log)
console.log('sendTx() res: ', res);
return res;
}
async sendTxWithRetry(hex, retries = 40) {
let res;
let count = 0;
while(res === undefined || res.length != 64) {
res = await this.sendTx(hex);
count++;
if(count > retries)
break;
await sleep(250);
}
if(res.length != 64)
throw new Error("BITBOX network error");
return res;
}
async monitorForPayment(paymentAddress, fee, onPaymentCB) {
if(this.isMonitoringPayment || this.stopPayMonitor)
return;
this.isMonitoringPayment = true;
// must be a cash or legacy addr
if(!this.BITBOX.Address.isCashAddress(paymentAddress) && !this.BITBOX.Address.isLegacyAddress(paymentAddress))
throw new Error("Not an a valid address format, must be cashAddr or Legacy address format.");
while (true) {
try {
var utxo = await this.getLastUtxo(paymentAddress);
if (utxo && utxo && utxo.satoshis >= fee && utxo.confirmations === 0) {
break;
}
} catch (ex) {
console.log('monitorForPayment() error: ', ex);
}
if(this.stopPayMonitor) {
this.isMonitoringPayment = false;
return;
}
await sleep(2000);
}
this.isMonitoringPayment = false;
onPaymentCB(utxo);
}
}
module.exports = BfpNetwork;
},{"grpc-bchrpc-web":36}],5:[function(require,module,exports){
(function (Buffer){
class BfpUtils {
static getPushDataOpcode(data) {
let length = data.length
if (length === 0)
return [0x4c, 0x00]
else if (length < 76)
return length
else if (length < 256)
return [0x4c, length]
else
throw Error("Pushdata too large")
}
static int2FixedBuffer(amount, size) {
let hex = amount.toString(16);
hex = hex.padStart(size * 2, '0');
if (hex.length % 2) hex = '0' + hex;
return Buffer.from(hex, 'hex');
}
static encodeScript(script) {
const bufferSize = script.reduce((acc, cur) => {
if (Array.isArray(cur)) return acc + cur.length
else return acc + 1
}, 0)
const buffer = Buffer.allocUnsafe(bufferSize)
let offset = 0
script.forEach((scriptItem) => {
if (Array.isArray(scriptItem)) {
scriptItem.forEach((item) => {
buffer.writeUInt8(item, offset)
offset += 1
})
} else {
buffer.writeUInt8(scriptItem, offset)
offset += 1
}
})
return buffer
}
}
module.exports = BfpUtils
}).call(this,require("buffer").Buffer)
},{"buffer":34}],6:[function(require,module,exports){
!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}(this,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(4);t.Metadata=r.BrowserHeaders},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.debug=function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];console.debug?console.debug.apply(null,e):console.log.apply(null,e)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=null;t.default=function(e){null===r?(r=[e],setTimeout(function(){!function e(){if(r){var t=r;r=null;for(var n=0;n<t.length;n++)try{t[n]()}catch(s){null===r&&(r=[],setTimeout(function(){e()},0));for(var o=t.length-1;o>n;o--)r.unshift(t[o]);throw s}}}()},0)):r.push(e)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(0),o=n(9),s=n(10),i=n(1),a=n(2),u=n(5),d=n(15);t.client=function(e,t){return new c(e,t)};var c=function(){function e(e,t){this.started=!1,this.sentFirstMessage=!1,this.completed=!1,this.closed=!1,this.finishedSending=!1,this.onHeadersCallbacks=[],this.onMessageCallbacks=[],this.onEndCallbacks=[],this.parser=new o.ChunkParser,this.methodDefinition=e,this.props=t,this.createTransport()}return e.prototype.createTransport=function(){var e=this.props.host+"/"+this.methodDefinition.service.serviceName+"/"+this.methodDefinition.methodName,t={methodDefinition:this.methodDefinition,debug:this.props.debug||!1,url:e,onHeaders:this.onTransportHeaders.bind(this),onChunk:this.onTransportChunk.bind(this),onEnd:this.onTransportEnd.bind(this)};this.props.transport?this.transport=this.props.transport(t):this.transport=u.makeDefaultTransport(t)},e.prototype.onTransportHeaders=function(e,t){if(this.props.debug&&i.debug("onHeaders",e,t),this.closed)this.props.debug&&i.debug("grpc.onHeaders received after request was closed - ignoring");else if(0===t);else{this.responseHeaders=e,this.props.debug&&i.debug("onHeaders.responseHeaders",JSON.stringify(this.responseHeaders,null,2));var n=p(e);this.props.debug&&i.debug("onHeaders.gRPCStatus",n);var r=n&&n>=0?n:s.httpStatusToCode(t);this.props.debug&&i.debug("onHeaders.code",r);var o=e.get("grpc-message")||[];if(this.props.debug&&i.debug("onHeaders.gRPCMessage",o),this.rawOnHeaders(e),r!==s.Code.OK){var a=this.decodeGRPCStatus(o[0]);this.rawOnError(r,a,e)}}},e.prototype.onTransportChunk=function(e){var t=this;if(this.closed)this.props.debug&&i.debug("grpc.onChunk received after request was closed - ignoring");else{var n=[];try{n=this.parser.parse(e)}catch(e){return this.props.debug&&i.debug("onChunk.parsing error",e,e.message),void this.rawOnError(s.Code.Internal,"parsing error: "+e.message)}n.forEach(function(e){if(e.chunkType===o.ChunkType.MESSAGE){var n=t.methodDefinition.responseType.deserializeBinary(e.data);t.rawOnMessage(n)}else e.chunkType===o.ChunkType.TRAILERS&&(t.responseHeaders?(t.responseTrailers=new r.Metadata(e.trailers),t.props.debug&&i.debug("onChunk.trailers",t.responseTrailers)):(t.responseHeaders=new r.Metadata(e.trailers),t.rawOnHeaders(t.responseHeaders)))})}},e.prototype.onTransportEnd=function(){if(this.props.debug&&i.debug("grpc.onEnd"),this.closed)this.props.debug&&i.debug("grpc.onEnd received after request was closed - ignoring");else if(void 0!==this.responseTrailers){var e=p(this.responseTrailers);if(null!==e){var t=this.responseTrailers.get("grpc-message"),n=this.decodeGRPCStatus(t[0]);this.rawOnEnd(e,n,this.responseTrailers)}else this.rawOnError(s.Code.Internal,"Response closed without grpc-status (Trailers provided)")}else{if(void 0===this.responseHeaders)return void this.rawOnError(s.Code.Unknown,"Response closed without headers");var r=p(this.responseHeaders),o=this.responseHeaders.get("grpc-message");if(this.props.debug&&i.debug("grpc.headers only response ",r,o),null===r)return void this.rawOnEnd(s.Code.Unknown,"Response closed without grpc-status (Headers only)",this.responseHeaders);var a=this.decodeGRPCStatus(o[0]);this.rawOnEnd(r,a,this.responseHeaders)}},e.prototype.decodeGRPCStatus=function(e){if(!e)return"";try{return decodeURIComponent(e)}catch(t){return e}},e.prototype.rawOnEnd=function(e,t,n){var r=this;this.props.debug&&i.debug("rawOnEnd",e,t,n),this.completed||(this.completed=!0,this.onEndCallbacks.forEach(function(o){a.default(function(){r.closed||o(e,t,n)})}))},e.prototype.rawOnHeaders=function(e){this.props.debug&&i.debug("rawOnHeaders",e),this.completed||this.onHeadersCallbacks.forEach(function(t){a.default(function(){t(e)})})},e.prototype.rawOnError=function(e,t,n){var o=this;void 0===n&&(n=new r.Metadata),this.props.debug&&i.debug("rawOnError",e,t),this.completed||(this.completed=!0,this.onEndCallbacks.forEach(function(r){a.default(function(){o.closed||r(e,t,n)})}))},e.prototype.rawOnMessage=function(e){var t=this;this.props.debug&&i.debug("rawOnMessage",e.toObject()),this.completed||this.closed||this.onMessageCallbacks.forEach(function(n){a.default(function(){t.closed||n(e)})})},e.prototype.onHeaders=function(e){this.onHeadersCallbacks.push(e)},e.prototype.onMessage=function(e){this.onMessageCallbacks.push(e)},e.prototype.onEnd=function(e){this.onEndCallbacks.push(e)},e.prototype.start=function(e){if(this.started)throw new Error("Client already started - cannot .start()");this.started=!0;var t=new r.Metadata(e||{});t.set("content-type","application/grpc-web+proto"),t.set("x-grpc-web","1"),this.transport.start(t)},e.prototype.send=function(e){if(!this.started)throw new Error("Client not started - .start() must be called before .send()");if(this.closed)throw new Error("Client already closed - cannot .send()");if(this.finishedSending)throw new Error("Client already finished sending - cannot .send()");if(!this.methodDefinition.requestStream&&this.sentFirstMessage)throw new Error("Message already sent for non-client-streaming method - cannot .send()");this.sentFirstMessage=!0;var t=d.frameRequest(e);this.transport.sendMessage(t)},e.prototype.finishSend=function(){if(!this.started)throw new Error("Client not started - .finishSend() must be called before .close()");if(this.closed)throw new Error("Client already closed - cannot .send()");if(this.finishedSending)throw new Error("Client already finished sending - cannot .finishSend()");this.finishedSending=!0,this.transport.finishSend()},e.prototype.close=function(){if(!this.started)throw new Error("Client not started - .start() must be called before .close()");if(this.closed)throw new Error("Client already closed - cannot .close()");this.closed=!0,this.props.debug&&i.debug("request.abort aborting request"),this.transport.cancel()},e}();function p(e){var t=e.get("grpc-status")||[];if(t.length>0)try{var n=t[0];return parseInt(n,10)}catch(e){return null}return null}},function(e,t,n){var r;r=function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{va