WIP: more work on trade-bot

This commit is contained in:
catbref 2020-06-11 09:33:33 +01:00
parent faa6e82bef
commit 04d691991a
5 changed files with 162 additions and 46 deletions

View File

@ -0,0 +1,33 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotCreateRequest {
@Schema(description = "Trade creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "81")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Schema(description = "Bitcoin amount wanted in return", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
public Integer tradeTimeout;
public TradeBotCreateRequest() {
}
}

View File

@ -38,6 +38,7 @@ import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.CrossChainCancelRequest;
import org.qortal.api.model.CrossChainSecretRequest;
import org.qortal.api.model.CrossChainTradeRequest;
import org.qortal.api.model.TradeBotCreateRequest;
import org.qortal.api.model.CrossChainBitcoinP2SHStatus;
import org.qortal.api.model.CrossChainBitcoinRedeemRequest;
import org.qortal.api.model.CrossChainBitcoinRefundRequest;
@ -720,6 +721,36 @@ public class CrossChainResource {
}
}
@POST
@Path("/tradebot")
@Operation(
summary = "Create a trade offer",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = TradeBotCreateRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] unsignedBytes = TradeBot.createTrade(repository, tradeBotCreateRequest);
return Base58.encode(unsignedBytes);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/tradebot/{ataddress}")
@Operation(

View File

@ -11,14 +11,23 @@ import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.model.TradeBotCreateRequest;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.NTP;
public class TradeBot {
@ -38,6 +47,49 @@ public class TradeBot {
return instance;
}
public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) {
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
byte[] tradePrivateKey = generateTradePrivateKey();
byte[] secret = generateSecret();
byte[] secretHash = Crypto.digest(secret);
byte[] tradeNativePublicKey = deriveTradeNativePublicKey(tradePrivateKey);
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
// Deploy AT
long timestamp = NTP.getTime();
byte[] reference = creator.getLastReference();
long fee = 0L;
byte[] signature = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
String name = "QORT/BTC ACCT";
String description = "QORT/BTC cross-chain trade";
String aTType = "ACCT";
String tags = "ACCT QORT BTC";
byte[] creationBytes = BTCACCT.buildQortalAT(creator.getAddress(), tradeNativePublicKeyHash, secretHash, tradeBotCreateRequest.tradeTimeout, tradeBotCreateRequest.qortAmount, tradeBotCreateRequest.bitcoinAmount);
long amount = tradeBotCreateRequest.fundingQortAmount;
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
String atAddress = deployAtTransactionData.getAtAddress();
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.BOB_WAITING_FOR_MESSAGE,
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, null);
repository.getCrossChainRepository().save(tradeBotData);
// Return to user for signing and broadcast as we don't have their Qortal private key
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
}
public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
@ -92,8 +144,8 @@ public class TradeBot {
for (TradeBotData tradeBotData : allTradeBotData)
switch (tradeBotData.getState()) {
case ALICE_START:
handleAliceStart(repository, tradeBotData);
case BOB_WAITING_FOR_MESSAGE:
handleBobWaitingForMessage(repository, tradeBotData);
break;
}
} catch (DataException e) {
@ -101,7 +153,7 @@ public class TradeBot {
}
}
private void handleAliceStart(Repository repository, TradeBotData tradeBotData) {
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) {
}

View File

@ -16,8 +16,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
public class TradeBotData {
public enum State {
BOB_START(0), BOB_WAITING_FOR_P2SH_A(10), BOB_WAITING_FOR_P2SH_B(20), BOB_WAITING_FOR_AT_REDEEM(30),
ALICE_START(100), ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130);
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_WAITING_FOR_P2SH_A(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);
public final int value;
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));

View File

@ -26,7 +26,7 @@ import com.google.common.base.Utf8;
public class DeployAtTransaction extends Transaction {
// Properties
private DeployAtTransactionData deployATTransactionData;
private DeployAtTransactionData deployAtTransactionData;
// Other useful constants
public static final int MAX_NAME_SIZE = 200;
@ -40,31 +40,31 @@ public class DeployAtTransaction extends Transaction {
public DeployAtTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.deployATTransactionData = (DeployAtTransactionData) this.transactionData;
this.deployAtTransactionData = (DeployAtTransactionData) this.transactionData;
}
// More information
@Override
public List<String> getRecipientAddresses() throws DataException {
return Collections.singletonList(this.deployATTransactionData.getAtAddress());
return Collections.singletonList(this.deployAtTransactionData.getAtAddress());
}
/** Returns AT version from the header bytes */
private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes();
byte[] creationBytes = deployAtTransactionData.getCreationBytes();
return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian
}
/** Make sure deployATTransactionData has an ATAddress */
private void ensureATAddress() throws DataException {
if (this.deployATTransactionData.getAtAddress() != null)
public static void ensureATAddress(DeployAtTransactionData deployAtTransactionData) throws DataException {
if (deployAtTransactionData.getAtAddress() != null)
return;
// Use transaction transformer
try {
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(this.deployATTransactionData));
this.deployATTransactionData.setAtAddress(atAddress);
String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(deployAtTransactionData));
deployAtTransactionData.setAtAddress(atAddress);
} catch (TransformationException e) {
throw new DataException("Unable to generate AT address");
}
@ -73,9 +73,9 @@ public class DeployAtTransaction extends Transaction {
// Navigation
public Account getATAccount() throws DataException {
ensureATAddress();
ensureATAddress(this.deployAtTransactionData);
return new Account(this.repository, this.deployATTransactionData.getAtAddress());
return new Account(this.repository, this.deployAtTransactionData.getAtAddress());
}
// Processing
@ -83,30 +83,30 @@ public class DeployAtTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Check name size bounds
int nameLength = Utf8.encodedLength(this.deployATTransactionData.getName());
int nameLength = Utf8.encodedLength(this.deployAtTransactionData.getName());
if (nameLength < 1 || nameLength > MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check description size bounds
int descriptionlength = Utf8.encodedLength(this.deployATTransactionData.getDescription());
int descriptionlength = Utf8.encodedLength(this.deployAtTransactionData.getDescription());
if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE)
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
// Check AT-type size bounds
int atTypeLength = Utf8.encodedLength(this.deployATTransactionData.getAtType());
int atTypeLength = Utf8.encodedLength(this.deployAtTransactionData.getAtType());
if (atTypeLength < 1 || atTypeLength > MAX_AT_TYPE_SIZE)
return ValidationResult.INVALID_AT_TYPE_LENGTH;
// Check tags size bounds
int tagsLength = Utf8.encodedLength(this.deployATTransactionData.getTags());
int tagsLength = Utf8.encodedLength(this.deployAtTransactionData.getTags());
if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE)
return ValidationResult.INVALID_TAGS_LENGTH;
// Check amount is positive
if (this.deployATTransactionData.getAmount() <= 0)
if (this.deployAtTransactionData.getAmount() <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
// Check asset even exists
if (assetData == null)
@ -117,7 +117,7 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.ASSET_NOT_SPENDABLE;
// Check asset amount is integer if asset is not divisible
if (!assetData.isDivisible() && this.deployATTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
if (!assetData.isDivisible() && this.deployAtTransactionData.getAmount() % Amounts.MULTIPLIER != 0)
return ValidationResult.INVALID_AMOUNT;
Account creator = this.getCreator();
@ -125,15 +125,15 @@ public class DeployAtTransaction extends Transaction {
// Check creator has enough funds
if (assetId == Asset.QORT) {
// Simple case: amount and fee both in QORT
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
return ValidationResult.NO_BALANCE;
} else {
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
return ValidationResult.NO_BALANCE;
}
@ -142,12 +142,12 @@ public class DeployAtTransaction extends Transaction {
return ValidationResult.INVALID_CREATION_BYTES;
// Check creation bytes are valid (for v2+)
this.ensureATAddress();
ensureATAddress(this.deployAtTransactionData);
// Just enough AT data to allow API to query initial balances, etc.
String atAddress = this.deployATTransactionData.getAtAddress();
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
long creation = this.deployATTransactionData.getTimestamp();
String atAddress = this.deployAtTransactionData.getAtAddress();
byte[] creatorPublicKey = this.deployAtTransactionData.getCreatorPublicKey();
long creation = this.deployAtTransactionData.getTimestamp();
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
@ -157,7 +157,7 @@ public class DeployAtTransaction extends Transaction {
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
try {
new MachineState(api, loggerFactory, this.deployATTransactionData.getCreationBytes());
new MachineState(api, loggerFactory, this.deployAtTransactionData.getCreationBytes());
} catch (IllegalArgumentException e) {
// Not valid
return ValidationResult.INVALID_CREATION_BYTES;
@ -169,25 +169,25 @@ public class DeployAtTransaction extends Transaction {
@Override
public ValidationResult isProcessable() throws DataException {
Account creator = getCreator();
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
// Check creator has enough funds
if (assetId == Asset.QORT) {
// Simple case: amount and fee both in QORT
long minimumBalance = this.deployATTransactionData.getFee() + this.deployATTransactionData.getAmount();
long minimumBalance = this.deployAtTransactionData.getFee() + this.deployAtTransactionData.getAmount();
if (creator.getConfirmedBalance(Asset.QORT) < minimumBalance)
return ValidationResult.NO_BALANCE;
} else {
if (creator.getConfirmedBalance(Asset.QORT) < this.deployATTransactionData.getFee())
if (creator.getConfirmedBalance(Asset.QORT) < this.deployAtTransactionData.getFee())
return ValidationResult.NO_BALANCE;
if (creator.getConfirmedBalance(assetId) < this.deployATTransactionData.getAmount())
if (creator.getConfirmedBalance(assetId) < this.deployAtTransactionData.getAmount())
return ValidationResult.NO_BALANCE;
}
// Check AT doesn't already exist
if (this.repository.getATRepository().exists(this.deployATTransactionData.getAtAddress()))
if (this.repository.getATRepository().exists(this.deployAtTransactionData.getAtAddress()))
return ValidationResult.AT_ALREADY_EXISTS;
return ValidationResult.OK;
@ -195,40 +195,40 @@ public class DeployAtTransaction extends Transaction {
@Override
public void process() throws DataException {
this.ensureATAddress();
ensureATAddress(this.deployAtTransactionData);
// Deploy AT, saving into repository
AT at = new AT(this.repository, this.deployATTransactionData);
AT at = new AT(this.repository, this.deployAtTransactionData);
at.deploy();
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
// Update creator's balance regarding initial payment to AT
Account creator = getCreator();
creator.modifyAssetBalance(assetId, - this.deployATTransactionData.getAmount());
creator.modifyAssetBalance(assetId, - this.deployAtTransactionData.getAmount());
// Update AT's reference, which also creates AT account
Account atAccount = this.getATAccount();
atAccount.setLastReference(this.deployATTransactionData.getSignature());
atAccount.setLastReference(this.deployAtTransactionData.getSignature());
// Update AT's balance
atAccount.setConfirmedBalance(assetId, this.deployATTransactionData.getAmount());
atAccount.setConfirmedBalance(assetId, this.deployAtTransactionData.getAmount());
}
@Override
public void orphan() throws DataException {
// Delete AT from repository
AT at = new AT(this.repository, this.deployATTransactionData);
AT at = new AT(this.repository, this.deployAtTransactionData);
at.undeploy();
long assetId = this.deployATTransactionData.getAssetId();
long assetId = this.deployAtTransactionData.getAssetId();
// Update creator's balance regarding initial payment to AT
Account creator = getCreator();
creator.modifyAssetBalance(assetId, this.deployATTransactionData.getAmount());
creator.modifyAssetBalance(assetId, this.deployAtTransactionData.getAmount());
// Delete AT's account (and hence its balance)
this.repository.getAccountRepository().delete(this.deployATTransactionData.getAtAddress());
this.repository.getAccountRepository().delete(this.deployAtTransactionData.getAtAddress());
}
}