package com.msgilligan.bitcoinj.test;
import com.msgilligan.bitcoinj.rpc.BitcoinExtendedClient;
import com.msgilligan.bitcoinj.rpc.JsonRPCException;
import com.msgilligan.bitcoinj.json.pojo.Outpoint;
import com.msgilligan.bitcoinj.json.pojo.SignedRawTransaction;
import com.msgilligan.bitcoinj.json.pojo.TxOutInfo;
import com.msgilligan.bitcoinj.json.pojo.UnspentOutput;
import com.msgilligan.bitcoinj.json.conversion.BitcoinMath;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* FundingSource using RegTest mining
*/
public class RegTestFundingSource implements FundingSource {
final Integer defaultMaxConf = 9999999;
private static final Logger log = LoggerFactory.getLogger(RegTestFundingSource.class);
protected BitcoinExtendedClient client;
public RegTestFundingSource(BitcoinExtendedClient client) {
this.client = client;
}
/**
* Generate blocks and fund an address with requested amount of BTC
*
* TODO: Improve performance. Can we mine multiple blocks with a single RPC?
*
* @param toAddress Address to fund with BTC
* @param requestedBtc Amount of BTC to "mine" and send
* @return The hash of transaction that provided the funds.
*/
@Override
public Sha256Hash requestBitcoin(Address toAddress, Coin requestedBtc) throws JsonRPCException, IOException {
log.debug("requestBitcoin requesting {}", requestedBtc);
if (requestedBtc.value > NetworkParameters.MAX_MONEY.value) {
throw new IllegalArgumentException("request exceeds MAX_MONEY");
}
long amountGatheredSoFar = 0;
ArrayList<Outpoint> inputs = new ArrayList<Outpoint>();
// Newly mined coins need to mature to be spendable
final int minCoinAge = 100;
if (client.getBlockCount() < minCoinAge) {
client.generate(minCoinAge - client.getBlockCount());
}
while (amountGatheredSoFar < requestedBtc.value) {
client.generate();
int blockIndex = client.getBlockCount() - minCoinAge;
Block block = client.getBlock(blockIndex);
List<Transaction> blockTxs = block.getTransactions();
Sha256Hash coinbaseTx = blockTxs.get(0).getHash();
TxOutInfo txout = client.getTxOut(coinbaseTx, 0);
// txout is empty, if output was already spent
if (txout != null && txout.getValue().value > 0) {
log.debug("txout = {}, value = {}", txout, txout.getValue().value);
amountGatheredSoFar += txout.getValue().value;
inputs.add(new Outpoint(coinbaseTx, 0));
}
log.debug("amountGatheredSoFar = {}", BitcoinMath.satoshiToBtc(amountGatheredSoFar));
}
// Don't care about change, we mine it anyway
String unsignedTxHex = client.createRawTransaction(inputs, Collections.singletonMap(toAddress, requestedBtc));
SignedRawTransaction signingResult = client.signRawTransaction(unsignedTxHex);
assert signingResult.isComplete();
String signedTxHex = signingResult.getHex();
Sha256Hash txid = client.sendRawTransaction(signedTxHex, true);
return txid;
}
/**
* Create an address and fund it with bitcoin
*
* @param amount
* @return Newly created address with the requested amount of bitcoin
*/
public Address createFundedAddress(Coin amount) throws Exception {
Address address = client.getNewAddress();
requestBitcoin(address, amount);
return address;
}
/**
* Create everything needed to assemble a custom transaction
* @param amount Amount of BTC to be available on new address
* @return An address, private key, and list of unspent outputs
* @throws JsonRPCException
* @throws IOException
*/
public TransactionIngredients createIngredients(Coin amount) throws JsonRPCException, IOException {
TransactionIngredients ingredients = new TransactionIngredients();
Address address = client.getNewAddress();
requestBitcoin(address, amount);
ingredients.address = address;
ingredients.privateKey = client.dumpPrivKey(address);
ingredients.outPoints = client.listUnspentOutPoints(address);
return ingredients;
}
@Override
public void fundingSourceMaintenance() {
try {
consolidateCoins();
} catch (JsonRPCException e) {
e.printStackTrace();
throw new RuntimeException(e);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* Collects *all* unspent outputs and spends the whole amount minus `stdRelayTxFee`, which is sent
* to a new address, as fee, to sweep dust and to minimize the number of unspent outputs, to avoid creating too
* large transactions. No new block is generated afterwards.
*
* Can be used in cleanupSpec() methods of integration tests.
*
* @see <a href="https://github.com/OmniLayer/OmniJ/issues/50">Issue #50 on GitHub</a>
*
* @return True, if enough outputs with a value of at least {@code stdRelayTxFee} were spent
*/
void consolidateCoins() throws JsonRPCException, IOException {
long amountIn = 0;
List<Outpoint> inputs = new ArrayList<Outpoint>();
List<UnspentOutput> unspentOutputs = client.listUnspent(1,defaultMaxConf);
// Gather inputs
for (UnspentOutput unspentOutput : unspentOutputs) {
amountIn += unspentOutput.getAmount().value;
inputs.add(new Outpoint(unspentOutput.getTxid(), unspentOutput.getVout()));
}
// Check if there is a sufficient high amount to sweep at all
if (amountIn < client.stdRelayTxFee.value) {
return; //false;
}
// No receiver, just spend most of it as fee (!)
Map<Address,Coin> outputs = new HashMap<Address, Coin>();
outputs.put(client.getNewAddress(), client.stdRelayTxFee);
String unsignedTxHex = client.createRawTransaction(inputs, outputs);
SignedRawTransaction signingResult = client.signRawTransaction(unsignedTxHex);
Boolean complete = signingResult.isComplete();
assert complete;
String signedTxHex = signingResult.getHex();
Object txid = client.sendRawTransaction(signedTxHex, true);
return; //true;
}
}