diff --git a/src/masternodes/accounts.cpp b/src/masternodes/accounts.cpp index e8bbcd77ae6..4e26c6e011d 100644 --- a/src/masternodes/accounts.cpp +++ b/src/masternodes/accounts.cpp @@ -113,3 +113,11 @@ void CAccountsView::ForEachFuturesUserValues(std::function(callback, start); } +Res CAccountsView::EraseFuturesUserValues(const CFuturesUserKey& key) +{ + if (!EraseBy(key)) { + return Res::Err("Failed to erase futures"); + } + + return Res::Ok(); +} diff --git a/src/masternodes/accounts.h b/src/masternodes/accounts.h index a802f26f991..c34d74f6f1a 100644 --- a/src/masternodes/accounts.h +++ b/src/masternodes/accounts.h @@ -34,6 +34,10 @@ struct CFuturesUserKey { READWRITE(WrapBigEndian(txn_)); } } + + bool operator<(const CFuturesUserKey& o) const { + return std::tie(height, owner, txn) < std::tie(o.height, o.owner, o.txn); + } }; struct CFuturesUserValue { @@ -66,6 +70,7 @@ class CAccountsView : public virtual CStorageView Res UpdateBalancesHeight(CScript const & owner, uint32_t height); Res StoreFuturesUserValues(const CFuturesUserKey& key, const CFuturesUserValue& futures); + Res EraseFuturesUserValues(const CFuturesUserKey& key); void ForEachFuturesUserValues(std::function callback, const CFuturesUserKey& start = {}); // tags diff --git a/src/masternodes/balances.h b/src/masternodes/balances.h index c5349f3fd65..cf8dc77be50 100644 --- a/src/masternodes/balances.h +++ b/src/masternodes/balances.h @@ -228,6 +228,7 @@ struct CDFIP2203Message { CScript owner; CTokenAmount source{}; uint32_t destination{}; + bool withdraw{}; ADD_SERIALIZE_METHODS; @@ -236,6 +237,7 @@ struct CDFIP2203Message { READWRITE(owner); READWRITE(source); READWRITE(destination); + READWRITE(withdraw); } }; diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 4042d7ee4ab..afbcbc5b040 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -1532,23 +1532,75 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("Failed to get smart contract address from chainparams"); } - CTxDestination dest; - ExtractDestination(contractAddress, dest); + CDataStructureV0 liveKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::DFIP2203Tokens}; + auto balances = attributes->GetValue(liveKey, CBalances{}); - auto res = TransferTokenBalance(obj.source.nTokenId, obj.source.nValue, obj.owner, contractAddress); - if (!res) { - return res; - } + if (obj.withdraw) { + const auto blockPeriod = attributes->GetValue(blockKey, CAmount{}); + const uint32_t startHeight = height - (height % blockPeriod); + std::map userFuturesValues; - res = mnview.StoreFuturesUserValues({height, obj.owner, txn}, {obj.source, obj.destination}); - if (!res) { - return res; - } + mnview.ForEachFuturesUserValues([&](const CFuturesUserKey& key, const CFuturesUserValue& futuresValues) { + if (key.height <= startHeight) { + return false; + } - CDataStructureV0 liveKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::DFIP2203Tokens}; - auto balances = attributes->GetValue(liveKey, CBalances{}); + if (source->symbol == "DUSD") { + if (key.owner == obj.owner && futuresValues.destination == obj.destination) { + userFuturesValues[key] = futuresValues; + } + } else { + if (key.owner == obj.owner && futuresValues.source.nTokenId == obj.source.nTokenId) { + userFuturesValues[key] = futuresValues; + } + } + + return true; + }, {height, obj.owner, std::numeric_limits::max()}); + + CTokenAmount totalFutures{}; + totalFutures.nTokenId = obj.source.nTokenId; + + for (const auto& [key, value] : userFuturesValues) { + totalFutures.Add(value.source.nValue); + mnview.EraseFuturesUserValues(key); + } + + auto res = totalFutures.Sub(obj.source.nValue); + if (!res) { + return res; + } + + if (totalFutures.nValue > 0) { + auto res = mnview.StoreFuturesUserValues({height, obj.owner, txn}, {totalFutures, obj.destination}); + if (!res) { + return res; + } + } + + res = TransferTokenBalance(obj.source.nTokenId, obj.source.nValue, contractAddress, obj.owner); + if (!res) { + return res; + } + + res = balances.Sub(CTokenAmount{obj.source.nTokenId, obj.source.nValue}); + if (!res) { + return res; + } + } else { + auto res = TransferTokenBalance(obj.source.nTokenId, obj.source.nValue, obj.owner, contractAddress); + if (!res) { + return res; + } + + res = mnview.StoreFuturesUserValues({height, obj.owner, txn}, {obj.source, obj.destination}); + if (!res) { + return res; + } + + balances.Add(CTokenAmount{obj.source.nTokenId, obj.source.nValue}); + } - balances.Add(CTokenAmount{obj.source.nTokenId, obj.source.nValue}); attributes->attributes[liveKey] = balances; mnview.SetVariable(*attributes); diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index 9fc8f98129a..cd7df406a8d 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -2112,6 +2112,90 @@ UniValue futureswap(const JSONRPCRequest& request) { return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); } + +UniValue withdrawfutureswap(const JSONRPCRequest& request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{"withdrawfutureswap", + "\nCreates and submits to the network a withdrawl from futures contract transaction.\n" + " Withdrawal will be back to the address specified in the futures contract." + + HelpRequiringPassphrase(pwallet) + "\n", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "Address used to fund contract with"}, + {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "Amount to withdraw in amount@token format"}, + {"destination", RPCArg::Type::NUM, RPCArg::Optional::OMITTED_NAMED_ARG, "The dToken if DUSD supplied"}, + {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, + "A json array of json objects", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + }, + }, + }, + }, + }, + RPCResult{ + "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" + }, + RPCExamples{ + HelpExampleCli("futureswap", "dLb2jq51qkaUbVkLyCiVQCoEHzRSzRPEsJ 1000@TSLA") + + HelpExampleRpc("futureswap", "dLb2jq51qkaUbVkLyCiVQCoEHzRSzRPEsJ, 1000@TSLA") + }, + }.Check(request); + + if (pwallet->chain().isInitialBlockDownload()) { + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot create transactions while still in Initial Block Download"); + } + pwallet->BlockUntilSyncedToCurrentChain(); + + const auto dest = DecodeDestination(request.params[0].getValStr()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); + } + + CDFIP2203Message msg{}; + msg.owner = GetScriptForDestination(dest); + msg.source = DecodeAmount(pwallet->chain(), request.params[1], ""); + msg.withdraw = true; + + if (!request.params[2].isNull()) { + msg.destination = request.params[2].get_int(); + } + + // Encode + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + metadata << static_cast(CustomTxType::DFIP2203) + << msg; + + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(metadata); + + int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + + rawTx.vout.emplace_back(0, scriptMeta); + + CTransactionRef optAuthTx; + std::set auth{msg.owner}; + rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auth, false, optAuthTx, request.params[3]); + + // Set change address + CCoinControl coinControl; + coinControl.destChange = dest; + + // Fund + fund(rawTx, pwallet, optAuthTx, &coinControl); + + // Check execution + execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); + + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); +} + UniValue listpendingfutures(const JSONRPCRequest& request) { RPCHelpMan{"listpendingfutures", "Get all pending futures.\n", @@ -2142,7 +2226,7 @@ UniValue listpendingfutures(const JSONRPCRequest& request) { UniValue listFutures{UniValue::VARR}; pcustomcsview->ForEachFuturesUserValues([&](const CFuturesUserKey& key, const CFuturesUserValue& futuresValues){ - if (key.height < startPeriod) { + if (key.height <= startPeriod) { return false; } @@ -2214,7 +2298,7 @@ UniValue getpendingfutures(const JSONRPCRequest& request) { std::vector storedFutures; pcustomcsview->ForEachFuturesUserValues([&](const CFuturesUserKey& key, const CFuturesUserValue& futuresValues) { - if (key.height < startPeriod) { + if (key.height <= startPeriod) { return false; } @@ -2277,6 +2361,7 @@ static const CRPCCommand commands[] = {"accounts", "getburninfo", &getburninfo, {}}, {"accounts", "executesmartcontract", &executesmartcontract, {"name", "amount", "inputs"}}, {"accounts", "futureswap", &futureswap, {"name", "amount", "destination", "inputs"}}, + {"accounts", "withdrawfutureswap", &withdrawfutureswap, {"name", "amount", "destination", "inputs"}}, {"accounts", "listpendingfutures", &listpendingfutures, {}}, {"accounts", "getpendingfutures", &getpendingfutures, {"address"}}, }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index a76e8b7191c..d76f5452ac8 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -217,6 +217,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "accounttoutxos", 2, "inputs" }, { "futureswap", 2, "destination"}, { "futureswap", 3, "inputs"}, + { "withdrawfutureswap", 2, "destination"}, + { "withdrawfutureswap", 3, "inputs"}, { "icx_createorder", 0, "order" }, { "icx_createorder", 1, "inputs" }, diff --git a/src/validation.cpp b/src/validation.cpp index 06b3755290b..9027d920e5d 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3337,8 +3337,10 @@ void CChainState::ProcessFutures(const CBlockIndex* pindex, CCustomCSView& cache assert(destToken); const auto& premiumPrice = futuresPrices.at(destId).premium; - const auto total = DivideAmounts(futuresValues.source.nValue, premiumPrice); - cache.AddBalance(key.owner, {destId, total}); + if (premiumPrice > 0) { + const auto total = DivideAmounts(futuresValues.source.nValue, premiumPrice); + cache.AddBalance(key.owner, {destId, total}); + } } else { const auto tokenDUSD = cache.GetToken("DUSD"); assert(tokenDUSD); diff --git a/test/functional/feature_futures.py b/test/functional/feature_futures.py index e9100f4e4b8..2ca9570c1b4 100755 --- a/test/functional/feature_futures.py +++ b/test/functional/feature_futures.py @@ -3,7 +3,7 @@ # Copyright (c) DeFi Blockchain Developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -"""Test Futures's contract RPC.""" +"""Test Futures contract RPC.""" from test_framework.test_framework import DefiTestFramework @@ -11,6 +11,9 @@ from decimal import Decimal import time +def truncate(str, decimal): + return str if not str.find('.') + 1 else str[:str.find('.') + decimal + 1] + class FuturesTest(DefiTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -36,7 +39,16 @@ def run_test(self): self.test_dusd_to_dtoken() # Test futures block range - self.check_block_range() + self.check_swap_block_range() + + # Test multiple swaps per account + self.check_multiple_swaps() + + # Test withdrawal + self.check_withdrawals() + + # Test Satoshi swaps + self.check_minimum_swaps() def setup_test(self): @@ -244,9 +256,9 @@ def futures_pricing(self): self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount()) # Make sure that DUSD no longer is in futures prices result - result = self.nodes[0].listfutures() - assert_equal(len(result), 4) - for price in result: + self.prices = self.nodes[0].listfutures() + assert_equal(len(self.prices), 4) + for price in self.prices: assert(price['tokenSymbol'] != self.symbolDUSD) def test_dtoken_to_dusd(self): @@ -363,59 +375,56 @@ def test_dusd_to_dtoken(self): address_googl = self.nodes[0].getnewaddress("", "legacy") address_msft = self.nodes[0].getnewaddress("", "legacy") - # Get futures prices - prices = self.nodes[0].listfutures() - # Fund addresses - self.nodes[0].accounttoaccount(self.address, {address_tsla: f'{prices[0]["premiumPrice"]}@{self.symbolDUSD}'}) - self.nodes[0].accounttoaccount(self.address, {address_googl: f'{prices[1]["premiumPrice"]}@{self.symbolDUSD}'}) - self.nodes[0].accounttoaccount(self.address, {address_twtr: f'{prices[2]["premiumPrice"]}@{self.symbolDUSD}'}) - self.nodes[0].accounttoaccount(self.address, {address_msft: f'{prices[3]["premiumPrice"]}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_tsla: f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_googl: f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_twtr: f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_msft: f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}'}) self.nodes[0].generate(1) # Create user futures contracts - self.nodes[0].futureswap(address_msft, f'{prices[3]["premiumPrice"]}@{self.symbolDUSD}', int(self.idMSFT)) + self.nodes[0].futureswap(address_msft, f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}', int(self.idMSFT)) self.nodes[0].generate(1) - self.nodes[0].futureswap(address_twtr, f'{prices[2]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTWTR)) + self.nodes[0].futureswap(address_twtr, f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTWTR)) self.nodes[0].generate(1) - self.nodes[0].futureswap(address_googl, f'{prices[1]["premiumPrice"]}@{self.symbolDUSD}', int(self.idGOOGL)) + self.nodes[0].futureswap(address_googl, f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}', int(self.idGOOGL)) self.nodes[0].generate(1) - self.nodes[0].futureswap(address_tsla, f'{prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].futureswap(address_tsla, f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) self.nodes[0].generate(1) # List user futures contracts result = self.nodes[0].listpendingfutures() assert_equal(result[0]['owner'], address_tsla) - assert_equal(result[0]['source'], f'{prices[0]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result[0]['source'], f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result[0]['destination'], self.symbolTSLA) assert_equal(result[1]['owner'], address_googl) - assert_equal(result[1]['source'], f'{prices[1]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result[1]['source'], f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result[1]['destination'], self.symbolGOOGL) assert_equal(result[2]['owner'], address_twtr) - assert_equal(result[2]['source'], f'{prices[2]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result[2]['source'], f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result[2]['destination'], self.symbolTWTR) assert_equal(result[3]['owner'], address_msft) - assert_equal(result[3]['source'], f'{prices[3]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result[3]['source'], f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result[3]['destination'], self.symbolMSFT) - # Get user MSFT futures swap by address + # Get user TSLA futures swap by address result = self.nodes[0].getpendingfutures(address_tsla) - assert_equal(result['values'][0]['source'], f'{prices[0]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['source'], f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result['values'][0]['destination'], self.symbolTSLA) # Get user GOOGL futures contracts by address result = self.nodes[0].getpendingfutures(address_googl) - assert_equal(result['values'][0]['source'], f'{prices[1]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['source'], f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result['values'][0]['destination'], self.symbolGOOGL) - # Get user TSLA futures contracts by address + # Get user TWTR futures contracts by address result = self.nodes[0].getpendingfutures(address_twtr) - assert_equal(result['values'][0]['source'], f'{prices[2]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['source'], f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result['values'][0]['destination'], self.symbolTWTR) - # Get user TWTR futures contracts by address + # Get user MSFT futures contracts by address result = self.nodes[0].getpendingfutures(address_msft) - assert_equal(result['values'][0]['source'], f'{prices[3]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['source'], f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}') assert_equal(result['values'][0]['destination'], self.symbolMSFT) # Move to next futures block @@ -456,16 +465,13 @@ def test_dusd_to_dtoken(self): result = self.nodes[0].getaccount(address_twtr) assert_equal(result, [f'1.00000000@{self.symbolTWTR}']) - def check_block_range(self): + def check_swap_block_range(self): # Create addresses for futures address = self.nodes[0].getnewaddress("", "legacy") - # Get futures prices - prices = self.nodes[0].listfutures() - # Fund addresses - self.nodes[0].accounttoaccount(self.address, {address: f'{prices[0]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address: f'{self.prices[0]["premiumPrice"] * 2}@{self.symbolDUSD}'}) self.nodes[0].generate(1) # Move to just before futures block @@ -473,13 +479,24 @@ def check_block_range(self): self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount() - 1) # Create user futures contracts on futures block - self.nodes[0].futureswap(address, f'{prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].futureswap(address, f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) self.nodes[0].generate(1) # Check that futures have been executed result = self.nodes[0].getaccount(address) assert_equal(result, [f'913.50000000@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}']) + # Check all pending swaps shows no entries + result = self.nodes[0].listpendingfutures() + assert_equal(len(result), 0) + + # Check user pending swaps is empty + result = self.nodes[0].getpendingfutures(address) + assert_equal(len(result['values']), 0) + + # Try and withdraw smallest amount now contract has been paid + assert_raises_rpc_error(-32600, 'amount 0.00000000 is less than 0.00000001', self.nodes[0].withdrawfutureswap, address, f'{Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idTSLA)) + # Move to just next futures block next_futures_block = self.nodes[0].getblockcount() + (self.futures_interval - (self.nodes[0].getblockcount() % self.futures_interval)) self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount()) @@ -492,5 +509,211 @@ def check_block_range(self): result = self.nodes[0].getaccount('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpsqgljc') assert_equal(result, [f'4905.60000000@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}', f'1.00000000@{self.symbolGOOGL}', f'1.00000000@{self.symbolTWTR}', f'1.00000000@{self.symbolMSFT}']) + def check_multiple_swaps(self): + + # Create addresses for futures + address_tsla = self.nodes[0].getnewaddress("", "legacy") + address_twtr = self.nodes[0].getnewaddress("", "legacy") + + # Fund addresses + self.nodes[0].accounttoaccount(self.address, {address_tsla: f'{self.prices[0]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_twtr: f'{self.prices[2]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].generate(1) + + # Create two user futures contracts + self.nodes[0].futureswap(address_tsla, f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].futureswap(address_tsla, f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].futureswap(address_twtr, f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTWTR)) + self.nodes[0].futureswap(address_twtr, f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTWTR)) + self.nodes[0].generate(1) + + # Get user TSLA futures swap by address + result = self.nodes[0].getpendingfutures(address_tsla) + assert_equal(result['values'][0]['source'], f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolTSLA) + assert_equal(result['values'][1]['source'], f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][1]['destination'], self.symbolTSLA) + + # Get user TWTR futures contracts by address + result = self.nodes[0].getpendingfutures(address_twtr) + assert_equal(result['values'][0]['source'], f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolTWTR) + assert_equal(result['values'][0]['source'], f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolTWTR) + + # Move to just next futures block + next_futures_block = self.nodes[0].getblockcount() + (self.futures_interval - (self.nodes[0].getblockcount() % self.futures_interval)) + self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount()) + + # Check that futures have been executed + result = self.nodes[0].getaccount(address_tsla) + assert_equal(result, [f'2.00000000@{self.symbolTSLA}']) + result = self.nodes[0].getaccount(address_twtr) + assert_equal(result, [f'2.00000000@{self.symbolTWTR}']) + + # Check contract address + result = self.nodes[0].getaccount('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpsqgljc') + assert_equal(result, [f'6810.30000000@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}', f'1.00000000@{self.symbolGOOGL}', f'1.00000000@{self.symbolTWTR}', f'1.00000000@{self.symbolMSFT}']) + + def check_withdrawals(self): + + # Create addresses for futures + address_tsla = self.nodes[0].getnewaddress("", "legacy") + address_twtr = self.nodes[0].getnewaddress("", "legacy") + address_googl = self.nodes[0].getnewaddress("", "legacy") + address_msft = self.nodes[0].getnewaddress("", "legacy") + + # Fund addresses + self.nodes[0].accounttoaccount(self.address, {address_tsla: f'{self.prices[0]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_googl: f'{self.prices[1]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_twtr: f'{self.prices[2]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].accounttoaccount(self.address, {address_msft: f'{self.prices[3]["premiumPrice"] * 2}@{self.symbolDUSD}'}) + self.nodes[0].generate(1) + + # Create user futures contracts + self.nodes[0].futureswap(address_msft, f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}', int(self.idMSFT)) + self.nodes[0].futureswap(address_msft, f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}', int(self.idMSFT)) + self.nodes[0].futureswap(address_twtr, f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTWTR)) + self.nodes[0].futureswap(address_twtr, f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTWTR)) + self.nodes[0].futureswap(address_googl, f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}', int(self.idGOOGL)) + self.nodes[0].futureswap(address_googl, f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}', int(self.idGOOGL)) + self.nodes[0].futureswap(address_tsla, f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].futureswap(address_tsla, f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].generate(1) + + # Get user MSFT futures swap by address + result = self.nodes[0].getpendingfutures(address_tsla) + assert_equal(result['values'][0]['source'], f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolTSLA) + assert_equal(result['values'][1]['source'], f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][1]['destination'], self.symbolTSLA) + + # Get user GOOGL futures contracts by address + result = self.nodes[0].getpendingfutures(address_googl) + assert_equal(result['values'][0]['source'], f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolGOOGL) + assert_equal(result['values'][1]['source'], f'{self.prices[1]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][1]['destination'], self.symbolGOOGL) + + # Get user TSLA futures contracts by address + result = self.nodes[0].getpendingfutures(address_twtr) + assert_equal(result['values'][0]['source'], f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolTWTR) + assert_equal(result['values'][1]['source'], f'{self.prices[2]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][1]['destination'], self.symbolTWTR) + + # Get user TWTR futures contracts by address + result = self.nodes[0].getpendingfutures(address_msft) + assert_equal(result['values'][0]['source'], f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolMSFT) + assert_equal(result['values'][1]['source'], f'{self.prices[3]["premiumPrice"]}@{self.symbolDUSD}') + assert_equal(result['values'][1]['destination'], self.symbolMSFT) + + # Check withdrawal failures + assert_raises_rpc_error(-32600, f'amount 0.00000000 is less than {self.prices[2]["premiumPrice"] * 2}', self.nodes[0].withdrawfutureswap, address_tsla, f'{self.prices[2]["premiumPrice"] * 2}@{self.symbolDUSD}', int(self.idTWTR)) + assert_raises_rpc_error(-32600, f'amount {self.prices[0]["premiumPrice"] * 2} is less than {(self.prices[0]["premiumPrice"] * 2) + Decimal("0.00000001")}', self.nodes[0].withdrawfutureswap, address_tsla, f'{(self.prices[0]["premiumPrice"] * 2) + Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idTSLA)) + + # Withdraw both TSLA contracts + self.nodes[0].withdrawfutureswap(address_tsla, f'{self.prices[0]["premiumPrice"] * 2}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].generate(1) + + # Check user pending swap is empty + result = self.nodes[0].getpendingfutures(address_tsla) + assert_equal(len(result['values']), 0) + + # Try and withdraw smallest amount now contract empty + assert_raises_rpc_error(-32600, 'amount 0.00000000 is less than 0.00000001', self.nodes[0].withdrawfutureswap, address_tsla, f'{Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idTSLA)) + + # Withdraw frm GOOGL everything but one Sat + self.nodes[0].withdrawfutureswap(address_googl, f'{(self.prices[1]["premiumPrice"] * 2) - Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idGOOGL)) + self.nodes[0].generate(1) + + # Check user pending swap + result = self.nodes[0].getpendingfutures(address_googl) + assert_equal(result['values'][0]['source'], f'0.00000001@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolGOOGL) + + # Withdraw one TWTR contract plus 1 Sat of the second one + self.nodes[0].withdrawfutureswap(address_twtr, f'{self.prices[2]["premiumPrice"] + Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idTWTR)) + self.nodes[0].generate(1) + + # Check user pending swap + result = self.nodes[0].getpendingfutures(address_twtr) + assert_equal(result['values'][0]['source'], f'{self.prices[2]["premiumPrice"] - Decimal("0.00000001")}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolTWTR) + + # Withdraw one Sat + self.nodes[0].withdrawfutureswap(address_msft, f'{Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idMSFT)) + self.nodes[0].generate(1) + + # Check user pending swap + result = self.nodes[0].getpendingfutures(address_msft) + assert_equal(result['values'][0]['source'], f'{(self.prices[3]["premiumPrice"] * 2) - Decimal("0.00000001")}@{self.symbolDUSD}') + assert_equal(result['values'][0]['destination'], self.symbolMSFT) + + # Move to next futures block + next_futures_block = self.nodes[0].getblockcount() + (self.futures_interval - (self.nodes[0].getblockcount() % self.futures_interval)) + self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount()) + + # Check final balances + result = self.nodes[0].getaccount(address_tsla) + assert_equal(result, [f'{self.prices[0]["premiumPrice"] * 2}@{self.symbolDUSD}']) + result = self.nodes[0].getaccount(address_twtr) + assert_equal(result, [f'{self.prices[2]["premiumPrice"] + Decimal("0.00000001")}@{self.symbolDUSD}', f'0.99999999@{self.symbolTWTR}']) + result = self.nodes[0].getaccount(address_googl) + assert_equal(result, [f'{(self.prices[1]["premiumPrice"] * 2) - Decimal("0.00000001")}@{self.symbolDUSD}']) + result = self.nodes[0].getaccount(address_msft) + assert_equal(result, [f'0.00000001@{self.symbolDUSD}', f'1.99999999@{self.symbolMSFT}']) + + # Check contract address + result = self.nodes[0].getaccount('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpsqgljc') + assert_equal(result, [f'7468.64999999@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}', f'1.00000000@{self.symbolGOOGL}', f'1.00000000@{self.symbolTWTR}', f'1.00000000@{self.symbolMSFT}']) + + # Check DFI2203 address on listgovs + result = self.nodes[0].listgovs()[8][0]['ATTRIBUTES'] + assert_equal(result['v0/live/economy/dfip_tokens'], [f'7468.64999999@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}', f'1.00000000@{self.symbolGOOGL}', f'1.00000000@{self.symbolTWTR}', f'1.00000000@{self.symbolMSFT}']) + + # Check DFI2203 address on getburninfo + result = self.nodes[0].getburninfo() + assert_equal(result['dfip2203'], [f'7468.64999999@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}', f'1.00000000@{self.symbolGOOGL}', f'1.00000000@{self.symbolTWTR}', f'1.00000000@{self.symbolMSFT}']) + + def check_minimum_swaps(self): + + # Create addresses for futures + address = self.nodes[0].getnewaddress("", "legacy") + + # Fund addresses + self.nodes[0].accounttoaccount(self.address, {address: f'{self.prices[0]["premiumPrice"]}@{self.symbolDUSD}'}) + self.nodes[0].generate(1) + + # Create user futures contract with 1 Satoshi + self.nodes[0].futureswap(address, f'{Decimal("0.00000001")}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].generate(1) + + # Move to just next futures block + next_futures_block = self.nodes[0].getblockcount() + (self.futures_interval - (self.nodes[0].getblockcount() % self.futures_interval)) + self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount()) + + # Check one Satoshi swap yields no TSLA + result = self.nodes[0].getaccount(address) + assert_equal(result, [f'{self.prices[0]["premiumPrice"] - Decimal("0.00000001")}@{self.symbolDUSD}']) + + # Check contract address + result = self.nodes[0].getaccount('bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpsqgljc') + assert_equal(result, [f'7468.65000000@{self.symbolDUSD}', f'1.00000000@{self.symbolTSLA}', f'1.00000000@{self.symbolGOOGL}', f'1.00000000@{self.symbolTWTR}', f'1.00000000@{self.symbolMSFT}']) + + # Create user futures contract to purchase one Satoshi of TSLA + min_purchase = round(self.prices[0]["premiumPrice"] / 100000000, 8) + self.nodes[0].futureswap(address, f'{min_purchase}@{self.symbolDUSD}', int(self.idTSLA)) + self.nodes[0].generate(1) + + # Move to just next futures block + next_futures_block = self.nodes[0].getblockcount() + (self.futures_interval - (self.nodes[0].getblockcount() % self.futures_interval)) + self.nodes[0].generate(next_futures_block - self.nodes[0].getblockcount()) + + # Check one Satoshi swap yields one TSLA Satoshi + result = self.nodes[0].getaccount(address) + assert_equal(result, [f'{self.prices[0]["premiumPrice"] - Decimal("0.00000001") - Decimal(min_purchase)}@{self.symbolDUSD}', f'0.00000001@{self.symbolTSLA}']) + if __name__ == '__main__': FuturesTest().main()