Last active
July 28, 2022 00:41
-
-
Save jonajosejg/7344964e0ce3c2bbd43c43307c81c47f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const assert = require('assert'); | |
const bcoin = require('bcoin'); | |
const MTX = bcoin.MTX; | |
const Keyring = bcoin.wallet.WalletKey; | |
const Outpoint = bcoin.Outpoint; | |
const Script = bcoin.Script; | |
const Coin = bcoin.Coin; | |
const policy = bcoin.protocol.policy | |
const fundingTarget = 100000000; // 1 BTC | |
const amountToFund = 50000000; // .5 BTC | |
const txRate = 10000; // 10000 satoshis/kb | |
// Create an HD master keypair. | |
const master = bcoin.hd.generate(); | |
const fundeeKey = master.derive(0); | |
const fundeeKeyring = new Keyring(fundeeKey.privateKey); | |
const fundeeAddress = fundeeKeyring.getAddress(); | |
// Derive 2 more private hd keys and keyrings for funders | |
const funder1Key = master.derive(1); | |
const funder1Keyring = new Keyring(funder1Key.privateKey); | |
const funder2Key = master.derive(2); | |
const funder2Keyring = new Keyring(funder2Key.privateKey); | |
const funders = [funder1Keyring, funder2Keyring]; | |
// create some coinbase transactions to fund our wallets | |
const coins = {}; | |
for(let i=0; i < funders.length; i++) { | |
const cb = new MTX(); | |
// Add a typical coinbase input | |
cb.addInput({ | |
prevout: new Outpoint(), | |
script: new Script() | |
}); | |
cb.addOutput({ | |
address: funders[i].getAddress(), | |
value: 500000000 // give the funder 5BTC | |
}); | |
const addInput = function addInput(coin, inputIndex, mtx, keyring, hashType) { | |
const sampleCoin = coin instanceof Coin ? coin : Coin.fromJSON(coin); | |
if(!hashType) hashType = Script.hashType.ANYONECANPAY | Script.hashType.ALL; | |
mtx.addCoin(sampleCoin); | |
mtx.scriptInput(inputIndex, sampleCoin, keyring); | |
mtx.signInput(inputIndex, sampleCoin, keyring, hashType); | |
assert(mtx.isSigned(), 'Input was not signed properly'); | |
} | |
const getFeeForInput = function getFeeForInput(coin, address, keyring, rate) { | |
const fundingTarget = 100000000; // 1 BTC (arbitrary for purposes of this function) | |
const testMTX = new MTX(); | |
// we're not actually going to use this tx for anything other than calculate what fee should be | |
addInput(coin, 0, testMTX, keyring); | |
return testMTX.getMinFee(null, rate); | |
} | |
const splitCoinbase = async function splitCoinbase(funderKeyring, coin, targetAmount, txRate) { | |
// loop through each coinbase coin to split | |
let coins = []; | |
const mtx = new MTX(); | |
assert(coin.value > targetAmount, 'coin value is not enough!'); | |
// creating a transaction that will have an output equal to what we want to fund | |
mtx.addOutput({ | |
address: funderKeyring.getAddress(), | |
value: targetAmount | |
}); | |
// the fund method will automatically split | |
// the remaining funds to the change address | |
// Note that in a real application these splitting transactions will also | |
// have to be broadcast to the network | |
await mtx.fund([coin], { | |
rate: txRate, | |
// send change back to an address belonging to the funder | |
changeAddress: funderKeyring.getAddress() | |
}).then(() => { | |
// sign the mtx to finalize split | |
mtx.sign(funderKeyring); | |
assert(mtx.verify()); | |
const tx = mtx.toTX(); | |
assert(tx.verify(mtx.view)); | |
const outputs = tx.outputs; | |
// get coins from tx | |
outputs.forEach((outputs, index) => { | |
coins.push(Coin.fromTX(tx, index, -1)); | |
}); | |
}) | |
.catch(e => console.log('There was an error: ', e)); | |
return coins; | |
}; | |
const composeCrowdfund = async function composeCrowdfund(coins) { | |
const funderCoins = {}; | |
// Loop through each coinbase | |
for (let index in coins) { | |
const coinbase = coins[index][0]; | |
// estimate fee for each coin (assuming their split coins will use same tx type) | |
const estimatedFee = getFeeForInput(coinbase, fundeeAddress, funders[index], txRate); | |
const targetPlusFee = amountToFund + estimatedFee; | |
// split the coinbase with targetAmount plus estimated fee | |
const splitCoins = await Utils.splitCoinbase(funders[index], coinbase, targetPlusFee, txRate); | |
// add to funderCoins object with returned coins from splitCoinbase being value, | |
// and index being the key | |
funderCoins[index] = splitCoins; | |
} | |
// ... we'll keep filling out the rest of the code here | |
}; | |
const composeCrowdfund = async function composeCrowdfund(coins) { | |
//... | |
const fundMe = new MTX(); | |
// add an output with the target funding amount | |
fundMe.addOutput({ value: fundingTarget, address: fundeeAddress }); | |
// fund with first funder | |
let fundingCoin = funderCoins['0'][0]; | |
addInput(fundingCoin, 0, fundMe, funder1Keyring); | |
// fund with second funder | |
fundingCoin = funderCoins['1'][0]; | |
addInput(fundingCoin, 1, fundMe, funder2Keyring); | |
// We want to confirm that total value of inputs covers the funding goal | |
// NOTE: the difference goes to the miner in the form of fees | |
assert(fundMe.getInputValue() >= fundMe.outputs[0].value, 'Total inputs not enough to fund'); | |
assert(fundMe.verify(), 'The mtx is malformed'); | |
const tx = fundMe.toTX(); | |
console.log('total input value = ', fundMe.getInputValue()); | |
console.log('Fee getting sent to miners:', fundMe.getInputValue() - fundingTarget, 'satoshis'); | |
assert(tx.verify(fundMe.view), 'there is a problem with your tx'); | |
return tx; | |
}; | |
composeCrowdfund(coins).then(myCrowdfundTx => console.log(myCrowdfundTx)); | |
const composeWalletCrowdfund = async function composeWalletCrowdfund() { | |
//... | |
const fundingCoins = {}; | |
// go through each funding wallet to prepare coins | |
for(let id in funders) { | |
const funder = funders[id]; | |
const coins = await funder.getCoins(); | |
const funderInfo = await funder.getInfo(); | |
// Before we do anything we need to get | |
// the fee that will be necessary for each funder's input. | |
const funderKey = await funder.getWIF(coins[0].address); | |
const funderKeyring = new bcoin.KeyRing.fromSecret(funderKey.privateKey); | |
const feeForInput = Utils.getFeeForInput(coins[0], fundeeAddress.address, funderKeyring, rate); | |
amountToFund += feeForInput; | |
// Next, go through available coins | |
// to find a coin equal to or greater than value to fund | |
// We didn't do this before because we knew what coins were available. But if we have one already in our wallets, then we can just use that! | |
let fundingCoin = {}; | |
for(let coin of coins) { | |
if (coin.value === amountToFund) { | |
// if we already have a coin of the right value we can use that | |
fundingCoin = coin; | |
break; | |
} | |
} | |
if (!Object.keys(fundingCoin).length) { | |
// if we don't have a coin of the right amount to fund with | |
// we need to create one by sending the funder wallet | |
// a tx that includes an output of the right amount | |
// this is similar to what we did in the manual version | |
const receiveAddress = await funder.createAddress('default') // send it back to the funder | |
const tx = await funder.send({ | |
rate, | |
outputs: [{ | |
value: amountToFund, | |
address: receiveAddress.address | |
}] | |
}); | |
// get index of ouput for fundingCoin | |
let coinIndex; | |
for (let i=0; i < tx.outputs.length; i++) { | |
if (tx.outputs[i].value === amountToFund) { | |
coinIndex = i; | |
break; | |
} | |
} | |
assert(tx.outputs[coinIndex].value === amountToFund, 'value of output at index not correct'); | |
// first argument is for the account | |
// default is being used for all examples | |
fundingCoin = await funder.getCoin('default', tx.hash, coinIndex); | |
} | |
fundingCoins[funder.id] = fundingCoin; | |
} | |
} | |
const composeWalletCrowdfund = async function composeWalletCrowdfund() { | |
//... | |
const fundMe = new MTX(); | |
// Use the maxFee to calculate output value for transaction | |
fundMe.addOutput({value: fundingTarget, address: fundeeAddress.address }); | |
// go through our coins and add each as an input in our transaction | |
let inputCounter = 0; | |
for(let funder in fundingCoins) { | |
const wallet = funders[funder]; | |
const coinOptions = fundingCoins[funder]; | |
const key = await wallet.getWIF(coinOptions.address); | |
const keyring = new bcoin.KeyRing.fromSecret(key.privateKey); | |
// this is the same utility as we used in our other example | |
addInput(coinOptions, inputCounter, fundMe, keyring); | |
assert(fundMe.isSigned(), 'Input has not been signed correctly'); | |
inputCounter++; | |
} | |
// confirm that the transaction has been properly templated and signed | |
assert( | |
fundMe.inputs.length === Object.keys(funders).length, | |
'Number of inputs in MTX is incorrect' | |
); | |
assert(fundMe.verify(), 'MTX is malformed'); | |
// make our transaction immutable so we can send it to th enetwork | |
const tx = fundMe.toTX(); | |
assert(tx.verify(fundMe.view), 'TX is malformed. Fix before broadcasting'); | |
// check the value of our inputs just to confirm what the fees are | |
console.log('Total value of inputs: ', fundMe.getInputValue() ); | |
console.log('Fee to go to miners: ', fundMe.getInputValue() - fundingTarget); | |
// Finally, broadcast tx | |
try { | |
const broadcastStatus = await client.broadcast(tx); | |
return tx; | |
} catch (e) { | |
console.log('There was a problem: ', e); | |
} | |
} | |
composeWalletCrowdfund() | |
.then(myCrowdfundTx => console.log('Transaction broadcast: ', myCrowdfundTx)) | |
.catch(e => console.log('There was a problem: ', e)); | |
const getFeeForInput = function getFeeForInput(coin, address, keyring, rate) { | |
const fundingTarget = 100000000; // 1 BTC (arbitrary for purposes of this function) | |
const testMTX = new MTX(); | |
// we're not actually going to use this tx for anything other than calculate what fee should be | |
addInput(coin, 0, testMTX, keyring); | |
return testMTX.getMinFee(null, rate); | |
} | |
const splitCoinbase = async function splitCoinbase(funderKeyring, coin, targetAmount, txRate) { | |
// loop through each coinbase coin to split | |
let coins = []; | |
const mtx = new MTX(); | |
assert(coin.value > targetAmount, 'coin value is not enough!'); | |
// creating a transaction that will have an output equal to what we want to fund | |
mtx.addOutput({ | |
address: funderKeyring.getAddress(), | |
value: targetAmount | |
}); | |
// the fund method will automatically split | |
// the remaining funds to the change address | |
// Note that in a real application these splitting transactions will also | |
// have to be broadcast to the network | |
await mtx.fund([coin], { | |
rate: txRate, | |
// send change back to an address belonging to the funder | |
changeAddress: funderKeyring.getAddress() | |
}).then(() => { | |
// sign the mtx to finalize split | |
mtx.sign(funderKeyring); | |
assert(mtx.verify()); | |
const tx = mtx.toTX(); | |
assert(tx.verify(mtx.view)); | |
const outputs = tx.outputs; | |
// get coins from tx | |
outputs.forEach((outputs, index) => { | |
coins.push(Coin.fromTX(tx, index, -1)); | |
}); | |
}) | |
.catch(e => console.log('There was an error: ', e)); | |
return coins; | |
}; | |
const composeCrowdfund = async function composeCrowdfund(coins) { | |
const funderCoins = {}; | |
// Loop through each coinbase | |
for (let index in coins) { | |
const coinbase = coins[index][0]; | |
// estimate fee for each coin (assuming their split coins will use same tx type) | |
const estimatedFee = getFeeForInput(coinbase, fundeeAddress, funders[index], txRate); | |
const targetPlusFee = amountToFund + estimatedFee; | |
// split the coinbase with targetAmount plus estimated fee | |
const splitCoins = await Utils.splitCoinbase(funders[index], coinbase, targetPlusFee, txRate); | |
// add to funderCoins object with returned coins from splitCoinbase being value, | |
// and index being the key | |
funderCoins[index] = splitCoins; | |
} | |
// ... we'll keep filling out the rest of the code here | |
}; | |
const composeCrowdfund = async function composeCrowdfund(coins) { | |
//... | |
const fundMe = new MTX(); | |
// add an output with the target funding amount | |
fundMe.addOutput({ value: fundingTarget, address: fundeeAddress }); | |
// fund with first funder | |
let fundingCoin = funderCoins['0'][0]; | |
addInput(fundingCoin, 0, fundMe, funder1Keyring); | |
// fund with second funder | |
fundingCoin = funderCoins['1'][0]; | |
addInput(fundingCoin, 1, fundMe, funder2Keyring); | |
// We want to confirm that total value of inputs covers the funding goal | |
// NOTE: the difference goes to the miner in the form of fees | |
assert(fundMe.getInputValue() >= fundMe.outputs[0].value, 'Total inputs not enough to fund'); | |
assert(fundMe.verify(), 'The mtx is malformed'); | |
const tx = fundMe.toTX(); | |
console.log('total input value = ', fundMe.getInputValue()); | |
console.log('Fee getting sent to miners:', fundMe.getInputValue() - fundingTarget, 'satoshis'); | |
assert(tx.verify(fundMe.view), 'there is a problem with your tx'); | |
return tx; | |
}; | |
composeCrowdfund(coins).then(myCrowdfundTx => console.log(myCrowdfundTx)); | |
assert(cb.inputs[0].isCoinbase()); | |
// Convert the coinbase output to a Coin | |
// object and add it to the available coins for that keyring. | |
// In reality you might get these coins from a wallet. | |
coins[i] = [Coin.fromTX(cb, 0, -1)]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment