From a3517568831d0e68276046b6ac03972647c36df3 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 3 Aug 2020 15:52:17 +0100 Subject: [PATCH] WIP: trade-bot: add missing JavaTypeAdapter to TradeBotData.bitcoinAmount Also: unify "receiveAddress" to "receivingAddress" Fix missed changes from "recipient" to "partner" in BTCACCT, etc. --- .../model/CrossChainBitcoinRedeemRequest.java | 2 +- .../api/model/TradeBotCreateRequest.java | 2 +- .../api/resource/CrossChainResource.java | 14 +++--- .../java/org/qortal/controller/TradeBot.java | 36 +++++++------- .../java/org/qortal/crosschain/BTCACCT.java | 48 +++++++++---------- .../java/org/qortal/crosschain/BTCP2SH.java | 6 +-- .../qortal/data/crosschain/TradeBotData.java | 2 + 7 files changed, 56 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java index 68129a3c..074fd24d 100644 --- a/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java +++ b/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java @@ -26,7 +26,7 @@ public class CrossChainBitcoinRedeemRequest { public byte[] secret; @Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf") - public byte[] receivePublicKeyHash; + public byte[] receivingAccountInfo; public CrossChainBitcoinRedeemRequest() { } diff --git a/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java b/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java index 386715bd..adc319e3 100644 --- a/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java +++ b/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java @@ -28,7 +28,7 @@ public class TradeBotCreateRequest { public int tradeTimeout; @Schema(description = "Bitcoin address for receiving", example = "1NCTG9oLk41bU6pcehLNo9DVJup77EHAVx") - public String receiveAddress; + public String receivingAddress; public TradeBotCreateRequest() { } diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index efe86f23..fd7853c8 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -853,10 +853,10 @@ public class CrossChainResource { if (redeemRequest.secret == null || redeemRequest.secret.length != BTCACCT.SECRET_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - if (redeemRequest.receivePublicKeyHash == null) - redeemRequest.receivePublicKeyHash = redeemKey.getPubKeyHash(); + if (redeemRequest.receivingAccountInfo == null) + redeemRequest.receivingAccountInfo = redeemKey.getPubKeyHash(); - if (redeemRequest.receivePublicKeyHash.length != 20) + if (redeemRequest.receivingAccountInfo.length != 20) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); // Extract data from cross-chain trading AT @@ -899,7 +899,7 @@ public class CrossChainResource { Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue()); - org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret, redeemRequest.receivePublicKeyHash); + org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret, redeemRequest.receivingAccountInfo); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction); if (!wasBroadcast) @@ -961,15 +961,15 @@ public class CrossChainResource { public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) { Security.checkApiCallAllowed(request); - Address receiveAddress; + Address receivingAddress; try { - receiveAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receiveAddress); + receivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); } catch (AddressFormatException e) { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); } // We only support P2PKH addresses at this time - if (receiveAddress.getOutputScriptType() != ScriptType.P2PKH) + if (receivingAddress.getOutputScriptType() != ScriptType.P2PKH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); if (tradeBotCreateRequest.tradeTimeout < 60) diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 8286e843..db9f0f8d 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -108,17 +108,17 @@ public class TradeBot { byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey); byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); - // Convert Bitcoin receive address into public key hash (we only support P2PKH at this time) - Address bitcoinReceiveAddress; + // Convert Bitcoin receiving address into public key hash (we only support P2PKH at this time) + Address bitcoinReceivingAddress; try { - bitcoinReceiveAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receiveAddress); + bitcoinReceivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); } catch (AddressFormatException e) { - throw new DataException("Unsupported Bitcoin receive address: " + tradeBotCreateRequest.receiveAddress); + throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress); } - if (bitcoinReceiveAddress.getOutputScriptType() != ScriptType.P2PKH) - throw new DataException("Unsupported Bitcoin receive address: " + tradeBotCreateRequest.receiveAddress); + if (bitcoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH) + throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress); - byte[] bitcoinReceivePublicKeyHash = bitcoinReceiveAddress.getHash(); + byte[] bitcoinReceivingAccountInfo = bitcoinReceivingAddress.getHash(); PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey); @@ -151,7 +151,7 @@ public class TradeBot { tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, secretB, hashOfSecretB, tradeForeignPublicKey, tradeForeignPublicKeyHash, - tradeBotCreateRequest.bitcoinAmount, null, null, null, bitcoinReceivePublicKeyHash); + tradeBotCreateRequest.bitcoinAmount, null, null, null, bitcoinReceivingAccountInfo); repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); @@ -718,9 +718,9 @@ public class TradeBot { Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress); - byte[] receivePublicKeyHash = tradeBotData.getReceivingAccountInfo(); + byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); - Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, tradeBotData.getSecret(), receivePublicKeyHash); + Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, tradeBotData.getSecret(), receivingAccountInfo); if (!BTC.getInstance().broadcastTransaction(p2shRedeemTransaction)) { // We couldn't redeem P2SH-B at this time @@ -787,8 +787,8 @@ public class TradeBot { // Send 'redeem' MESSAGE to AT using both secrets byte[] secretA = tradeBotData.getSecret(); - String qortalReceiveAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH - byte[] messageData = BTCACCT.buildRedeemMessage(secretA, secretB, qortalReceiveAddress); + String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH + byte[] messageData = BTCACCT.buildRedeemMessage(secretA, secretB, qortalReceivingAddress); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), messageData, false, false); @@ -809,10 +809,10 @@ public class TradeBot { repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); - String receiveAddress = tradeBotData.getTradeNativeAddress(); + String receivingAddress = tradeBotData.getTradeNativeAddress(); LOGGER.info(() -> String.format("P2SH-B %s redeemed, using secrets to redeem AT %s. Funds should arrive at %s", - p2shAddress, tradeBotData.getAtAddress(), receiveAddress)); + p2shAddress, tradeBotData.getAtAddress(), receivingAddress)); } /** @@ -873,9 +873,9 @@ public class TradeBot { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress); - byte[] receivePublicKeyHash = tradeBotData.getReceivingAccountInfo(); + byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); - Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA, receivePublicKeyHash); + Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA, receivingAccountInfo); if (!BTC.getInstance().broadcastTransaction(p2shRedeemTransaction)) { // We couldn't redeem P2SH-A at this time @@ -887,9 +887,9 @@ public class TradeBot { repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); - String receiveAddress = BTC.getInstance().pkhToAddress(receivePublicKeyHash); + String receivingAddress = BTC.getInstance().pkhToAddress(receivingAccountInfo); - LOGGER.info(() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receiveAddress)); + LOGGER.info(() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress)); } /** diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 8f9948d9..3eb1689d 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -88,10 +88,10 @@ import com.google.common.primitives.Bytes; *
    *
  • secret-A
  • *
  • secret-B
  • - *
  • Qortal receive address of her chosing
  • + *
  • Qortal receiving address of her chosing
  • *
* - *
  • AT's QORT funds are sent to Qortal receive address
  • + *
  • AT's QORT funds are sent to Qortal receiving address
  • * * *
  • Bob checks AT, extracts secret-A @@ -124,7 +124,7 @@ public class BTCACCT { + 8 /*lockTimeB*/ + 24 /*hash of secret-A (padded from 20 to 24)*/ + 8 /*lockTimeA*/; - public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret*/ + 32 /*secret*/ + 32 /*partner's Qortal receive address padded from 25 to 32*/; + public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret*/ + 32 /*secret*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/; public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/; public enum Mode { @@ -195,7 +195,7 @@ public class BTCACCT { final int addrHashOfSecretAPointer = addrCounter++; final int addrRedeemMessageSecretBOffset = addrCounter++; - final int addrRedeemMessageReceiveAddressOffset = addrCounter++; + final int addrRedeemMessageReceivingAddressOffset = addrCounter++; final int addrMessageDataPointer = addrCounter++; final int addrMessageDataLength = addrCounter++; @@ -290,7 +290,7 @@ public class BTCACCT { assert dataByteBuffer.position() == addrHashOfSecretBPointer * MachineState.VALUE_SIZE : "addrHashOfSecretBPointer incorrect"; dataByteBuffer.putLong(addrHashOfSecretB); - // Index into data segment of recipient address, used by SET_B_IND + // Index into data segment of partner's Qortal address, used by SET_B_IND assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect"; dataByteBuffer.putLong(addrQortalPartnerAddress1); @@ -318,8 +318,8 @@ public class BTCACCT { assert dataByteBuffer.position() == addrRedeemMessageSecretBOffset * MachineState.VALUE_SIZE : "addrRedeemMessageSecretBOffset incorrect"; dataByteBuffer.putLong(32L); - // Offset into 'redeem' MESSAGE data payload for extracting Qortal receive address - assert dataByteBuffer.position() == addrRedeemMessageReceiveAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceiveAddressOffset incorrect"; + // Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address + assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect"; dataByteBuffer.putLong(64L); // Source location and length for hashing any passed secret @@ -540,12 +540,12 @@ public class BTCACCT { codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout))); codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop)); - /* Success! Pay arranged amount to receive address */ + /* Success! Pay arranged amount to receiving address */ labelPayout = codeByteBuffer.position(); - // Extract Qortal receive address from next 32 bytes of message from transaction into B register - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceiveAddressOffset)); - // Pay AT's balance to recipient + // Extract Qortal receiving address from next 32 bytes of message from transaction into B register + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset)); + // Pay AT's balance to receiving address codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount)); // Set redeemed mode codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.REDEEMED.value)); @@ -678,7 +678,7 @@ public class BTCACCT { // Skip pointer to message sender dataByteBuffer.position(dataByteBuffer.position() + 8); - // Skip 'trade' message data offset for recipient's bitcoin PKH + // Skip 'trade' message data offset for partner's bitcoin PKH dataByteBuffer.position(dataByteBuffer.position() + 8); // Skip pointer to partner's bitcoin PKH @@ -693,7 +693,7 @@ public class BTCACCT { // Skip 'redeem' message data offset for secret-B dataByteBuffer.position(dataByteBuffer.position() + 8); - // Skip 'redeem' message data offset for partner's Qortal receive address + // Skip 'redeem' message data offset for partner's Qortal receiving address dataByteBuffer.position(dataByteBuffer.position() + 8); // Skip pointer to message data @@ -751,9 +751,9 @@ public class BTCACCT { dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes // Potential partner's Bitcoin PKH - byte[] recipientBitcoinPKH = new byte[20]; - dataByteBuffer.get(recipientBitcoinPKH); - dataByteBuffer.position(dataByteBuffer.position() + 32 - recipientBitcoinPKH.length); // skip to 32 bytes + byte[] partnerBitcoinPKH = new byte[20]; + dataByteBuffer.get(partnerBitcoinPKH); + dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerBitcoinPKH.length); // skip to 32 bytes long modeValue = dataByteBuffer.getLong(); Mode mode = Mode.valueOf((int) (modeValue & 0xffL)); @@ -764,7 +764,7 @@ public class BTCACCT { tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; tradeData.qortalPartnerAddress = qortalRecipient; tradeData.hashOfSecretA = hashOfSecretA; - tradeData.partnerBitcoinPKH = recipientBitcoinPKH; + tradeData.partnerBitcoinPKH = partnerBitcoinPKH; tradeData.lockTimeA = lockTimeA; tradeData.lockTimeB = lockTimeB; } else { @@ -775,9 +775,9 @@ public class BTCACCT { } /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */ - public static byte[] buildOfferMessage(byte[] recipientBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) { + public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) { byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); - return Bytes.concat(recipientBitcoinPKH, hashOfSecretA, lockTimeABytes); + return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes); } /** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */ @@ -796,11 +796,11 @@ public class BTCACCT { /** Returns 'trade' MESSAGE payload for AT creator to send to AT. */ public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int lockTimeB) { byte[] data = new byte[TRADE_MESSAGE_LENGTH]; - byte[] recipientQortalAddressBytes = Base58.decode(partnerQortalTradeAddress); + byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress); byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); byte[] lockTimeBBytes = BitTwiddling.toBEByteArray((long) lockTimeB); - System.arraycopy(recipientQortalAddressBytes, 0, data, 0, recipientQortalAddressBytes.length); + System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length); System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length); System.arraycopy(lockTimeBBytes, 0, data, 56, lockTimeBBytes.length); System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length); @@ -820,13 +820,13 @@ public class BTCACCT { } /** Returns 'redeem' MESSAGE payload for trade partner/ to send to AT. */ - public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB, String qortalReceiveAddress) { + public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB, String qortalReceivingAddress) { byte[] data = new byte[REDEEM_MESSAGE_LENGTH]; - byte[] qortalReceiveAddressBytes = Base58.decode(qortalReceiveAddress); + byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress); System.arraycopy(secretA, 0, data, 0, secretA.length); System.arraycopy(secretB, 0, data, 32, secretB.length); - System.arraycopy(qortalReceiveAddressBytes, 0, data, 64, qortalReceiveAddressBytes.length); + System.arraycopy(qortalReceivingAddressBytes, 0, data, 64, qortalReceivingAddressBytes.length); return data; } diff --git a/src/main/java/org/qortal/crosschain/BTCP2SH.java b/src/main/java/org/qortal/crosschain/BTCP2SH.java index 319fc5ac..8a0fa546 100644 --- a/src/main/java/org/qortal/crosschain/BTCP2SH.java +++ b/src/main/java/org/qortal/crosschain/BTCP2SH.java @@ -163,10 +163,10 @@ public class BTCP2SH { * @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 - * @param receivePublicKeyHash PKH used for output + * @param receivingAccountInfo Bitcoin PKH used for output * @return Signed Bitcoin transaction for redeeming P2SH */ - public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivePublicKeyHash) { + public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) { Function redeemSigScriptBuilder = (txSigBytes) -> { // Build scriptSig with... ScriptBuilder scriptBuilder = new ScriptBuilder(); @@ -187,7 +187,7 @@ public class BTCP2SH { return scriptBuilder.build(); }; - return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivePublicKeyHash); + return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo); } /** Returns 'secret', if any, given list of raw bitcoin transactions. */ diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index fdfa9926..6544999b 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -8,6 +8,7 @@ import java.util.Map; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import io.swagger.v3.oas.annotations.media.Schema; @@ -46,6 +47,7 @@ public class TradeBotData { private byte[] tradeForeignPublicKey; private byte[] tradeForeignPublicKeyHash; + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) private long bitcoinAmount; // Never expose this via API