diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java
index a59970b3..23bf02cd 100644
--- a/src/main/java/org/qortal/at/AT.java
+++ b/src/main/java/org/qortal/at/AT.java
@@ -5,6 +5,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.MachineState;
+import org.ciyam.at.Timestamp;
import org.qortal.asset.Asset;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
@@ -48,7 +49,13 @@ public class AT {
short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
if (version >= 2) {
- MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
+ // Just enough AT data to allow API to query initial balances, etc.
+ ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
+
+ long blockTimestamp = Timestamp.toLong(height, 0);
+ QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
+
+ MachineState machineState = new MachineState(api, deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java
index 9016bb17..27f67ac0 100644
--- a/src/main/java/org/qortal/at/QortalATAPI.java
+++ b/src/main/java/org/qortal/at/QortalATAPI.java
@@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import org.ciyam.at.API;
@@ -35,11 +36,15 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction;
+import org.qortal.transaction.Transaction.TransactionType;
+import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
public class QortalATAPI extends API {
+ private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH];
+
// Properties
private Repository repository;
private ATData atData;
@@ -316,18 +321,30 @@ public class QortalATAPI extends API {
public void putAddressFromTransactionInAIntoB(MachineState state) {
TransactionData transactionData = this.getTransactionFromA(state);
- // We actually use public key as it has more potential utility (e.g. message verification) than an address
- byte[] bytes = transactionData.getCreatorPublicKey();
+ String address;
+ if (transactionData.getType() == TransactionType.AT) {
+ // Use AT address from transaction data, as transaction's public key will always be fake
+ address = ((ATTransactionData) transactionData).getATAddress();
+ } else {
+ byte[] publicKey = transactionData.getCreatorPublicKey();
+ address = Crypto.toAddress(publicKey);
+ }
- this.setB(state, bytes);
+ // Convert to byte form as this only takes 25 bytes,
+ // compared to string-form's 34 bytes,
+ // and we only have 32 bytes available.
+ byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
+
+ this.setB(state, addressBytes);
}
@Override
public void putCreatorAddressIntoB(MachineState state) {
- // We actually use public key as it has more potential utility (e.g. message verification) than an address
- byte[] bytes = atData.getCreatorPublicKey();
+ byte[] publicKey = atData.getCreatorPublicKey();
+ String address = Crypto.toAddress(publicKey);
+ byte[] addressBytes = Bytes.ensureCapacity(address.getBytes(), 32, 0);
- this.setB(state, bytes);
+ this.setB(state, addressBytes);
}
@Override
@@ -490,10 +507,7 @@ public class QortalATAPI extends API {
*
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
*/
-
- // XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
- // return this.blockTimestamp + this.transactions.size();
- throw new RuntimeException("AT timestamp code not fixed!");
+ return this.blockTimestamp + this.transactions.size();
}
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */
@@ -515,21 +529,22 @@ public class QortalATAPI extends API {
/**
* Returns Account (possibly PublicKeyAccount) based on value in B.
*
- * If bytes in B start with 'Q' then use B as an address, but only if valid.
+ * If first byte in B starts with either address version bytes,
+ * and bytes 26 to 32 are zero, then use as an address, but only if valid.
*
* Otherwise, assume B is a public key.
- * @return
*/
private Account getAccountFromB(MachineState state) {
byte[] bBytes = state.getB();
- if (bBytes[0] == 'Q') {
- int zeroIndex = Bytes.indexOf(bBytes, (byte) 0);
- if (zeroIndex > 0) {
- String address = new String(bBytes, 0, zeroIndex);
- if (Crypto.isValidAddress(address))
- return new Account(this.repository, address);
- }
+ if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
+ && Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) {
+ // Extract only the bytes containing address
+ byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH);
+ // If address (in byte form) is valid...
+ if (Crypto.isValidAddress(addressBytes))
+ // ...then return an Account using address (converted to Base58
+ return new Account(this.repository, Base58.encode(addressBytes));
}
return new PublicKeyAccount(this.repository, bBytes);
diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java
index f7f9a979..2ad069f7 100644
--- a/src/main/java/org/qortal/crosschain/BTC.java
+++ b/src/main/java/org/qortal/crosschain/BTC.java
@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -466,18 +467,6 @@ public class BTC {
}
}
- private static class TransactionStorage {
- private Transaction transaction;
-
- public void store(Transaction transaction) {
- this.transaction = transaction;
- }
-
- public Transaction getTransaction() {
- return this.transaction;
- }
- }
-
public List getOutputs(byte[] txId, long startTime) {
Wallet wallet = createEmptyWallet();
@@ -487,7 +476,7 @@ public class BTC {
final Sha256Hash txHash = Sha256Hash.wrap(txId);
- final TransactionStorage transactionStorage = new TransactionStorage();
+ final AtomicReference foundTransaction = new AtomicReference<>();
final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> {
List transactions = block.getTransactions();
@@ -498,7 +487,7 @@ public class BTC {
for (Transaction transaction : transactions)
if (transaction.getTxId().equals(txHash)) {
System.out.println(String.format("We downloaded block containing tx!"));
- transactionStorage.store(transaction);
+ foundTransaction.set(transaction);
}
};
@@ -508,7 +497,7 @@ public class BTC {
try {
replayChain(startTime, wallet, replayHooks);
- Transaction realTx = transactionStorage.getTransaction();
+ Transaction realTx = foundTransaction.get();
return realTx.getOutputs();
} catch (BlockStoreException e) {
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java
index a0480317..1ee78f2c 100644
--- a/src/main/java/org/qortal/crosschain/BTCACCT.java
+++ b/src/main/java/org/qortal/crosschain/BTCACCT.java
@@ -16,9 +16,11 @@ 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.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
+import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
@@ -170,9 +172,10 @@ public class BTCACCT {
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
}
- public static byte[] buildQortalAT(byte[] secretHash, String destinationQortalAddress, long refundMinutes, BigDecimal initialPayout) {
+ public static byte[] buildQortalAT(byte[] secretHash, String recipientQortalAddress, long refundMinutes, BigDecimal initialPayout) {
// Labels for data segment addresses
int addrCounter = 0;
+ // Constants (with corresponding dataByteBuffer.put*() calls below)
final int addrHashPart1 = addrCounter++;
final int addrHashPart2 = addrCounter++;
final int addrHashPart3 = addrCounter++;
@@ -185,6 +188,8 @@ public class BTCACCT {
final int addrHashTempIndex = addrCounter++;
final int addrHashTempLength = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++;
+ final int addrExpectedTxType = addrCounter++;
+ // Variables
final int addrRefundTimestamp = addrCounter++;
final int addrLastTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++;
@@ -200,72 +205,99 @@ public class BTCACCT {
final int addrHashTemp4 = addrCounter++;
// Data segment
- ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8);
+ ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// Hash of secret into HashPart1-4
- dataByteBuffer.put(secretHash);
+ assert dataByteBuffer.position() == addrHashPart1 * MachineState.VALUE_SIZE : "addrHashPart1 incorrect";
+ dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0));
- // Destination Qortal account's public key
- dataByteBuffer.put(Bytes.ensureCapacity(destinationQortalAddress.getBytes(), 32, 0));
+ // Recipient Qortal address, decoded from Base58
+ assert dataByteBuffer.position() == addrAddressPart1 * MachineState.VALUE_SIZE : "addrAddressPart1 incorrect";
+ byte[] recipientAddressBytes = Base58.decode(recipientQortalAddress);
+ dataByteBuffer.put(Bytes.ensureCapacity(recipientAddressBytes, 32, 0));
// Expiry in minutes
+ assert dataByteBuffer.position() == addrRefundMinutes * MachineState.VALUE_SIZE : "addrRefundMinutes incorrect";
dataByteBuffer.putLong(refundMinutes);
- // Temp buffer for hashing any passed secret
+ // Source location and length for hashing any passed secret
+ assert dataByteBuffer.position() == addrHashTempIndex * MachineState.VALUE_SIZE : "addrHashTempIndex incorrect";
dataByteBuffer.putLong(addrHashTemp1);
+ assert dataByteBuffer.position() == addrHashTempLength * MachineState.VALUE_SIZE : "addrHashTempLength incorrect";
dataByteBuffer.putLong(32L);
// Initial payout amount
+ assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
dataByteBuffer.putLong(initialPayout.unscaledValue().longValue());
+ // We're only interested in MESSAGE transactions
+ assert dataByteBuffer.position() == addrExpectedTxType * MachineState.VALUE_SIZE : "addrExpectedTxType incorrect";
+ dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
+
// Code labels
- final int addrTxLoop = 0x36;
- final int addrCheckTx = 0x4b;
- final int addrCheckSender = 0x64;
- final int addrCheckMessage = 0xab;
- final int addrPayout = 0xdf;
- final int addrRefund = 0x102;
- final int addrEndOfCode = 0x109;
+ final int addrTxLoop = 0x0036;
+ final int addrCheckTx = 0x004b;
+ final int addrRefund = 0x00c6;
+ final int addrEndOfCode = 0x00cd;
int tempPC;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1);
- // init:
- codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrRefundTimestamp);
- codeByteBuffer.put(OpCode.SET_DAT.value).putInt(addrLastTimestamp).putInt(addrRefundTimestamp);
- codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp)
- .putInt(addrRefundTimestamp).putInt(addrRefundMinutes);
+ /* Initialization */
+ // Use AT creation 'timestamp' as starting point for finding transactions sent to AT
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrLastTimestamp);
+ // Calculate refund 'timestamp' by adding minutes to above 'timestamp', then save into addrRefundTimestamp
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp)
+ .putInt(addrLastTimestamp).putInt(addrRefundMinutes);
+
+ // Load recipient's address into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
+ // Send initial payment to recipient so they have enough funds to message AT if all goes well
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(addrInitialPayoutAmount);
+ // Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.value);
- // loop:
- codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(addrBlockTimestamp);
- tempPC = codeByteBuffer.position();
- codeByteBuffer.put(OpCode.BLT_DAT.value).putInt(addrBlockTimestamp).putInt(addrRefundTimestamp).put((byte) (addrTxLoop - tempPC));
- codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrRefund);
+ /* Main loop */
- // txloop:
+ // Fetch current block 'timestamp'
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(addrBlockTimestamp);
+ // If we're past refund 'timestamp' then go refund everything back to AT creator
+ tempPC = codeByteBuffer.position();
+ codeByteBuffer.put(OpCode.BGE_DAT.value).putInt(addrBlockTimestamp).putInt(addrRefundTimestamp).put((byte) (addrRefund - tempPC));
+
+ /* Transaction processing loop */
assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect";
+
+ // Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(addrLastTimestamp);
+ // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(addrComparator);
+ // If addrComparator is zero (i.e. A is non-zero, transaction was found) then branch to addrCheckTx
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC));
+ // Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.value);
- // checkTx:
- codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(addrLastTimestamp);
- codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(addrTxType);
- tempPC = codeByteBuffer.position();
- codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrTxType).put((byte) (addrCheckSender - tempPC));
- codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop);
+ /* Check transaction */
+ assert codeByteBuffer.position() == addrCheckTx : "addrCheckTx incorrect";
- // checkSender
- assert codeByteBuffer.position() == addrCheckSender : "addrCheckSender incorrect";
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(addrLastTimestamp);
+ // Extract transaction type (message/payment) from transaction and save type in addrTxType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(addrTxType);
+ // If transaction type is not MESSAGE type then go look for another transaction
+ tempPC = codeByteBuffer.position();
+ codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrTxType).putInt(addrExpectedTxType).put((byte) (addrTxLoop - tempPC));
+
+ /* Check transaction's sender */
+
+ // Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value);
+ // Save B register into data segment starting at addrAddressTemp1
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrAddressTemp1);
+ // Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC));
tempPC = codeByteBuffer.position();
@@ -275,26 +307,38 @@ public class BTCACCT {
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp4).putInt(addrAddressPart4).put((byte) (addrTxLoop - tempPC));
- // checkMessage:
- assert codeByteBuffer.position() == addrCheckMessage : "addrCheckMessage incorrect";
- codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value);
- codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1);
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1);
- codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrHashTempIndex).putInt(addrHashTempLength);
- tempPC = codeByteBuffer.position();
- codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrComparator).put((byte) (addrPayout - tempPC));
- codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop);
+ /* Check 'secret' in transaction's message */
- // payout:
- assert codeByteBuffer.position() == addrPayout : "addrPayout incorrect";
+ // Extract message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value);
+ // Save B register into data segment starting at addrHashTemp1
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1);
+ // Load B register with expected hash result
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1);
+ // Perform HASH160 using source data at addrHashTemp1 through addrHashTemp4. (Location and length specified via addrHashTempIndex and addrHashTemplength).
+ // Save the equality result (1 if they match, 0 otherwise) into addrComparator.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrComparator).putInt(addrHashTempIndex).putInt(addrHashTempLength);
+ // If hashes don't match, addrComparator will be zero so go find another transaction
+ tempPC = codeByteBuffer.position();
+ codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrTxLoop - tempPC));
+
+ /* Success! Pay balance to intended recipient */
+
+ // Load B register with intended recipient address.
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
+ // Pay AT's balance to recipient
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value);
+ // We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.value);
- // refund:
+ /* Refund balance back to AT creator */
assert codeByteBuffer.position() == addrRefund : "addrRefund incorrect";
+
+ // Load B register with AT creator's address.
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_CREATOR_INTO_B.value);
+ // Pay AT's balance back to AT's creator.
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value);
+ // We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.value);
// end-of-code
diff --git a/src/main/java/org/qortal/crypto/Crypto.java b/src/main/java/org/qortal/crypto/Crypto.java
index 36c06585..c940c6ab 100644
--- a/src/main/java/org/qortal/crypto/Crypto.java
+++ b/src/main/java/org/qortal/crypto/Crypto.java
@@ -108,14 +108,15 @@ public class Crypto {
return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION);
}
+ public static boolean isValidAddress(byte[] addressBytes) {
+ return areValidTypedAddressBytes(addressBytes, ADDRESS_VERSION, AT_ADDRESS_VERSION);
+ }
+
public static boolean isValidAtAddress(String address) {
return isValidTypedAddress(address, AT_ADDRESS_VERSION);
}
private static boolean isValidTypedAddress(String address, byte...addressVersions) {
- if (addressVersions == null || addressVersions.length == 0)
- return false;
-
byte[] addressBytes;
try {
@@ -125,6 +126,13 @@ public class Crypto {
return false;
}
+ return areValidTypedAddressBytes(addressBytes, addressVersions);
+ }
+
+ private static boolean areValidTypedAddressBytes(byte[] addressBytes, byte...addressVersions) {
+ if (addressVersions == null || addressVersions.length == 0)
+ return false;
+
// Check address length
if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH)
return false;
diff --git a/src/main/java/org/qortal/data/at/ATData.java b/src/main/java/org/qortal/data/at/ATData.java
index 0ab73bdc..b12439e0 100644
--- a/src/main/java/org/qortal/data/at/ATData.java
+++ b/src/main/java/org/qortal/data/at/ATData.java
@@ -46,6 +46,14 @@ public class ATData {
this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8);
}
+ /** For constructing skeleton ATData with bare minimum info. */
+ public ATData(String ATAddress, byte[] creatorPublicKey, long creation, long assetId) {
+ this.ATAddress = ATAddress;
+ this.creatorPublicKey = creatorPublicKey;
+ this.creation = creation;
+ this.assetId = assetId;
+ }
+
// Getters / setters
public String getATAddress() {
diff --git a/src/main/java/org/qortal/transaction/DeployAtTransaction.java b/src/main/java/org/qortal/transaction/DeployAtTransaction.java
index 191adeec..8995b626 100644
--- a/src/main/java/org/qortal/transaction/DeployAtTransaction.java
+++ b/src/main/java/org/qortal/transaction/DeployAtTransaction.java
@@ -8,12 +8,15 @@ import java.util.ArrayList;
import java.util.List;
import org.ciyam.at.MachineState;
+import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.at.AT;
+import org.qortal.at.QortalATAPI;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.asset.AssetData;
+import org.qortal.data.at.ATData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
@@ -79,6 +82,7 @@ public class DeployAtTransaction extends Transaction {
/** Returns AT version from the header bytes */
private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes();
+ // XXX this is currently little-endian, but ok for the moment as newer versions will be reported as 512+ so tests like version >= 2 will work
return (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
}
@@ -87,6 +91,13 @@ public class DeployAtTransaction extends Transaction {
if (this.deployATTransactionData.getAtAddress() != null)
return;
+ // For new version, simply use transaction signature
+ if (this.getVersion() > 1) {
+ String atAddress = Crypto.toATAddress(this.deployATTransactionData.getSignature());
+ this.deployATTransactionData.setAtAddress(atAddress);
+ return;
+ }
+
int blockHeight = this.getHeight();
if (blockHeight == 0)
blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1;
@@ -189,8 +200,21 @@ public class DeployAtTransaction extends Transaction {
// Check creation bytes are valid (for v2+)
if (this.getVersion() >= 2) {
// Do actual validation
+ ensureATAddress();
+
+ // 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();
+ ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
+
+ int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
+ long blockTimestamp = Timestamp.toLong(height, 0);
+
+ QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
+
try {
- new MachineState(deployATTransactionData.getCreationBytes());
+ new MachineState(api, deployATTransactionData.getCreationBytes());
} catch (IllegalArgumentException e) {
// Not valid
return ValidationResult.INVALID_CREATION_BYTES;
diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java
index 36dbdc5a..98e2daa7 100644
--- a/src/test/java/org/qortal/test/btcacct/DeployAT.java
+++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java
@@ -129,8 +129,13 @@ public class DeployAT {
}
BigDecimal fee = BigDecimal.ZERO;
+ String name = "QORT-BTC cross-chain trade";
+ String description = String.format("Qortal-Bitcoin cross-chain trade between %s and %s", refundAccount.getAddress(), redeemAddress);
+ String atType = "ACCT";
+ String tags = "QORT-BTC ACCT";
+
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null);
- TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, "QORT-BTC", "QORT-BTC ACCT", "", "", creationBytes, qortAmount, Asset.QORT);
+ TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, qortAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
diff --git a/src/test/java/org/qortal/test/btcacct/GetTransaction.java b/src/test/java/org/qortal/test/btcacct/GetTransaction.java
index c97762b9..48af4ebf 100644
--- a/src/test/java/org/qortal/test/btcacct/GetTransaction.java
+++ b/src/test/java/org/qortal/test/btcacct/GetTransaction.java
@@ -22,32 +22,32 @@ public class GetTransaction {
if (error != null)
System.err.println(error);
- System.err.println(String.format("usage: GetTransaction "));
- System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660"));
- System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e"));
+ System.err.println(String.format("usage: GetTransaction "));
+ System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000"));
+ System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000"));
System.exit(1);
}
public static void main(String[] args) {
- if (args.length < 1 || args.length > 1)
+ if (args.length < 2 || args.length > 2)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
byte[] transactionId = null;
+ int startTime = 0;
try {
int argIndex = 0;
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
+
+ startTime = Integer.parseInt(args[argIndex++]);
} catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage()));
}
- // Chain replay start time
- long startTime = (System.currentTimeMillis() / 1000L) - 14 * 24 * 60 * 60; // 14 days before now, in seconds
-
// Grab all outputs from transaction
List fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime);
if (fundingOutputs == null) {