forked from Qortal/qortal
WIP: split P2SH from BTCACCT, add more fields to TradeBotData, remove initial QORT payout
This commit is contained in:
parent
a6fa4fc613
commit
da254058c5
@ -12,13 +12,9 @@ public class CrossChainBuildRequest {
|
|||||||
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
|
||||||
public byte[] creatorPublicKey;
|
public byte[] creatorPublicKey;
|
||||||
|
|
||||||
@Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000")
|
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
|
||||||
public long initialQortAmount;
|
|
||||||
|
|
||||||
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
|
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public long finalQortAmount;
|
public long qortAmount;
|
||||||
|
|
||||||
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
|
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
@ -48,6 +48,7 @@ import org.qortal.asset.Asset;
|
|||||||
import org.qortal.controller.TradeBot;
|
import org.qortal.controller.TradeBot;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
@ -162,17 +163,14 @@ public class CrossChainResource {
|
|||||||
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
|
if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
if (tradeRequest.initialQortAmount < 0)
|
if (tradeRequest.qortAmount <= 0)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
|
||||||
|
|
||||||
if (tradeRequest.finalQortAmount <= 0)
|
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
if (tradeRequest.fundingQortAmount <= 0)
|
if (tradeRequest.fundingQortAmount <= 0)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
// funding amount must exceed initial + final
|
// funding amount must exceed initial + final
|
||||||
if (tradeRequest.fundingQortAmount <= tradeRequest.initialQortAmount + tradeRequest.finalQortAmount)
|
if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
if (tradeRequest.bitcoinAmount <= 0)
|
if (tradeRequest.bitcoinAmount <= 0)
|
||||||
@ -182,7 +180,7 @@ public class CrossChainResource {
|
|||||||
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
|
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
|
||||||
|
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.secretHash,
|
byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.secretHash,
|
||||||
tradeRequest.tradeTimeout, tradeRequest.initialQortAmount, tradeRequest.finalQortAmount, tradeRequest.bitcoinAmount);
|
tradeRequest.tradeTimeout, tradeRequest.qortAmount, tradeRequest.bitcoinAmount);
|
||||||
|
|
||||||
long txTimestamp = NTP.getTime();
|
long txTimestamp = NTP.getTime();
|
||||||
byte[] lastReference = creatorAccount.getLastReference();
|
byte[] lastReference = creatorAccount.getLastReference();
|
||||||
@ -434,7 +432,7 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -485,7 +483,7 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -516,7 +514,7 @@ public class CrossChainResource {
|
|||||||
if (now >= medianBlockTime * 1000L) {
|
if (now >= medianBlockTime * 1000L) {
|
||||||
// See if we can extract secret
|
// See if we can extract secret
|
||||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress);
|
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress);
|
||||||
p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions);
|
p2shStatus.secret = BTCP2SH.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return p2shStatus;
|
return p2shStatus;
|
||||||
@ -582,7 +580,7 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -608,7 +606,7 @@ public class CrossChainResource {
|
|||||||
|
|
||||||
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
||||||
|
|
||||||
org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime);
|
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime);
|
||||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
||||||
|
|
||||||
if (!wasBroadcast)
|
if (!wasBroadcast)
|
||||||
@ -680,7 +678,7 @@ public class CrossChainResource {
|
|||||||
if (crossChainTradeData.mode == Mode.OFFER)
|
if (crossChainTradeData.mode == Mode.OFFER)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -709,7 +707,7 @@ public class CrossChainResource {
|
|||||||
|
|
||||||
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue());
|
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue());
|
||||||
|
|
||||||
org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret);
|
org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret);
|
||||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
|
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
|
||||||
|
|
||||||
if (!wasBroadcast)
|
if (!wasBroadcast)
|
||||||
|
@ -8,17 +8,16 @@ import java.util.Random;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.model.TradeBotCreateRequest;
|
import org.qortal.api.model.TradeBotCreateRequest;
|
||||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
@ -32,8 +31,8 @@ import org.qortal.repository.Repository;
|
|||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.transaction.DeployAtTransaction;
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
import org.qortal.transaction.MessageTransaction;
|
import org.qortal.transaction.MessageTransaction;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ public class TradeBot {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) {
|
public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
|
||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
NetworkParameters params = btc.getNetworkParameters();
|
NetworkParameters params = btc.getNetworkParameters();
|
||||||
|
|
||||||
@ -90,12 +89,18 @@ public class TradeBot {
|
|||||||
String atAddress = deployAtTransactionData.getAtAddress();
|
String atAddress = deployAtTransactionData.getAtAddress();
|
||||||
|
|
||||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.BOB_WAITING_FOR_MESSAGE,
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.BOB_WAITING_FOR_MESSAGE,
|
||||||
|
atAddress, tradeBotCreateRequest.tradeTimeout,
|
||||||
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
|
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, null);
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
tradeBotCreateRequest.bitcoinAmount, null);
|
||||||
repository.getCrossChainRepository().save(tradeBotData);
|
repository.getCrossChainRepository().save(tradeBotData);
|
||||||
|
|
||||||
// Return to user for signing and broadcast as we don't have their Qortal private key
|
// Return to user for signing and broadcast as we don't have their Qortal private key
|
||||||
|
try {
|
||||||
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
|
||||||
@ -113,12 +118,14 @@ public class TradeBot {
|
|||||||
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
|
||||||
|
|
||||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A,
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A,
|
||||||
|
crossChainTradeData.qortalAtAddress, crossChainTradeData.tradeTimeout,
|
||||||
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
|
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash, crossChainTradeData.qortalAtAddress, null);
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
crossChainTradeData.expectedBitcoin, null);
|
||||||
repository.getCrossChainRepository().save(tradeBotData);
|
repository.getCrossChainRepository().save(tradeBotData);
|
||||||
|
|
||||||
// P2SH_a to be funded
|
// P2SH_a to be funded
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash);
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
@ -176,7 +183,7 @@ public class TradeBot {
|
|||||||
repository.getCrossChainRepository().save(tradeBotData);
|
repository.getCrossChainRepository().save(tradeBotData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) {
|
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||||
// Fetch AT so we can determine trade start timestamp
|
// Fetch AT so we can determine trade start timestamp
|
||||||
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
||||||
if (atData == null) {
|
if (atData == null) {
|
||||||
@ -220,7 +227,7 @@ public class TradeBot {
|
|||||||
|
|
||||||
// Determine P2SH address and confirm funded
|
// Determine P2SH address and confirm funded
|
||||||
int lockTime = (int) (tradeStartTimestamp / 1000L + tradeBotData.getTradeTimeout() / 4 * 60); // First P2SH locktime is ¼ of timeout period
|
int lockTime = (int) (tradeStartTimestamp / 1000L + tradeBotData.getTradeTimeout() / 4 * 60); // First P2SH locktime is ¼ of timeout period
|
||||||
byte[] redeemScript = BTCACCT.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
|
byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
|
||||||
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
|
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
|
||||||
|
|
||||||
Long balance = BTC.getInstance().getBalance(p2shAddress);
|
Long balance = BTC.getInstance().getBalance(p2shAddress);
|
||||||
|
@ -4,23 +4,7 @@ import static org.ciyam.at.OpCode.calcOffset;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import org.bitcoinj.core.Address;
|
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
import org.bitcoinj.core.ECKey;
|
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
|
||||||
import org.bitcoinj.core.Transaction;
|
|
||||||
import org.bitcoinj.core.Transaction.SigHash;
|
|
||||||
import org.bitcoinj.core.TransactionInput;
|
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
|
||||||
import org.bitcoinj.crypto.TransactionSignature;
|
|
||||||
import org.bitcoinj.script.Script;
|
|
||||||
import org.bitcoinj.script.ScriptBuilder;
|
|
||||||
import org.bitcoinj.script.ScriptChunk;
|
|
||||||
import org.bitcoinj.script.ScriptOpCodes;
|
|
||||||
import org.ciyam.at.API;
|
import org.ciyam.at.API;
|
||||||
import org.ciyam.at.CompilationException;
|
import org.ciyam.at.CompilationException;
|
||||||
import org.ciyam.at.FunctionCode;
|
import org.ciyam.at.FunctionCode;
|
||||||
@ -40,7 +24,6 @@ import org.qortal.data.crosschain.CrossChainTradeData;
|
|||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.BitTwiddling;
|
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
@ -72,164 +55,6 @@ public class BTCACCT {
|
|||||||
public static final int MIN_LOCKTIME = 1500000000;
|
public static final int MIN_LOCKTIME = 1500000000;
|
||||||
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes
|
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes
|
||||||
|
|
||||||
/*
|
|
||||||
* OP_TUCK (to copy public key to before signature)
|
|
||||||
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
|
|
||||||
* OP_HASH160 (convert public key to PKH)
|
|
||||||
* OP_DUP (duplicate PKH)
|
|
||||||
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
|
|
||||||
* OP_IF
|
|
||||||
* OP_DROP (no need for duplicate PKH)
|
|
||||||
* <push 4 bytes> <locktime>
|
|
||||||
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
|
|
||||||
* OP_ELSE
|
|
||||||
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
|
|
||||||
* OP_HASH160 (hash secret)
|
|
||||||
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
|
|
||||||
* OP_ENDIF
|
|
||||||
*/
|
|
||||||
|
|
||||||
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
|
|
||||||
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
|
|
||||||
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
|
|
||||||
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
|
|
||||||
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns Bitcoin redeemScript used for cross-chain trading.
|
|
||||||
* <p>
|
|
||||||
* See comments in {@link BTCACCT} for more details.
|
|
||||||
*
|
|
||||||
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
|
||||||
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
|
||||||
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
|
||||||
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
|
|
||||||
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
|
||||||
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a custom transaction to spend P2SH.
|
|
||||||
*
|
|
||||||
* @param amount output amount, should be total of input amounts, less miner fees
|
|
||||||
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
|
|
||||||
* @param fundingOutput output from transaction that funded P2SH address
|
|
||||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
|
||||||
* @param lockTime (optional) transaction nLockTime, used in refund scenario
|
|
||||||
* @param scriptSigBuilder function for building scriptSig using transaction input signature
|
|
||||||
* @return Signed Bitcoin transaction for spending P2SH
|
|
||||||
*/
|
|
||||||
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) {
|
|
||||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
|
||||||
|
|
||||||
Transaction transaction = new Transaction(params);
|
|
||||||
transaction.setVersion(2);
|
|
||||||
|
|
||||||
// Output is back to P2SH funder
|
|
||||||
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash()));
|
|
||||||
|
|
||||||
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
|
||||||
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
|
|
||||||
|
|
||||||
// Input (without scriptSig prior to signing)
|
|
||||||
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
|
|
||||||
if (lockTime != null)
|
|
||||||
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
|
|
||||||
else
|
|
||||||
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
|
|
||||||
transaction.addInput(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set locktime after inputs added but before input signatures are generated
|
|
||||||
if (lockTime != null)
|
|
||||||
transaction.setLockTime(lockTime);
|
|
||||||
|
|
||||||
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
|
||||||
// Generate transaction signature for input
|
|
||||||
final boolean anyoneCanPay = false;
|
|
||||||
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
|
||||||
|
|
||||||
// Calculate transaction signature
|
|
||||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
|
||||||
|
|
||||||
// Build scriptSig using lambda and tx signature
|
|
||||||
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
|
|
||||||
|
|
||||||
// Set input scriptSig
|
|
||||||
transaction.getInput(inputIndex).setScriptSig(scriptSig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns signed Bitcoin transaction claiming refund from P2SH address.
|
|
||||||
*
|
|
||||||
* @param refundAmount refund amount, should be total of input amounts, less miner fees
|
|
||||||
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
|
|
||||||
* @param fundingOutput output from transaction that funded P2SH address
|
|
||||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
|
||||||
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
|
|
||||||
* @return Signed Bitcoin transaction for refunding P2SH
|
|
||||||
*/
|
|
||||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
|
|
||||||
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
|
||||||
// Build scriptSig with...
|
|
||||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
|
||||||
|
|
||||||
// transaction signature
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
|
||||||
|
|
||||||
// redeem public key
|
|
||||||
byte[] refundPubKey = refundKey.getPubKey();
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
|
|
||||||
|
|
||||||
// redeem script
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
|
||||||
|
|
||||||
return scriptBuilder.build();
|
|
||||||
};
|
|
||||||
|
|
||||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
|
|
||||||
*
|
|
||||||
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
|
|
||||||
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
|
|
||||||
* @param fundingOutput output from transaction that funded P2SH address
|
|
||||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
|
||||||
* @param secret actual 32-byte secret used when building redeemScript
|
|
||||||
* @return Signed Bitcoin transaction for redeeming P2SH
|
|
||||||
*/
|
|
||||||
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret) {
|
|
||||||
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
|
|
||||||
// Build scriptSig with...
|
|
||||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
|
||||||
|
|
||||||
// secret
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
|
|
||||||
|
|
||||||
// transaction signature
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
|
||||||
|
|
||||||
// redeem public key
|
|
||||||
byte[] redeemPubKey = redeemKey.getPubKey();
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
|
|
||||||
|
|
||||||
// redeem script
|
|
||||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
|
||||||
|
|
||||||
return scriptBuilder.build();
|
|
||||||
};
|
|
||||||
|
|
||||||
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns Qortal AT creation bytes for cross-chain trading AT.
|
* Returns Qortal AT creation bytes for cross-chain trading AT.
|
||||||
* <p>
|
* <p>
|
||||||
@ -240,12 +65,11 @@ public class BTCACCT {
|
|||||||
* @param bitcoinPublicKeyHash 20-byte HASH160 of creator's bitcoin public key
|
* @param bitcoinPublicKeyHash 20-byte HASH160 of creator's bitcoin public key
|
||||||
* @param secretHash 20-byte HASH160 of 32-byte secret
|
* @param secretHash 20-byte HASH160 of 32-byte secret
|
||||||
* @param tradeTimeout how many minutes, from start of 'trade mode' until AT auto-refunds AT creator
|
* @param tradeTimeout how many minutes, from start of 'trade mode' until AT auto-refunds AT creator
|
||||||
* @param initialPayout how much QORT to pay trade partner upon switch to 'trade mode'
|
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secret to AT
|
||||||
* @param redeemPayout how much QORT to pay trade partner if they send correct 32-byte secret to AT
|
|
||||||
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
|
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static byte[] buildQortalAT(String qortalCreator, byte[] bitcoinPublicKeyHash, byte[] secretHash, int tradeTimeout, long initialPayout, long redeemPayout, long bitcoinAmount) {
|
public static byte[] buildQortalAT(String qortalCreator, byte[] bitcoinPublicKeyHash, byte[] secretHash, int tradeTimeout, long qortAmount, long bitcoinAmount) {
|
||||||
// Labels for data segment addresses
|
// Labels for data segment addresses
|
||||||
int addrCounter = 0;
|
int addrCounter = 0;
|
||||||
|
|
||||||
@ -263,8 +87,7 @@ public class BTCACCT {
|
|||||||
addrCounter += 4;
|
addrCounter += 4;
|
||||||
|
|
||||||
final int addrTradeTimeout = addrCounter++;
|
final int addrTradeTimeout = addrCounter++;
|
||||||
final int addrInitialPayoutAmount = addrCounter++;
|
final int addrQortAmount = addrCounter++;
|
||||||
final int addrRedeemPayoutAmount = addrCounter++;
|
|
||||||
final int addrBitcoinAmount = addrCounter++;
|
final int addrBitcoinAmount = addrCounter++;
|
||||||
|
|
||||||
final int addrMessageTxType = addrCounter++;
|
final int addrMessageTxType = addrCounter++;
|
||||||
@ -319,13 +142,9 @@ public class BTCACCT {
|
|||||||
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
|
||||||
dataByteBuffer.putLong(tradeTimeout);
|
dataByteBuffer.putLong(tradeTimeout);
|
||||||
|
|
||||||
// Initial payout amount
|
// Redeem Qort amount
|
||||||
assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
|
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
|
||||||
dataByteBuffer.putLong(initialPayout);
|
dataByteBuffer.putLong(qortAmount);
|
||||||
|
|
||||||
// Redeem payout amount
|
|
||||||
assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect";
|
|
||||||
dataByteBuffer.putLong(redeemPayout);
|
|
||||||
|
|
||||||
// Expected Bitcoin amount
|
// Expected Bitcoin amount
|
||||||
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
|
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
|
||||||
@ -433,9 +252,6 @@ public class BTCACCT {
|
|||||||
/* Switch to 'trade mode' */
|
/* Switch to 'trade mode' */
|
||||||
labelTradeMode = codeByteBuffer.position();
|
labelTradeMode = codeByteBuffer.position();
|
||||||
|
|
||||||
// Send initial payment to recipient so they have enough funds to message AT if all goes well
|
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount));
|
|
||||||
|
|
||||||
// Calculate trade timeout refund 'timestamp' by adding addrTradeTimeout minutes to above message's 'timestamp', then save into addrTradeRefundTimestamp
|
// Calculate trade timeout refund 'timestamp' by adding addrTradeTimeout minutes to above message's 'timestamp', then save into addrTradeRefundTimestamp
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrTradeRefundTimestamp, addrLastTxTimestamp, addrTradeTimeout));
|
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrTradeRefundTimestamp, addrLastTxTimestamp, addrTradeTimeout));
|
||||||
|
|
||||||
@ -504,7 +320,7 @@ public class BTCACCT {
|
|||||||
// Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer)
|
// Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer)
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer));
|
||||||
// Pay AT's balance to recipient
|
// Pay AT's balance to recipient
|
||||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount));
|
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
|
||||||
// Fall-through to refunding any remaining balance back to AT creator
|
// Fall-through to refunding any remaining balance back to AT creator
|
||||||
|
|
||||||
/* Refund balance back to AT creator */
|
/* Refund balance back to AT creator */
|
||||||
@ -578,13 +394,10 @@ public class BTCACCT {
|
|||||||
dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes
|
dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes
|
||||||
|
|
||||||
// Trade timeout
|
// Trade timeout
|
||||||
tradeData.tradeRefundTimeout = dataByteBuffer.getLong();
|
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Initial payout
|
|
||||||
tradeData.initialPayout = dataByteBuffer.getLong();
|
|
||||||
|
|
||||||
// Redeem payout
|
// Redeem payout
|
||||||
tradeData.redeemPayout = dataByteBuffer.getLong();
|
tradeData.qortAmount = dataByteBuffer.getLong();
|
||||||
|
|
||||||
// Expected BTC amount
|
// Expected BTC amount
|
||||||
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
tradeData.expectedBitcoin = dataByteBuffer.getLong();
|
||||||
@ -623,12 +436,12 @@ public class BTCACCT {
|
|||||||
// We'll suggest half of trade timeout
|
// We'll suggest half of trade timeout
|
||||||
CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
||||||
|
|
||||||
int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeRefundTimeout / ciyamAtSettings.minutesPerBlock);
|
int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeTimeout / ciyamAtSettings.minutesPerBlock);
|
||||||
|
|
||||||
BlockData blockData = repository.getBlockRepository().fromHeight(tradeModeSwitchHeight);
|
BlockData blockData = repository.getBlockRepository().fromHeight(tradeModeSwitchHeight);
|
||||||
if (blockData != null) {
|
if (blockData != null) {
|
||||||
tradeData.tradeModeTimestamp = blockData.getTimestamp(); // NOTE: milliseconds from epoch
|
tradeData.tradeModeTimestamp = blockData.getTimestamp(); // NOTE: milliseconds from epoch
|
||||||
tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeRefundTimeout / 2 * 60);
|
tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeTimeout / 2 * 60);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tradeData.mode = CrossChainTradeData.Mode.OFFER;
|
tradeData.mode = CrossChainTradeData.Mode.OFFER;
|
||||||
@ -637,46 +450,4 @@ public class BTCACCT {
|
|||||||
return tradeData;
|
return tradeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
|
|
||||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
|
||||||
|
|
||||||
for (byte[] rawTransaction : rawTransactions) {
|
|
||||||
Transaction transaction = new Transaction(params, rawTransaction);
|
|
||||||
|
|
||||||
// Cycle through inputs, looking for one that spends our P2SH
|
|
||||||
for (TransactionInput input : transaction.getInputs()) {
|
|
||||||
Script scriptSig = input.getScriptSig();
|
|
||||||
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
|
||||||
|
|
||||||
// Expected number of script chunks for redeem. Refund might not have the same number.
|
|
||||||
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
|
|
||||||
if (scriptChunks.size() != expectedChunkCount)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// We're expecting last chunk to contain the actual redeemScript
|
|
||||||
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
|
|
||||||
byte[] redeemScriptBytes = lastChunk.data;
|
|
||||||
|
|
||||||
// If non-push scripts, redeemScript will be null
|
|
||||||
if (redeemScriptBytes == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
|
||||||
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
|
||||||
|
|
||||||
if (!inputAddress.toString().equals(p2shAddress))
|
|
||||||
// Input isn't spending our P2SH
|
|
||||||
continue;
|
|
||||||
|
|
||||||
byte[] secret = scriptChunks.get(0).data;
|
|
||||||
if (secret.length != BTCACCT.SECRET_LENGTH)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
232
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
232
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Address;
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
import org.bitcoinj.core.Transaction.SigHash;
|
||||||
|
import org.bitcoinj.core.TransactionInput;
|
||||||
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
|
import org.bitcoinj.crypto.TransactionSignature;
|
||||||
|
import org.bitcoinj.script.Script;
|
||||||
|
import org.bitcoinj.script.ScriptBuilder;
|
||||||
|
import org.bitcoinj.script.ScriptChunk;
|
||||||
|
import org.bitcoinj.script.ScriptOpCodes;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
|
||||||
|
public class BTCP2SH {
|
||||||
|
|
||||||
|
public static final int SECRET_LENGTH = 32;
|
||||||
|
public static final int MIN_LOCKTIME = 1500000000;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OP_TUCK (to copy public key to before signature)
|
||||||
|
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
|
||||||
|
* OP_HASH160 (convert public key to PKH)
|
||||||
|
* OP_DUP (duplicate PKH)
|
||||||
|
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
|
||||||
|
* OP_IF
|
||||||
|
* OP_DROP (no need for duplicate PKH)
|
||||||
|
* <push 4 bytes> <locktime>
|
||||||
|
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
|
||||||
|
* OP_ELSE
|
||||||
|
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
|
||||||
|
* OP_HASH160 (hash secret)
|
||||||
|
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
|
||||||
|
* OP_ENDIF
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
|
||||||
|
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
|
||||||
|
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
|
||||||
|
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
|
||||||
|
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Bitcoin redeemScript used for cross-chain trading.
|
||||||
|
* <p>
|
||||||
|
* See comments in {@link BTCP2SH} for more details.
|
||||||
|
*
|
||||||
|
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
|
||||||
|
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
|
||||||
|
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
|
||||||
|
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
|
||||||
|
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
||||||
|
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a custom transaction to spend P2SH.
|
||||||
|
*
|
||||||
|
* @param amount output amount, should be total of input amounts, less miner fees
|
||||||
|
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
|
||||||
|
* @param fundingOutput output from transaction that funded P2SH address
|
||||||
|
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||||
|
* @param lockTime (optional) transaction nLockTime, used in refund scenario
|
||||||
|
* @param scriptSigBuilder function for building scriptSig using transaction input signature
|
||||||
|
* @return Signed Bitcoin transaction for spending P2SH
|
||||||
|
*/
|
||||||
|
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) {
|
||||||
|
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||||
|
|
||||||
|
Transaction transaction = new Transaction(params);
|
||||||
|
transaction.setVersion(2);
|
||||||
|
|
||||||
|
// Output is back to P2SH funder
|
||||||
|
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash()));
|
||||||
|
|
||||||
|
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
||||||
|
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
|
||||||
|
|
||||||
|
// Input (without scriptSig prior to signing)
|
||||||
|
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
|
||||||
|
if (lockTime != null)
|
||||||
|
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
|
||||||
|
else
|
||||||
|
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
|
||||||
|
transaction.addInput(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set locktime after inputs added but before input signatures are generated
|
||||||
|
if (lockTime != null)
|
||||||
|
transaction.setLockTime(lockTime);
|
||||||
|
|
||||||
|
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
|
||||||
|
// Generate transaction signature for input
|
||||||
|
final boolean anyoneCanPay = false;
|
||||||
|
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||||
|
|
||||||
|
// Calculate transaction signature
|
||||||
|
byte[] txSigBytes = txSig.encodeToBitcoin();
|
||||||
|
|
||||||
|
// Build scriptSig using lambda and tx signature
|
||||||
|
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
|
||||||
|
|
||||||
|
// Set input scriptSig
|
||||||
|
transaction.getInput(inputIndex).setScriptSig(scriptSig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns signed Bitcoin transaction claiming refund from P2SH address.
|
||||||
|
*
|
||||||
|
* @param refundAmount refund amount, should be total of input amounts, less miner fees
|
||||||
|
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
|
||||||
|
* @param fundingOutput output from transaction that funded P2SH address
|
||||||
|
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||||
|
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
|
||||||
|
* @return Signed Bitcoin transaction for refunding P2SH
|
||||||
|
*/
|
||||||
|
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
|
||||||
|
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
||||||
|
// Build scriptSig with...
|
||||||
|
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||||
|
|
||||||
|
// transaction signature
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||||
|
|
||||||
|
// redeem public key
|
||||||
|
byte[] refundPubKey = refundKey.getPubKey();
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
|
||||||
|
|
||||||
|
// redeem script
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||||
|
|
||||||
|
return scriptBuilder.build();
|
||||||
|
};
|
||||||
|
|
||||||
|
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
|
||||||
|
*
|
||||||
|
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
|
||||||
|
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
|
||||||
|
* @param fundingOutput output from transaction that funded P2SH address
|
||||||
|
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||||
|
* @param secret actual 32-byte secret used when building redeemScript
|
||||||
|
* @return Signed Bitcoin transaction for redeeming P2SH
|
||||||
|
*/
|
||||||
|
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret) {
|
||||||
|
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
|
||||||
|
// Build scriptSig with...
|
||||||
|
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||||
|
|
||||||
|
// secret
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
|
||||||
|
|
||||||
|
// transaction signature
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||||
|
|
||||||
|
// redeem public key
|
||||||
|
byte[] redeemPubKey = redeemKey.getPubKey();
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
|
||||||
|
|
||||||
|
// redeem script
|
||||||
|
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||||
|
|
||||||
|
return scriptBuilder.build();
|
||||||
|
};
|
||||||
|
|
||||||
|
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
|
||||||
|
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
|
||||||
|
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||||
|
|
||||||
|
for (byte[] rawTransaction : rawTransactions) {
|
||||||
|
Transaction transaction = new Transaction(params, rawTransaction);
|
||||||
|
|
||||||
|
// Cycle through inputs, looking for one that spends our P2SH
|
||||||
|
for (TransactionInput input : transaction.getInputs()) {
|
||||||
|
Script scriptSig = input.getScriptSig();
|
||||||
|
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
|
||||||
|
|
||||||
|
// Expected number of script chunks for redeem. Refund might not have the same number.
|
||||||
|
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
|
||||||
|
if (scriptChunks.size() != expectedChunkCount)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// We're expecting last chunk to contain the actual redeemScript
|
||||||
|
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
|
||||||
|
byte[] redeemScriptBytes = lastChunk.data;
|
||||||
|
|
||||||
|
// If non-push scripts, redeemScript will be null
|
||||||
|
if (redeemScriptBytes == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
|
if (!inputAddress.toString().equals(p2shAddress))
|
||||||
|
// Input isn't spending our P2SH
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] secret = scriptChunks.get(0).data;
|
||||||
|
if (secret.length != BTCP2SH.SECRET_LENGTH)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -30,13 +30,9 @@ public class CrossChainTradeData {
|
|||||||
@Schema(description = "HASH160 of 32-byte secret")
|
@Schema(description = "HASH160 of 32-byte secret")
|
||||||
public byte[] secretHash;
|
public byte[] secretHash;
|
||||||
|
|
||||||
@Schema(description = "Initial QORT payment that will be sent to Qortal trade partner")
|
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
|
||||||
public long initialPayout;
|
|
||||||
|
|
||||||
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
|
@Schema(description = "Final QORT payment that will be sent to Qortal trade partner")
|
||||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
public long redeemPayout;
|
public long qortAmount;
|
||||||
|
|
||||||
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
|
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
|
||||||
public String qortalRecipient;
|
public String qortalRecipient;
|
||||||
@ -45,7 +41,7 @@ public class CrossChainTradeData {
|
|||||||
public Long tradeModeTimestamp;
|
public Long tradeModeTimestamp;
|
||||||
|
|
||||||
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
|
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
|
||||||
public long tradeRefundTimeout;
|
public int tradeTimeout;
|
||||||
|
|
||||||
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
||||||
public Integer tradeRefundHeight;
|
public Integer tradeRefundHeight;
|
||||||
|
@ -15,6 +15,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class TradeBotData {
|
public class TradeBotData {
|
||||||
|
|
||||||
|
// Never expose this
|
||||||
|
@XmlTransient
|
||||||
|
@Schema(hidden = true)
|
||||||
|
private byte[] tradePrivateKey;
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_SENDING_MESSAGE_TO_AT(30), BOB_WAITING_FOR_P2SH_B(40), BOB_WAITING_FOR_AT_REDEEM(50),
|
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_SENDING_MESSAGE_TO_AT(30), BOB_WAITING_FOR_P2SH_B(40), BOB_WAITING_FOR_AT_REDEEM(50),
|
||||||
ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130);
|
ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130);
|
||||||
@ -30,13 +35,10 @@ public class TradeBotData {
|
|||||||
return map.get(value);
|
return map.get(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private State tradeState;
|
private State tradeState;
|
||||||
|
|
||||||
// Never expose this
|
private String atAddress;
|
||||||
@XmlTransient
|
private int tradeTimeout;
|
||||||
@Schema(hidden = true)
|
|
||||||
private byte[] tradePrivateKey;
|
|
||||||
|
|
||||||
private byte[] tradeNativePublicKey;
|
private byte[] tradeNativePublicKey;
|
||||||
private byte[] tradeNativePublicKeyHash;
|
private byte[] tradeNativePublicKeyHash;
|
||||||
@ -47,23 +49,25 @@ public class TradeBotData {
|
|||||||
private byte[] tradeForeignPublicKey;
|
private byte[] tradeForeignPublicKey;
|
||||||
private byte[] tradeForeignPublicKeyHash;
|
private byte[] tradeForeignPublicKeyHash;
|
||||||
|
|
||||||
private String atAddress;
|
private long bitcoinAmount;
|
||||||
|
|
||||||
private byte[] lastTransactionSignature;
|
private byte[] lastTransactionSignature;
|
||||||
|
|
||||||
public TradeBotData(byte[] tradePrivateKey, State tradeState,
|
public TradeBotData(byte[] tradePrivateKey, State tradeState, String atAddress, int tradeTimeout,
|
||||||
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash,
|
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash,
|
||||||
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, String atAddress,
|
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash,
|
||||||
byte[] lastTransactionSignature) {
|
long bitcoinAmount, byte[] lastTransactionSignature) {
|
||||||
this.tradePrivateKey = tradePrivateKey;
|
this.tradePrivateKey = tradePrivateKey;
|
||||||
this.tradeState = tradeState;
|
this.tradeState = tradeState;
|
||||||
|
this.atAddress = atAddress;
|
||||||
|
this.tradeTimeout = tradeTimeout;
|
||||||
this.tradeNativePublicKey = tradeNativePublicKey;
|
this.tradeNativePublicKey = tradeNativePublicKey;
|
||||||
this.tradeNativePublicKeyHash = tradeNativePublicKeyHash;
|
this.tradeNativePublicKeyHash = tradeNativePublicKeyHash;
|
||||||
this.secret = secret;
|
this.secret = secret;
|
||||||
this.secretHash = secretHash;
|
this.secretHash = secretHash;
|
||||||
this.tradeForeignPublicKey = tradeForeignPublicKey;
|
this.tradeForeignPublicKey = tradeForeignPublicKey;
|
||||||
this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash;
|
this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash;
|
||||||
this.atAddress = atAddress;
|
this.bitcoinAmount = bitcoinAmount;
|
||||||
this.lastTransactionSignature = lastTransactionSignature;
|
this.lastTransactionSignature = lastTransactionSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +83,18 @@ public class TradeBotData {
|
|||||||
this.tradeState = state;
|
this.tradeState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAtAddress() {
|
||||||
|
return this.atAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAtAddress(String atAddress) {
|
||||||
|
this.atAddress = atAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTradeTimeout() {
|
||||||
|
return this.tradeTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getTradeNativePublicKey() {
|
public byte[] getTradeNativePublicKey() {
|
||||||
return this.tradeNativePublicKey;
|
return this.tradeNativePublicKey;
|
||||||
}
|
}
|
||||||
@ -103,12 +119,8 @@ public class TradeBotData {
|
|||||||
return this.tradeForeignPublicKeyHash;
|
return this.tradeForeignPublicKeyHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAtAddress() {
|
public long getBitcoinAmount() {
|
||||||
return this.atAddress;
|
return this.bitcoinAmount;
|
||||||
}
|
|
||||||
|
|
||||||
public void setAtAddress(String atAddress) {
|
|
||||||
this.atAddress = atAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getLastTransactionSignature() {
|
public byte[] getLastTransactionSignature() {
|
||||||
|
@ -19,8 +19,11 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TradeBotData> getAllTradeBotData() throws DataException {
|
public List<TradeBotData> getAllTradeBotData() throws DataException {
|
||||||
String sql = "SELECT trade_private_key, trade_state, trade_native_public_key, trade_native_public_key_hash, "
|
String sql = "SELECT trade_private_key, trade_state, at_address, trade_timeout, "
|
||||||
+ "secret, secret_hash, trade_foreign_public_key, trade_foreign_public_key_hash, at_address, last_transaction_signature "
|
+ "trade_native_public_key, trade_native_public_key_hash, "
|
||||||
|
+ "secret, secret_hash, "
|
||||||
|
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
|
||||||
|
+ "bitcoin_amount, last_transaction_signature "
|
||||||
+ "FROM TradeBotStates";
|
+ "FROM TradeBotStates";
|
||||||
|
|
||||||
List<TradeBotData> allTradeBotData = new ArrayList<>();
|
List<TradeBotData> allTradeBotData = new ArrayList<>();
|
||||||
@ -36,18 +39,22 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
|
|||||||
if (tradeState == null)
|
if (tradeState == null)
|
||||||
throw new DataException("Illegal trade-bot trade-state fetched from repository");
|
throw new DataException("Illegal trade-bot trade-state fetched from repository");
|
||||||
|
|
||||||
byte[] tradeNativePublicKey = resultSet.getBytes(3);
|
String atAddress = resultSet.getString(3);
|
||||||
byte[] tradeNativePublicKeyHash = resultSet.getBytes(4);
|
int tradeTimeout = resultSet.getInt(4);
|
||||||
byte[] secret = resultSet.getBytes(5);
|
byte[] tradeNativePublicKey = resultSet.getBytes(5);
|
||||||
byte[] secretHash = resultSet.getBytes(6);
|
byte[] tradeNativePublicKeyHash = resultSet.getBytes(6);
|
||||||
byte[] tradeForeignPublicKey = resultSet.getBytes(7);
|
byte[] secret = resultSet.getBytes(7);
|
||||||
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(8);
|
byte[] secretHash = resultSet.getBytes(8);
|
||||||
String atAddress = resultSet.getString(9);
|
byte[] tradeForeignPublicKey = resultSet.getBytes(9);
|
||||||
byte[] lastTransactionSignature = resultSet.getBytes(10);
|
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(10);
|
||||||
|
long bitcoinAmount = resultSet.getLong(11);
|
||||||
|
byte[] lastTransactionSignature = resultSet.getBytes(12);
|
||||||
|
|
||||||
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
|
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
|
||||||
|
atAddress, tradeTimeout,
|
||||||
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
|
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
|
||||||
tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, lastTransactionSignature);
|
tradeForeignPublicKey, tradeForeignPublicKeyHash,
|
||||||
|
bitcoinAmount, lastTransactionSignature);
|
||||||
allTradeBotData.add(tradeBotData);
|
allTradeBotData.add(tradeBotData);
|
||||||
} while (resultSet.next());
|
} while (resultSet.next());
|
||||||
|
|
||||||
@ -63,12 +70,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
|
|||||||
|
|
||||||
saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey())
|
saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey())
|
||||||
.bind("trade_state", tradeBotData.getState().value)
|
.bind("trade_state", tradeBotData.getState().value)
|
||||||
|
.bind("at_address", tradeBotData.getAtAddress())
|
||||||
|
.bind("trade_timeout", tradeBotData.getTradeTimeout())
|
||||||
.bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey())
|
.bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey())
|
||||||
.bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash())
|
.bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash())
|
||||||
.bind("secret", tradeBotData.getSecret()).bind("secret_hash", tradeBotData.getSecretHash())
|
.bind("secret", tradeBotData.getSecret()).bind("secret_hash", tradeBotData.getSecretHash())
|
||||||
.bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey())
|
.bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey())
|
||||||
.bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash())
|
.bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash())
|
||||||
.bind("at_address", tradeBotData.getAtAddress())
|
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
|
||||||
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature());
|
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -621,11 +621,11 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
case 20:
|
case 20:
|
||||||
// Trade bot
|
// Trade bot
|
||||||
stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, "
|
stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, "
|
||||||
|
+ "at_address QortalAddress, trade_timeout INT NOT NULL, "
|
||||||
+ "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, "
|
+ "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, "
|
||||||
+ "secret VARBINARY(32) NOT NULL, secret_hash VARBINARY(32) NOT NULL, "
|
+ "secret VARBINARY(32) NOT NULL, secret_hash VARBINARY(32) NOT NULL, "
|
||||||
+ "trade_foreign_public_key QortalPublicKey NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
|
+ "trade_foreign_public_key QortalPublicKey NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
|
||||||
+ "at_address QortalAddress, "
|
+ "bitcoin_amount BIGINT NOT NULL, last_transaction_signature Signature, PRIMARY KEY (trade_private_key))");
|
||||||
+ "last_transaction_signature Signature, PRIMARY KEY (trade_private_key))");
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -47,7 +47,6 @@ public class AtTests extends Common {
|
|||||||
public static final byte[] bitcoinPublicKeyHash = new byte[20]; // not used in tests
|
public static final byte[] bitcoinPublicKeyHash = new byte[20]; // not used in tests
|
||||||
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
|
||||||
public static final int refundTimeout = 10; // blocks
|
public static final int refundTimeout = 10; // blocks
|
||||||
public static final long initialPayout = 100000L;
|
|
||||||
public static final long redeemAmount = 80_40200000L;
|
public static final long redeemAmount = 80_40200000L;
|
||||||
public static final long fundingAmount = 123_45600000L;
|
public static final long fundingAmount = 123_45600000L;
|
||||||
public static final long bitcoinAmount = 864200L;
|
public static final long bitcoinAmount = 864200L;
|
||||||
@ -61,7 +60,7 @@ public class AtTests extends Common {
|
|||||||
public void testCompile() {
|
public void testCompile() {
|
||||||
Account deployer = Common.getTestAccount(null, "chloe");
|
Account deployer = Common.getTestAccount(null, "chloe");
|
||||||
|
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount);
|
||||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,44 +155,6 @@ public class AtTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
@Test
|
|
||||||
public void testInitialPayment() throws DataException {
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
|
||||||
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
|
|
||||||
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
|
|
||||||
|
|
||||||
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
|
|
||||||
long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT);
|
|
||||||
|
|
||||||
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer);
|
|
||||||
Account at = deployAtTransaction.getATAccount();
|
|
||||||
String atAddress = at.getAddress();
|
|
||||||
|
|
||||||
// Send recipient's address to AT
|
|
||||||
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0);
|
|
||||||
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
|
|
||||||
|
|
||||||
// Initial payment should happen 1st block after receiving recipient address
|
|
||||||
BlockUtils.mintBlock(repository);
|
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
|
||||||
|
|
||||||
assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
|
|
||||||
|
|
||||||
describeAt(repository, atAddress);
|
|
||||||
|
|
||||||
// Test orphaning
|
|
||||||
BlockUtils.orphanLastBlock(repository);
|
|
||||||
|
|
||||||
expectedBalance = recipientsInitialBalance;
|
|
||||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
|
||||||
|
|
||||||
assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
// TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@Test
|
@Test
|
||||||
@ -294,7 +255,7 @@ public class AtTests extends Common {
|
|||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
|
||||||
@ -304,7 +265,7 @@ public class AtTests extends Common {
|
|||||||
// Orphan redeem
|
// Orphan redeem
|
||||||
BlockUtils.orphanLastBlock(repository);
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||||
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
|
||||||
@ -347,7 +308,7 @@ public class AtTests extends Common {
|
|||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance + initialPayout;
|
long expectedBalance = recipientsInitialBalance;
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
@ -389,7 +350,7 @@ public class AtTests extends Common {
|
|||||||
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||||
BlockUtils.mintBlock(repository);
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
|
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
|
||||||
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
|
||||||
@ -435,7 +396,7 @@ public class AtTests extends Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
|
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount);
|
||||||
|
|
||||||
long txTimestamp = System.currentTimeMillis();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
byte[] lastReference = deployer.getLastReference();
|
byte[] lastReference = deployer.getLastReference();
|
||||||
@ -501,7 +462,7 @@ public class AtTests extends Common {
|
|||||||
|
|
||||||
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
|
||||||
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
long expectedMinimumBalance = deployersPostDeploymentBalance;
|
||||||
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - initialPayout;
|
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
|
||||||
|
|
||||||
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
|
||||||
|
|
||||||
@ -521,7 +482,6 @@ public class AtTests extends Common {
|
|||||||
+ "\tcreation timestamp: %s,\n"
|
+ "\tcreation timestamp: %s,\n"
|
||||||
+ "\tcurrent balance: %s QORT,\n"
|
+ "\tcurrent balance: %s QORT,\n"
|
||||||
+ "\tHASH160 of secret: %s,\n"
|
+ "\tHASH160 of secret: %s,\n"
|
||||||
+ "\tinitial payout: %s QORT,\n"
|
|
||||||
+ "\tredeem payout: %s QORT,\n"
|
+ "\tredeem payout: %s QORT,\n"
|
||||||
+ "\texpected bitcoin: %s BTC,\n"
|
+ "\texpected bitcoin: %s BTC,\n"
|
||||||
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
+ "\ttrade timeout: %d minutes (from trade start),\n"
|
||||||
@ -531,10 +491,9 @@ public class AtTests extends Common {
|
|||||||
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
epochMilliFormatter.apply(tradeData.creationTimestamp),
|
||||||
Amounts.prettyAmount(tradeData.qortBalance),
|
Amounts.prettyAmount(tradeData.qortBalance),
|
||||||
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40),
|
HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40),
|
||||||
Amounts.prettyAmount(tradeData.initialPayout),
|
Amounts.prettyAmount(tradeData.qortAmount),
|
||||||
Amounts.prettyAmount(tradeData.redeemPayout),
|
|
||||||
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
Amounts.prettyAmount(tradeData.expectedBitcoin),
|
||||||
tradeData.tradeRefundTimeout,
|
tradeData.tradeTimeout,
|
||||||
currentBlockHeight));
|
currentBlockHeight));
|
||||||
|
|
||||||
// Are we in 'offer' or 'trade' stage?
|
// Are we in 'offer' or 'trade' stage?
|
||||||
|
@ -10,7 +10,7 @@ import org.junit.After;
|
|||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ public class BtcTests extends Common {
|
|||||||
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
||||||
|
|
||||||
byte[] expectedSecret = AtTests.secret;
|
byte[] expectedSecret = AtTests.secret;
|
||||||
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
|
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
|
||||||
|
|
||||||
assertNotNull(secret);
|
assertNotNull(secret);
|
||||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
|
||||||
|
@ -13,7 +13,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -103,7 +103,7 @@ public class BuildP2SH {
|
|||||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
@ -15,7 +15,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -113,7 +113,7 @@ public class CheckP2SH {
|
|||||||
|
|
||||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
|
@ -34,21 +34,20 @@ public class DeployAT {
|
|||||||
if (error != null)
|
if (error != null)
|
||||||
System.err.println(error);
|
System.err.println(error);
|
||||||
|
|
||||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
|
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <AT trade timeout>"));
|
||||||
System.err.println(String.format("example: DeployAT "
|
System.err.println(String.format("example: DeployAT "
|
||||||
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
||||||
+ "\t80.4020 \\\n"
|
+ "\t80.4020 \\\n"
|
||||||
+ "\t0.00864200 \\\n"
|
+ "\t0.00864200 \\\n"
|
||||||
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
|
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
|
||||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||||
+ "\t0.0001 \\\n"
|
|
||||||
+ "\t123.456 \\\n"
|
+ "\t123.456 \\\n"
|
||||||
+ "\t10"));
|
+ "\t10"));
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (args.length != 8)
|
if (args.length != 7)
|
||||||
usage(null);
|
usage(null);
|
||||||
|
|
||||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||||
@ -59,7 +58,6 @@ public class DeployAT {
|
|||||||
long expectedBitcoin = 0;
|
long expectedBitcoin = 0;
|
||||||
byte[] bitcoinPublicKeyHash = null;
|
byte[] bitcoinPublicKeyHash = null;
|
||||||
byte[] secretHash = null;
|
byte[] secretHash = null;
|
||||||
long initialPayout = 0;
|
|
||||||
long fundingAmount = 0;
|
long fundingAmount = 0;
|
||||||
int tradeTimeout = 0;
|
int tradeTimeout = 0;
|
||||||
|
|
||||||
@ -85,10 +83,6 @@ public class DeployAT {
|
|||||||
if (secretHash.length != 20)
|
if (secretHash.length != 20)
|
||||||
usage("Hash of secret must be 20 bytes");
|
usage("Hash of secret must be 20 bytes");
|
||||||
|
|
||||||
initialPayout = Long.parseLong(args[argIndex++]);
|
|
||||||
if (initialPayout < 0)
|
|
||||||
usage("Initial QORT payout must be positive");
|
|
||||||
|
|
||||||
fundingAmount = Long.parseLong(args[argIndex++]);
|
fundingAmount = Long.parseLong(args[argIndex++]);
|
||||||
if (fundingAmount <= redeemAmount)
|
if (fundingAmount <= redeemAmount)
|
||||||
usage("AT funding amount must be greater than QORT redeem amount");
|
usage("AT funding amount must be greater than QORT redeem amount");
|
||||||
@ -120,7 +114,7 @@ public class DeployAT {
|
|||||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||||
|
|
||||||
// Deploy AT
|
// Deploy AT
|
||||||
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
|
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, tradeTimeout, redeemAmount, expectedBitcoin);
|
||||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||||
|
|
||||||
long txTimestamp = System.currentTimeMillis();
|
long txTimestamp = System.currentTimeMillis();
|
||||||
|
@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -121,7 +121,7 @@ public class Redeem {
|
|||||||
|
|
||||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
@ -182,7 +182,7 @@ public class Redeem {
|
|||||||
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
|
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
|
||||||
|
|
||||||
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
|
Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
|
||||||
|
|
||||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCP2SH;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -120,7 +120,7 @@ public class Refund {
|
|||||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||||
|
|
||||||
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
|
||||||
@ -186,7 +186,7 @@ public class Refund {
|
|||||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
||||||
|
|
||||||
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
||||||
|
|
||||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user