I had a similar issue for a store I had where I was accepting bitcoin. I ended up creating a nodejs app that would take the input of just a private key and an address. It would gather all the UTXOs that belong to the private key using a third party API, and then send ALL funds to the address specified. Back then I was using bitcore-lib, but that library is now outdated, I would use bitcoin-js if I re-did it today.
These are the basic functions I used:
app.post("/address", function(req,res){
var pkey = req.body.pkey;
var addr = req.body.addy;
validateAddress(addr, function(isAddyValid){
if(isAddyValid == 0){
res.render("pages/index.ejs", {
outMessage: "Destination address invalid"
});
} else {
validatePrivateKey(pkey, function(isPkValid){
if(isPkValid == 0){
res.render("pages/index.ejs", {
outMessage: "Private Key invalid"
});
} else {
convertPK(pkey, function(convertedAddy){
getUTXO(convertedAddy, function(result, feeAmt, totalToSend){
if(result == 1){
res.render("pages/index.ejs", {
outMessage: "No UTXO"
});
} else if(result == 2){
res.render("pages/index.ejs", {
outMessage: "Source offline"
});
} else if(result == 3){
res.render("pages/index.ejs", {
outMessage: "Insufficient funds to pay fee"
});
} else {
buildTX(result, feeAmt, totalToSend, pkey, addr, function(payloadTx){
pushTX(payloadTx, function(txdone){
if(txdone !== 1){
res.render("pages/index.ejs", {
outMessage: "TX ID: " + txdone
});
} else {
res.render("pages/index.ejs", {
outMessage: "broadcast failed try later"
});
}
});
});
}
});
});
}
});//end validatePrivateKey
}
});//end validate address
}); //end app post
function validateAddress(output, result){
addyValue = output.replace(/[^\w\s]/gi, '');
if(bitcore.Address.isValid(addyValue)){
result(1);
} else {
result(0);
};
};
//convert pk to addr
function convertPK(pkeyValue, result){
var address = new bitcore.PrivateKey(pkeyValue).toAddress();
result(address);
};
function validatePrivateKey(wif, result){
pkeyValue = wif.replace(/[^\w\s]/gi, '');
if(bitcore.PrivateKey.isValid(pkeyValue)){
//private key is valid
result(1);
} else {
result(0);
};
};
//get outputs
function getUTXO(address, callback){
request({
url: "https://chain.so/api/v2/get_tx_unspent/btc/"+address,
json: true
}, function(error, response, body){
if(!error && response.statusCode == 200){
if(body.data.txs.length < 1){
//no utxos
console.log("no utxo");
var err = 1;
callback(err);
}
var status = body.status;
var num = body.data.txs.length;
var utxos = [];
var totalSats = 0;
var txSize = 44;
//loop through all UTXOs
for(i=0;i < num; i++){
var convertSats = body.data.txs[i].value * 100000000;
convertSats = parseInt(convertSats);
var utxo = {
"txId": body.data.txs[i].txid,
"outputIndex": body.data.txs[i].output_no,
"address": address,
"script": body.data.txs[i].script_hex,
"satoshis": convertSats
};
utxos.push(utxo);
totalSats = totalSats + convertSats;
//calc tx size for fee
txSize = txSize + 180;
}; //end utxo loop
getBestFee(function(bestHourFee){
var fee = txSize * bestHourFee;
totalSats = totalSats - fee;
console.log(totalSats);
console.log(fee);
if(totalSats < 1){
//not enough funds to send
var err = 3;
callback(err);
} else {
callback(utxos, fee, totalSats);
}
});
} else {
//err or no response from api
console.log("no response from api");
var error = 2;
callback(error);
}
});
};
//build transaction
function buildTX(utxo, fee, total, pkeyValue, output, callback){
var transaction = new bitcore.Transaction()
.from(utxo)
.to(output, total)
.sign(pkeyValue);
//payload to push tx
var txjson = transaction.toString();
var pload = {
"tx": txjson
};
callback(pload);
};
//push transaction
function pushTX(pload, callback){
request({
url: "https://api.blockcypher.com/v1/btc/main/txs/push",
method: "POST",
json: true,
headers: {"content-type": "application/json"},
body: pload
}, function(err, response, body){
if(err){
//no response or error POST to chainso
callback(1);
} else {
console.log(JSON.stringify(body));
completeTxId = body.tx.hash;
console.log("done");
callback(completeTxId);
};
});
};
function getBestFee(bestFee){
var findfee = "https://bitcoinfees.21.co/api/v1/fees/recommended";
request({
url: findfee,
json: true
}, function(error, response, body){
if(!body.hourFee){
//no response from api use 150 sats per byte
var fee = 150;
bestFee(fee);
}
if(body.hourFee){
var fee = body.hourFee;
fee = fee * 0.5;
fee = Math.ceil(fee);
bestFee(fee);
}
});
}
If you want to see the entire project here's the repo, it's a simple nodejs express app with a form submission POST option and a GET endpoint /sweep
option. Be aware the way I wrote it, it will only work with legacy P2PKH addresses (addresses that start with a 1), no segwit.
https://github.com/coinables/sweepkey