Transaction

af26ed5c8e31fc9befe3f00e9e801f7accbdf7a9b6be40fbcc3b3a89335c4707
( - )
253,195
2019-07-21 02:47:19
1
18,073 B

1 Output

Total Output:
  • jmeta"12vtKhjeA9FkDdjgoqS5iqXYms19EnTPnX@876117fdc96a39cacccd0f069a854cc77d56738783c74745dff1cdf1f483b0e8"19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAutMUEimport fs from 'fs'; import path from 'path'; import readline from 'readline-promise'; import BitIndexSDK from 'bitindex-sdk'; import { MetanetCache } from './metanet_cache'; import { MetanetNode } from './metanet_node'; const bsv = require('bsv'); const bitindex = new BitIndexSDK(); /** * Recurses current directory and pushes all directories and files to the metanet. * Files/dir listed in .bsvignore will be ignored (currently only exact match). */ export class Push { fee = 400; feeb = 1.1; minimumOutputValue = 546; private _packageInfo; get packageInfo() { if (!this._packageInfo) { const packgeInfoPath = path.join(process.cwd(), 'bsvpush.json'); try { this._packageInfo = JSON.parse(fs.readFileSync(packgeInfoPath).toString()); } catch (error) { console.log(error, 'Error loading bsvpush.json: ' + packgeInfoPath); console.log('Try: bsvpush init'); process.exit(1); } } return this._packageInfo; } private _metanetCache: MetanetCache; get metanetCache(): MetanetCache { if (!this._metanetCache) { const metanetCachePath = path.join(process.cwd(), '.bsvpush', 'metanet.json'); try { this._metanetCache = new MetanetCache(JSON.parse(fs.readFileSync(metanetCachePath).toString())); this._metanetCache.root.name = this.packageInfo.name; } catch (error) { console.log(error, 'Error loading master key from: ' + metanetCachePath); console.log('Try: bsvpush init'); process.exit(1); } } return this._metanetCache; } private _fundingKey; get fundingKey() { if (!this._fundingKey) { const fundingKeyPath = path.join(process.env.HOME, '.bsvpush', 'funding_key'); try { const funding = JSON.parse(fs.readFileSync(fundingKeyPath).toString()); this._fundingKey = bsv.HDPrivateKey(funding.xprv).deriveChild(funding.derivationPath); } catch(error) { console.log(error, 'Error loading funding key from: ' + fundingKeyPath); process.exit(1); } } return this._fundingKey; } private _ignoreList: Array<string>; get ignoreList() { if (!this._ignoreList) { const ignorePath = path.join(process.cwd(), '.bsvignore'); if (fs.existsSync(ignorePath)) { const ignoreText = fs.readFileSync(ignorePath).toString(); this._ignoreList = ignoreText.split('\n').map(line => line.trim()); } else { this._ignoreList = []; } } return this._ignoreList; } ignore(file: string): boolean { return this.ignoreList.some(ignoreExp => ignoreExp == file); } /** * Creates any directories and files used by bsvpush if they don't already exist. * .bsvpush * .bsvpush/metanet.json * .bsvpush.json * HOME/.bsvpush * HOME/.bsvpush/funding_key * * Appends .bsvpush directory to .gitignore as a safety measure, as .bsvpush/metanet.json * includes the metanet master private key. */ init() { let bsvignore = path.join(process.cwd(), '.bsvignore'); if (!fs.existsSync(bsvignore)) { console.log(`Creating: ${bsvignore}`) const contents = `.bsvpush .git`; fs.writeFileSync(bsvignore, contents); } let bsvpushDir = path.join(process.cwd(), '.bsvpush'); if (!fs.existsSync(bsvpushDir)) { console.log(`Creating: ${bsvpushDir}`) fs.mkdirSync(bsvpushDir); } let metanet = path.join(bsvpushDir, 'metanet.json'); if (!fs.existsSync(metanet)) { console.log(`Creating: ${metanet}`) const masterKey = bsv.HDPrivateKey(); let json = { masterKey: masterKey.xprivkey, root: { keyPath: 'm/0', index: 0, children: {} } }; fs.writeFileSync(metanet, JSON.stringify(json, null, 2)); } let bsvpush = path.join(process.cwd(), 'bsvpush.json'); if (!fs.existsSync(bsvpush)) { console.log(`Creating: ${bsvpush}`) let json = { name: "", owner: "", description: "", sponsor: { to: "" }, version: "" }; fs.writeFileSync(bsvpush, JSON.stringify(json, null, 2)); } let bsvpushHomeDir = path.join(process.env.HOME, '.bsvpush'); if (!fs.existsSync(bsvpushHomeDir)) { console.log(`Creating: ${bsvpushHomeDir}`) fs.mkdirSync(bsvpushHomeDir); } let fundingKey = path.join(bsvpushHomeDir, 'funding_key'); if (!fs.existsSync(fundingKey)) { console.log(`Creating: ${fundingKey}`) let json = { xprv: '', derivationPath: 'm/0/0' }; fs.writeFileSync(fundingKey, JSON.stringify(json, null, 2)); } // Add .bsvpush directory to .gitignore // This is a safety measure as .bsvpush contains the master private key console.log(`Adding .bsvpush to .gitignore`) let gitIgnore = path.join(process.cwd(), '.gitignore'); if (!fs.existsSync(gitIgnore)) { fs.writeFileSync(gitIgnore, ".bsvpush"); } else { fs.appendFileSync(gitIgnore, "\n.bsvpush"); } } /** * Ensure all required files exist, if not exit. */ preflight() { const paths = [ path.join(process.env.HOME, '.bsvpush'), path.join(process.env.HOME, '.bsvpush', 'funding_key'), path.join(process.cwd(), '.bsvpush'), path.join(process.cwd(), '.bsvpush', 'metanet.json'), path.join(process.cwd(), '.bsvignore'), path.join(process.cwd(), 'bsvpush.json') ]; const pathsDontExist = []; paths.forEach(p => { if (!fs.existsSync(p)) { pathsDontExist.push(p); } }); if (pathsDontExist.length > 0) { const s = pathsDontExist.length > 1 ? 's' : ''; console.log(`Cannot find the following file${s}: `); pathsDontExist.forEach(p => console.log(`\t${p}`)); console.log(`Run: bsvpush init`); process.exit(1); } } /** * Pushes the current directory to metanet. * Asks the user before sending the transactions. */ async push() { // Ensure config files exist this.preflight(); const fees = this.generateScripts(process.cwd(), this.metanetCache.root, null); const fundingTx = await this.fundingTransaction(fees); const metanetFees = fees.map(entry => entry.fee).reduce((sum, fee) => sum + fee); const fundingTxFee = Math.max(Math.ceil(fundingTx._estimateSize() * this.feeb), this.minimumOutputValue); const totalFee = metanetFees + this.metanetCache.root.fee + fundingTxFee; console.log(`Metanet fees will be: ${this.feeToString(metanetFees)}`); console.log(`Metanet root node fee will be: ${this.feeToString(this.metanetCache.root.fee)}`); console.log(`Funding transaction fee will be: ${this.feeToString(fundingTxFee)}`); console.log(`Total: ${this.feeToString(totalFee)}`); const rlp = readline.createInterface({input: process.stdin, output: process.stdout}); const response = await rlp.questionAsync(`Continue (Y/n)?`); rlp.close(); if (response !== 'n' && response !== 'N') { console.log(`Sending funding transaction: ${fundingTx.id}`); const response = await bitindex.tx.send(fundingTx.toString()); await this.waitForConfirmation(fundingTx.id); this.sendMetanetTransactions(null, this.metanetCache.root); fs.writeFileSync(path.join(process.cwd(), '.bsvpush', 'metanet.json'), JSON.stringify(this.metanetCache.toJSON(), null, 2)); } } feeToString(fee: number): string { return `${fee} satoshis (${fee / 1e8} BSV)`; } /** * Stages the specified directory by generating the transaction and estimating the fees. * Also stages all children of the directory. * @param dir parent directory (not the directory of the node). * @param node node representing this directory. * @param parent node representing parent. */ generateScripts(dir: string, node: MetanetNode, parent: MetanetNode, fees = []) { this.dirScript(node, parent); // Stage this node as a directory, then process children this.estimateFee(node); console.log(`[${node.keyPath}] ${dir} (${node.fee} satoshis)`); // Root node is not included in the split funding transaction as it is funded directly // from the funding key if (parent) { fees.push({parentKeyPath: parent.keyPath, fee: node.fee}); } let files = fs.readdirSync(dir, {withFileTypes: true}); for (const file of files) { if (!this.ignore(file.name)) { if (file.isDirectory()) { let child = node.child(file.name); if (!child) { child = node.addChild(file.name); } child.dir = dir; this.generateScripts(path.join(dir, file.name), child, node, fees); } else { const child = this.fileScript(dir, file.name, node); this.estimateFee(child); fees.push({parentKeyPath: node.keyPath, fee: child.fee}); console.log(`[${child.keyPath}] ${dir}/${child.name} (${child.fee} satoshis)`); } } else { // If the file is ignored but appears as a child then mark it for removal let child = node.child(file.name); if (child && !child.removed) { child.remove(); } } } return fees; } /** * Create an OP RETURN script with the name of the directory. * @param child node of the directory * @param parent node of the parent directory */ dirScript(child: MetanetNode, parent: MetanetNode) { const oprParts = this.opReturnMetaHeader(parent, child); oprParts.push(Buffer.from(child.name).toString('hex')); // Push the file name child.opreturn = oprParts; } /** * Create an OP RETURN script with the contents of the file. * @param dir directory where the file is located (to be able to read from the file) * @param name name of the file * @param parent parent node * @returns Promise to the child node */ fileScript(dir: string, name: string, parent: MetanetNode): MetanetNode { let child = parent.child(name); if (!child) { child = parent.addChild(name); } child.dir = dir; const oprParts = this.opReturnMetaHeader(parent, child); // B:// format https://github.com/unwriter/B oprParts.push(Buffer.from('19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut').toString('hex')); // B:// oprParts.push(fs.readFileSync(path.join(dir, name)).toString('hex')); // Data oprParts.push(Buffer.from(' ').toString('hex')); // Media Type oprParts.push(Buffer.from(' ').toString('hex')); // Encoding oprParts.push(Buffer.from(name).toString('hex')); // Filename child.opreturn = oprParts; return child; } /** * Creates a OP RETURN metanet header with the public key of the child * and the public key and transaction id of the parent node. * @param parent * @param child */ opReturnMetaHeader(parent: MetanetNode, child: MetanetNode): Array<string> { const childKey = this.metanetCache.masterKey.deriveChild(child.keyPath); const oprParts = new Array<string>(); oprParts.push('OP_RETURN'); oprParts.push(Buffer.from('meta').toString('hex')); oprParts.push(Buffer.from(childKey.publicKey.toAddress().toString()).toString('hex')); const txid = (parent === null ? 'NULL' : parent.txId); oprParts.push(Buffer.from(txid).toString('hex')); return oprParts; } estimateFee(node: MetanetNode) { const script = bsv.Script.fromASM(node.opreturn.join(' ')); if (script.toBuffer().length > 100000) { console.log(`Maximum OP_RETURN size is 100000 bytes. Script is ${script.toBuffer().length} bytes.`); process.exit(1); } const tempTX = new bsv.Transaction().from([this.getDummyUTXO()]); tempTX.addOutput(new bsv.Transaction.Output({ script: script.toString(), satoshis: 0 })); node.fee = Math.max(Math.ceil(tempTX._estimateSize() * this.feeb), this.minimumOutputValue); // Use the dummy txid for now as it will be used in the children tx size calculations node.txId = tempTX.id.toString(); } /** * Creates a funding transaction from the funding key, with outputs to all of the parent keys. * There can be multiple outputs to a parent key. This allows multiple parent->child transactions * to be subsequently sent without waiting for the parent to confirm, only the funding transaction * must confirm, and then all of the metanet nodes transactions can be sent. * @param fees Array of {parentKeyPath, fee}, representing each metanet node, except for the root node */ async fundingTransaction(fees) { const feeForMetanetNodes = fees.map(entry => entry.fee).reduce((sum, fee) => sum + fee); let utxos = await bitindex.address.getUtxos(this.fundingKey.publicKey.toAddress().toString()); utxos = this.filterUTXOs(utxos, feeForMetanetNodes + this.fee); let tx = new bsv.Transaction() .from(utxos) .fee(this.fee) .change(this.fundingKey.publicKey.toAddress()); fees.forEach(entry => { const parentKey = this.metanetCache.masterKey.deriveChild(entry.parentKeyPath); tx.to(parentKey.publicKey.toAddress(), entry.fee); }); const thisFee = Math.ceil(tx._estimateSize() * this.feeb); tx.fee(thisFee); tx.sign(this.fundingKey.privateKey); return tx; } /** * Sends the metanet transaction for the specified node and all of its children. * @param node */ async sendMetanetTransactions(parent: MetanetNode, node: MetanetNode) { console.log(`[${node.keyPath}] ${node.dir}/${node.name}`); const tx = await this.metanetTransaction(this.fundingKey, parent, node); console.log(`Sending metanet transaction: ${tx.id}`); const response = await bitindex.tx.send(tx.toString()); console.log(response); await this.sleep(1000); // Get UTXOs for this key which will be the parent key of the children to be sent next const privateKey = this.metanetCache.masterKey.deriveChild(node.keyPath); node.utxos = await bitindex.address.getUtxos(privateKey.publicKey.toAddress().toString()); for (const key of node.getUnremovedChildKeys()) { await this.sendMetanetTransactions(node, node.children[key]); } } async waitForConfirmation(txid: string) { // Wait until the transaction appears while (!('txid' in await bitindex.tx.get(txid))) { console.log('Waiting for transaction to appear...'); await this.sleep(1000); } console.log('Waiting for transaction to be confirmed... (this could take a few minutes)'); while (!('confirmations' in await bitindex.tx.get(txid))) { process.stdout.write('.'); await this.sleep(1000); } console.log(JSON.stringify(await bitindex.tx.get(txid), null, 2)); console.log(`\nTransaction confirmed: ${txid}`); } /** * Creates a metanet transaction and a funding transaction to fund the parent key * but doesn't send either transaction. Both transactions are stored in the MetanetNode. * https://github.com/bowstave/meta-writer/blob/master/index.js */ async metanetTransaction(fundingKey, parent: MetanetNode, node: MetanetNode) { let utxos = []; if (parent) { // There should be a transaction output from the parent for every child with the exact fee utxos = this.filterUtxosExact(parent.utxos, node.fee); } else { // The root node is funded directly from the funding key utxos = await bitindex.address.getUtxos(fundingKey.publicKey.toAddress().toString()); utxos = this.filterUTXOs(utxos, node.fee); } // Patch in the parent's transaction id into the script which wasn't known when // the script was generated const parentTxId = (parent === null ? 'NULL' : parent.txId); node.opreturn[3] = Buffer.from(parentTxId).toString('hex'); const script = bsv.Script.fromASM(node.opreturn.join(' ')); let metaTX = new bsv.Transaction().from(utxos); metaTX.addOutput(new bsv.Transaction.Output({ script: script.toString(), satoshis: 0 })); if (!parent) { metaTX.fee(node.fee); metaTX.change(fundingKey.publicKey.toAddress()); metaTX.sign(fundingKey.privateKey); } else { metaTX.sign(this.metanetCache.masterKey.deriveChild(parent.keyPath).privateKey); } node.txId = metaTX.id; return metaTX; } getDummyUTXO() { return bsv.Transaction.UnspentOutput({ address: '19dCWu1pvak7cgw5b1nFQn9LapFSQLqahC', txId: 'e29bc8d6c7298e524756ac116bd3fb5355eec1da94666253c3f40810a4000804', outputIndex: 0, satoshis: 5000000000, scriptPubKey: '21034b2edef6108e596efb2955f796aa807451546025025833e555b6f9b433a4a146ac' }); } filterUTXOs(utxos, satoshisRequired) { let total = 0; const res = utxos.filter((utxo, i) => { if (total < satoshisRequired) { total += utxo.satoshis; return true; } return false; }); if (total < satoshisRequired) { throw new Error(`Insufficient funds (need ${satoshisRequired} satoshis, have ${total})`); } return res; } /** * Finds a single exact match UTXO for satoshisRequired and removes it from the utxos array. * @param utxos * @param satoshisRequired */ filterUtxosExact(utxos, satoshisRequired) { let total = 0; const index = utxos.findIndex(utxo => utxo.satoshis == satoshisRequired); const utxo = utxos[index]; // Remove utxo utxos.splice(index, 1); if (!utxo) { throw new Error(`Could not find a parent UTXO with the exact satoshis required (need ${satoshisRequired} satoshis)`); } return utxo; } sleep(ms){ return new Promise(resolve=>{ setTimeout(resolve,ms) }) } } export const push = new Push();   push.ts
    https://whatsonchain.com/tx/af26ed5c8e31fc9befe3f00e9e801f7accbdf7a9b6be40fbcc3b3a89335c4707