Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

- Added support for taproot address format #141

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 21 additions & 18 deletions build.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
/*
* nwjs builder script
*/
var NwBuilder = require('nw-builder');
var nw = new NwBuilder({
files: './build/**', // use the glob format
platforms: ['osx64', 'win32', 'win64', 'linux32', 'linux64'],
cacheDir: './cache',
buildDir: './builds/',
macIcns: './build/images/FreeWallet.icns',
// macCredits: './build/html/credits.html'
// winIco: '',
});
import nwbuilder from 'nw-builder';

// Output logs to the console
nw.on('log', console.log);
async function init(){

// Build returns a promise
nw.build().then(function () {
console.log('all done!');
}).catch(function (error) {
console.error(error);
});
//var NwBuilder = require('nw-builder');
var nw = await nwbuilder({
srcDir: './build/**', // use the glob format
platform: "win",
arch: "x64",
cacheDir: './cache',
outDir: './builds/',
icon: './build/images/FreeWallet.icns',
// macCredits: './build/html/credits.html'
// winIco: '',
});

// Output logs to the console
//nw.on('log', console.log); //This event no longer exists

console.log('all done!');
}

init()
4 changes: 2 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ nwjs=/Applications/nwjs.app/Contents/MacOS/nwjs
###

# Verify we are building on a mac
if [ "$OSTYPE" != "darwin" ] ; then
echo "Build script is meant to be run on MacOS!"
if [ "$OSTYPE" != "darwin" ] && [ "$OSTYPE" != "linux-gnu" ]; then
echo "Build script is meant to be run on MacOS or Linux!"
exit
fi

Expand Down
14 changes: 14 additions & 0 deletions html/addresses.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<button type="button" value="0" class="btn btn-default active" style="width:60px">All</button>
<button type="button" value="1" class="btn btn-default">Normal</button>
<button type="button" value="5" class="btn btn-default">Segwit</button>
<button type="button" value="6" class="btn btn-default">Taproot</button>
<button type="button" value="2" class="btn btn-default">Imported</button>
<button type="button" value="4" class="btn btn-default">Hardware</button>
<button type="button" value="3" class="btn btn-default">Watch</button>
Expand All @@ -29,6 +30,7 @@
<ul class="dropdown-menu">
<li><a href="#" id="btn-add-normal"><i class="fa fa-lg fa-plus-circle"></i> Add New Normal Address</a></li>
<li><a href="#" id="btn-add-segwit"><i class="fa fa-lg fa-plus-circle"></i> Add New Segwit Address</a></li>
<li><a href="#" id="btn-add-taproot"><i class="fa fa-lg fa-plus-circle"></i> Add New Taproot Address</a></li>
<li><a href="#" id="btn-import"><i class="fa fa-lg fa-key"></i> Import Private Key</a></li>
<li><a href="#" id="btn-hardware"><i class="fa fa-lg fa-lock"></i> Add Hardware Wallet Address</a></li>
<li><a href="#" id="btn-watch"><i class="fa fa-lg fa-eye"></i> Add 'Watch Only' Address</a></li>
Expand Down Expand Up @@ -100,6 +102,18 @@
updateAddressList();
}));

// Handle generating taproot addresses
$('#btn-add-taproot').click($.debounce(100,function(e){
// Make sure wallet is unlocked
if(dialogCheckLocked('generate a taproot address'))
return;
addr = addNewWalletAddress(FW.WALLET_NETWORK, 'taproot');
// Only display confirmation message if we have an address
if(addr)
dialogMessage('<i class="fa fa-lg fa-fw fa-info-circle"></i> New Wallet Address', 'Address ' + addr + ' has been added to your wallet.');
updateAddressList();
}));

// Handle adding 'Watch Only' addresses
$('#btn-watch').click($.debounce(100,function(e){
// Make sure wallet is unlocked
Expand Down
25,604 changes: 25,602 additions & 2 deletions js/bitcoinjs-lib.min.js

Large diffs are not rendered by default.

152 changes: 128 additions & 24 deletions js/freewallet-desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ FW.ASSET_DIVISIBLE = {};
FW.ASSET_DIVISIBLE['BTC'] = true;
FW.ASSET_DIVISIBLE['XCP'] = true;

//Init ecc lib for taproot to work
bitcoinjs.initEccLib(bitcoinjs.tiny_secp256k1)
ECPairFactory = bitcoinjs.ecpairfactory
ECPair = ECPairFactory(bitcoinjs.tiny_secp256k1)

// Start loading the wallet
$(document).ready(function(){

Expand Down Expand Up @@ -377,8 +382,10 @@ function createWallet( passphrase, isBip39=false ){
var d = s.derive("m/0'/0/" + i),
a = bc.Address(d.publicKey, network).toString();
b = bitcoinjs.payments.p2wpkh({ pubkey: d.publicKey.toBuffer(), network: bitcoinjs.networks[netname] }).address;
c = bitcoinjs.payments.p2tr({ pubkey: d.publicKey.toBuffer().slice(1, 33), network: bitcoinjs.networks[netname] }).address;
addWalletAddress(net, a, 'Address #' + (i + 1), 1, i);
addWalletAddress(net, b, 'Segwit Address #' + (i + 1), 7, i);
addWalletAddress(net, c, 'Taproot Address #' + (i + 1), 8, i);
}
});
// Set current address to first address in wallet
Expand All @@ -392,7 +399,12 @@ function addNewWalletAddress(net=1, type='normal'){
var ls = localStorage;
net = (net=='testnet' || net==2) ? 2 : 1;
network = (net==2) ? 'testnet' : 'mainnet',
addrtype = (type=='segwit') ? 7 : 1;
addrtype = 1;
if (type=="segwit"){
addrtype = 7;
} else if (type=="taproot"){
addrtype = 8;
}
address = false;
// Lookup the highest indexed address so far
var idx = 0;
Expand All @@ -413,6 +425,11 @@ function addNewWalletAddress(net=1, type='normal'){
var netname = (net==2) ? 'testnet' : 'bitcoin';
var address = bitcoinjs.payments.p2wpkh({ pubkey: d.publicKey.toBuffer(), network: bitcoinjs.networks[netname] }).address;
label = 'Segwit ' + label;
// Support generating Taproot Addresses (Bech32m)
} else if (type=='taproot'){
var netname = (net==2) ? 'testnet' : 'bitcoin';
var address = bitcoinjs.payments.p2tr({ pubkey: d.publicKey.toBuffer(), network: bitcoinjs.networks[netname] }).address;
label = 'Taproot ' + label;
}
// Add the address to the wallet
addWalletAddress(network, address, label, addrtype, idx);
Expand Down Expand Up @@ -556,7 +573,7 @@ function addWalletAddress( network=1, address='', label='', type=1, index='', pa
address: address, // Address to add
network: network, // Default to mainnet (1=mainnet, 2=testnet)
label: label, // Default to blank label
type: type, // 1=indexed, 2=imported (privkey), 3=watch-only, 4=trezor, 5=ledger, 6=keepkey, 7=segwit
type: type, // 1=indexed, 2=imported (privkey), 3=watch-only, 4=trezor, 5=ledger, 6=keepkey, 7=segwit, 8=taproot
path: path, // node path for address (ex: m/44'/0'/0)
index: index // wallet address index (used in address sorting)
};
Expand Down Expand Up @@ -1615,10 +1632,10 @@ function getSatoshis(amount){
// Handle checking if addresses is bech32
function isBech32(addr) {
try {
bitcoinjs.address.fromBech32(addr)
return true
decoded_address = bitcoinjs.address.fromBech32(addr)
return [true, decoded_address.version]
} catch (e) {
return false
return [false, -1]
}
}

Expand All @@ -1638,7 +1655,7 @@ function getPrivateKey(network, address, prepend=false){
// Check any we have a match in imported addresses
if(FW.WALLET_KEYS[address]){
priv = FW.WALLET_KEYS[address];
if(prepend && isBech32(address))
if(prepend && isBech32(address)[0])
priv = prependStr + priv;
}
// Loop through HD addresses trying to find private key
Expand All @@ -1657,7 +1674,15 @@ function getPrivateKey(network, address, prepend=false){
if(a!=address){
var netname = (network=='testnet') ? 'testnet' : 'bitcoin';
a = bitcoinjs.payments.p2wpkh({ pubkey: d.publicKey.toBuffer(), network: bitcoinjs.networks[netname] }).address;
if(a==address){
if(a!=address){
a = bitcoinjs.payments.p2tr({ pubkey: d.publicKey.toBuffer().slice(1, 33), network: bitcoinjs.networks[netname] }).address;

if(a==address){
priv = d.privateKey.toWIF();
if(prepend)
priv = prependStr + priv;
}
} else {
priv = d.privateKey.toWIF();
if(prepend)
priv = prependStr + priv;
Expand Down Expand Up @@ -2220,6 +2245,9 @@ function updateAddressList(){
// Match segwit address (Bech32)
if(type==5 && (item.type==7))
typeMatch = true;
// Match taproot address (Bech32m)
if(type==6 && (item.type==8))
typeMatch = true;
// Only display if we have both filter and type matches
if(filterMatch && typeMatch){
cnt++;
Expand Down Expand Up @@ -3243,25 +3271,30 @@ function signTransaction(network, source, destination, unsignedTx, callback){
callback(signedTx);
}
// Check if any of the addresses are bech32
var sourceIsBech32 = isBech32(source);
var hasDestBech32 = destination.reduce((p, x) => p || isBech32(x), false);
var [sourceIsBech32, bech32SourceVersion] = isBech32(source);
var hasDestBech32 = destination.reduce((p, x) => p || isBech32(x)[0], false);
var hasAnyBech32 = hasDestBech32 || sourceIsBech32;
// Handle signing bech32 addresses
if(hasAnyBech32){
// Use bitcoinjs implementation
var tx = bitcoinjs.Transaction.fromHex(unsignedTx),
netName = (net=='testnet') ? 'testnet' : 'bitcoin', // bitcoinjs
network = bitcoinjs.networks[netName],
txb = new bitcoinjs.TransactionBuilder(network),
keypair = bitcoinjs.ECPair.fromWIF(cwKey.getWIF(), network);
txb = new bitcoinjs.Psbt(network),
keypair = ECPair.fromWIF(cwKey.getWIF(), network);
//keypair = bitcoinjs.ECPair.fromWIF(cwKey.getWIF(), network);
// Callback to modify transaction after we get a list of UTXOs back
var utxoCb = function(data){
var utxoMap = {};
data.forEach(utxo => {
utxoMap[utxo.txid] = utxo;
});
if(sourceIsBech32){
var input = bitcoinjs.payments.p2wpkh({ pubkey: keypair.publicKey, network: network });
if (bech32SourceVersion == 0){//segwit
var input = bitcoinjs.payments.p2wpkh({ pubkey: keypair.publicKey, network: network });
} else if (bech32SourceVersion == 1){//taproot
var input = bitcoinjs.payments.p2tr({ pubkey: keypair.publicKey.slice(1, 33), network: network });
}
} else {
var input = bitcoinjs.payments.p2pkh({ pubkey: keypair.publicKey, network: network });
}
Expand All @@ -3270,13 +3303,34 @@ function signTransaction(network, source, destination, unsignedTx, callback){
// We get reversed tx hashes somehow after parsing
var txhash = tx.ins[i].hash.reverse().toString('hex');
var prev = utxoMap[txhash];
if(prev)
txb.addInput(tx.ins[i].hash.toString('hex'), prev.vout, null, input.output);
if(prev){
var nextInput = null
if (sourceIsBech32){
var witness = {script:input.output,value:prev.value}
if (bech32SourceVersion == 0){ //segwit
nextInput = {hash:tx.ins[i].hash.toString('hex'), index:prev.vout, witnessUtxo:witness};
} else if (bech32SourceVersion == 1){ //taproot
nextInput = {hash:tx.ins[i].hash.toString('hex'), index:prev.vout, witnessUtxo:witness, tapInternalKey: keypair.publicKey.slice(1,33)};
}
} else {
var prev_tx_hex_buffer = bitcoinjs.Buffer.from(prev.prev_tx_hex,"hex")
nextInput = {hash:tx.ins[i].hash.toString('hex'), index:prev.vout, nonWitnessUtxo:prev_tx_hex_buffer};
}

if (nextInput){
txb.addInput(nextInput);
} else {
console.log("Failed to sign input: script type not implemented")
return
}
}
}
// Handle adding outputs
for(var i=0; i < tx.outs.length; i++){
var txout = tx.outs[i];
txb.addOutput(txout.script, txout.value);
var outputScript = bitcoinjs.Buffer.from(txout.script.buffer.slice(txout.script.byteOffset, txout.script.byteLength + txout.script.byteOffset));

txb.addOutput({script:outputScript, value:txout.value});
}
// var signedHex = txb.build().toHex();
// console.log('signedHex before=',signedHex);
Expand All @@ -3290,7 +3344,8 @@ function signTransaction(network, source, destination, unsignedTx, callback){
redeemScript = // Future support for P2WSH
}*/
// Math.floor is less than ideal in this scenario, we need to get the raw satoshi value in the utxo map
txb.sign(i, keypair, null, null, prev.value, redeemScript);
//txb.sign(i, keypair, null, null, prev.value, redeemScript);
txb.signInput(i, keypair);
} else {
// Throw error that we couldn't sign tx
console.log("Failed to sign transaction: " + "Incomplete SegWit inputs");
Expand All @@ -3300,14 +3355,15 @@ function signTransaction(network, source, destination, unsignedTx, callback){
var signedHex = false,
error = false;
try {
signedHex = txb.build().toHex();
txb.finalizeAllInputs();
signedHex = txb.extractTransaction().toHex();
} catch(e){
error = e;
}
cb(error, signedHex);
}
// Get list of utxo
getUTXOs(net, source, utxoCb);
// Get list of utxo with tx hex
getUtxosWithRawTransactions(net, source, utxoCb);
} else {
// Sign using bitcore
CWBitcore.signRawTransaction(unsignedTx, cwKey, cb);
Expand Down Expand Up @@ -3362,6 +3418,54 @@ function getUTXOs(network, address, callback){
});
}

// Handle getting a raw transaction for a given tx hash
function getRawTransactions(network, hashList, callback){
var txs = [];
var data = {
method: "getrawtransaction_batch",
params: {
txhash_list: hashList
},
jsonrpc: "2.0",
id: 0
};
cpRequest(network, data, function(o){
if(o && o.result){
if(callback)
callback(o.result);
};
});
}

//Handle getting utxos and their respectives prevout tx hex
function getUtxosWithRawTransactions(network, address, callback){
var utxosCb = function(dataUtxos){
//Adding hex data to the utxos
var rawTransactionCb = function (dataRawTransactions){
for (var nextUtxoIndex in dataUtxos){
var nextUtxo = dataUtxos[nextUtxoIndex]
nextUtxo["prev_tx_hex"] = dataRawTransactions[nextUtxo.txid]
}

if (callback){
callback(dataUtxos)
}
}

//Preparing the list with all obtained utxos hashes
var hashList = []
for (var nextUtxoIndex in dataUtxos){
var nextUtxo = dataUtxos[nextUtxoIndex]
hashList.push(nextUtxo.txid)
}

getRawTransactions(network, hashList, rawTransactionCb)
}

//Obtaining utxos
getUTXOs(network, address, utxosCb)
}

// Handle signing a message and returning the signature
function signMessage(network, source, message){
var net = (network=='testnet') ? 'testnet' : 'mainnet',
Expand Down Expand Up @@ -4621,8 +4725,8 @@ function displayContextMenu(event){
dialogViewAddress(addr);
}
}));
// Display 'View Private Key' for certain addresses (1=normal, 2=imported, 7=segwit)
if([1,2,7].indexOf(info.type)!=-1){
// Display 'View Private Key' for certain addresses (1=normal, 2=imported, 7=segwit, 8=taproot)
if([1,2,7,8].indexOf(info.type)!=-1){
mnu.append(new nw.MenuItem({
label: 'View Private Key',
click: function(){
Expand Down Expand Up @@ -6480,9 +6584,9 @@ function checkDonate(network, source, destination, unsignedTx){
if(typeof(destination)==='string')
destination = [destination];
// Check if any of the addresses are bech32
var sourceIsBech32 = isBech32(source),
destIsBech32 = destination.reduce((p, x) => p || isBech32(x), false),
donateisBech32 = isBech32(FW.DONATE_ADDRESS),
var [sourceIsBech32, sourceBech32Version] = isBech32(source),
destIsBech32 = destination.reduce((p, x) => p || isBech32(x)[0], false),
donateisBech32 = isBech32(FW.DONATE_ADDRESS)[0],
hasAnyBech32 = sourceIsBech32 || destIsBech32 || donateisBech32;
// Handle updating transaction
if(hasAnyBech32){
Expand Down