Skip to content

Instantly share code, notes, and snippets.

@jonajosejg
Last active July 28, 2022 00:41
Show Gist options
  • Save jonajosejg/7344964e0ce3c2bbd43c43307c81c47f to your computer and use it in GitHub Desktop.
Save jonajosejg/7344964e0ce3c2bbd43c43307c81c47f to your computer and use it in GitHub Desktop.
'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