diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 8e6ed5765..c3fd51bc7 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -455,6 +455,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/GenesisMint.cpp src/ripple/app/tx/impl/Import.cpp src/ripple/app/tx/impl/Invoke.cpp + src/ripple/app/tx/impl/Remit.cpp src/ripple/app/tx/impl/SetSignerList.cpp src/ripple/app/tx/impl/SetTrust.cpp src/ripple/app/tx/impl/SignerEntries.cpp @@ -740,6 +741,7 @@ if (tests) src/test/app/RCLCensorshipDetector_test.cpp src/test/app/RCLValidations_test.cpp src/test/app/Regression_test.cpp + src/test/app/Remit_test.cpp src/test/app/SHAMapStore_test.cpp src/test/app/SetAuth_test.cpp src/test/app/SetRegularKey_test.cpp @@ -889,6 +891,7 @@ if (tests) src/test/jtx/impl/rate.cpp src/test/jtx/impl/regkey.cpp src/test/jtx/impl/reward.cpp + src/test/jtx/impl/remit.cpp src/test/jtx/impl/sendmax.cpp src/test/jtx/impl/seq.cpp src/test/jtx/impl/sig.cpp diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 84a5ff582..3cdf14bc6 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -597,7 +597,8 @@ ValidNewAccountRoot::finalize( return false; } - if ((tt == ttPAYMENT || tt == ttIMPORT || tt == ttGENESIS_MINT) && + if ((tt == ttPAYMENT || tt == ttIMPORT || tt == ttGENESIS_MINT || + tt == ttREMIT) && result == tesSUCCESS) { std::uint32_t const startingSeq{ diff --git a/src/ripple/app/tx/impl/Remit.cpp b/src/ripple/app/tx/impl/Remit.cpp new file mode 100644 index 000000000..987e03a47 --- /dev/null +++ b/src/ripple/app/tx/impl/Remit.cpp @@ -0,0 +1,501 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +namespace ripple { + +TxConsequences +Remit::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, TxConsequences::normal}; +} + +NotTEC +Remit::preflight(PreflightContext const& ctx) +{ + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + // There are no flags (other than universal). + JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; + return temINVALID_FLAG; + } + + AccountID const dstID = ctx.tx.getAccountID(sfDestination); + AccountID const srcID = ctx.tx.getAccountID(sfAccount); + + if (dstID == srcID) + { + JLOG(ctx.j.warn()) << "Malformed transaction: Remit to self."; + return temREDUNDANT; + } + + // sanity check amounts + if (ctx.tx.isFieldPresent(sfAmounts)) + { + std::map> already; + bool nativeAlready = false; + + STArray const& sEntries(ctx.tx.getFieldArray(sfAmounts)); + for (STObject const& sEntry : sEntries) + { + // Validate the AmountEntry. + if (sEntry.getFName() != sfAmountEntry) + { + JLOG(ctx.j.warn()) << "Malformed: Expected AmountEntry."; + return temMALFORMED; + } + + STAmount const amount = sEntry.getFieldAmount(sfAmount); + if (!isLegalNet(amount) || amount.signum() <= 0) + { + JLOG(ctx.j.warn()) << "Malformed transaction: bad amount: " + << amount.getFullText(); + return temBAD_AMOUNT; + } + + if (isBadCurrency(amount.getCurrency())) + { + JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency."; + return temBAD_CURRENCY; + } + + if (isXRP(amount)) + { + if (nativeAlready) + { + JLOG(ctx.j.warn()) << "Malformed transaction: Native " + "Currency appears more than once."; + return temMALFORMED; + } + + nativeAlready = true; + continue; + } + + auto found = already.find(amount.getCurrency()); + if (found == already.end()) + { + already.emplace( + amount.getCurrency(), + std::set{amount.getIssuer()}); + continue; + } + + if (found->second.find(amount.getIssuer()) != found->second.end()) + { + JLOG(ctx.j.warn()) << "Malformed transaction: Issued Currency " + "appears more than once."; + return temMALFORMED; + } + + found->second.emplace(amount.getIssuer()); + } + } + + // sanity check minturitoken + if (ctx.tx.isFieldPresent(sfMintURIToken)) + { + STObject const& mint = const_cast(ctx.tx) + .getField(sfMintURIToken) + .downcast(); + // RH TODO: iterate mint fields detect any that shouldnt be there + + Blob const uri = mint.getFieldVL(sfURI); + if (uri.size() < 1 || uri.size() > 256) + { + JLOG(ctx.j.warn()) + << "Malformed transaction: URI was too short/long."; + return temMALFORMED; + } + + if (!URIToken::validateUTF8(mint.getFieldVL(sfURI))) + { + JLOG(ctx.j.warn()) + << "Malformed transaction: Invalid UTF8 inside MintURIToken."; + return temMALFORMED; + } + + if (mint.isFieldPresent(sfFlags)) + { + if (mint.getFieldU32(sfFlags) & tfURITokenMintMask) + return temINVALID_FLAG; + } + } + + // check uritokenids for duplicates + if (ctx.tx.isFieldPresent(sfURITokenIDs)) + { + STVector256 ids = ctx.tx.getFieldV256(sfURITokenIDs); + std::sort(ids.begin(), ids.end()); + if (std::adjacent_find(ids.begin(), ids.end()) != ids.end()) + { + JLOG(ctx.j.warn()) + << "Malformed transaction: Duplicate URITokenID."; + return temMALFORMED; + } + } + + return preflight2(ctx); +} + +TER +Remit::doApply() +{ + if (!view().rules().enabled(featureRemit)) + return temDISABLED; + + Sandbox sb(&ctx_.view()); + + beast::Journal const& j = ctx_.journal; + + auto const srcAccID = ctx_.tx[sfAccount]; + + auto sleSrcAcc = sb.peek(keylet::account(srcAccID)); + if (!sleSrcAcc) + return terNO_ACCOUNT; + + XRPAmount const accountReserve{sb.fees().accountReserve(0)}; + XRPAmount const objectReserve{sb.fees().accountReserve(1) - accountReserve}; + + // amount of native tokens we will transfer to cover reserves for the + // tls/acc/uritokens created, and native tokens listed in amounts + XRPAmount nativeRemit{0}; + + AccountID const dstAccID{ctx_.tx[sfDestination]}; + auto sleDstAcc = sb.peek(keylet::account(dstAccID)); + auto const flags = !sleDstAcc ? 0 : sleDstAcc->getFlags(); + + // Check if the destination has disallowed incoming + if (sb.rules().enabled(featureDisallowIncoming) && + (flags & lsfDisallowIncomingRemit)) + return tecNO_PERMISSION; + + // the destination may require a dest tag + if ((flags & lsfRequireDestTag) && + !ctx_.tx.isFieldPresent(sfDestinationTag)) + { + JLOG(j.warn()) + << "Remit: DestinationTag required for this destination."; + return tecDST_TAG_NEEDED; + } + + // if the destination doesn't exist, create it. + bool const createDst = !sleDstAcc; + if (createDst) + { + // sender will pay the reserve + nativeRemit += accountReserve; + + // Create the account. + std::uint32_t const seqno{ + sb.rules().enabled(featureXahauGenesis) + ? sb.info().parentCloseTime.time_since_epoch().count() + : sb.rules().enabled(featureDeletableAccounts) ? sb.seq() : 1}; + + sleDstAcc = std::make_shared(keylet::account(dstAccID)); + sleDstAcc->setAccountID(sfAccount, dstAccID); + + sleDstAcc->setFieldU32(sfSequence, seqno); + sleDstAcc->setFieldU32(sfOwnerCount, 0); + + if (sb.exists(keylet::fees()) && sb.rules().enabled(featureXahauGenesis)) + { + auto sleFees = sb.peek(keylet::fees()); + uint64_t accIdx = sleFees->isFieldPresent(sfAccountCount) + ? sleFees->getFieldU64(sfAccountCount) + : 0; + sleDstAcc->setFieldU64(sfAccountIndex, accIdx); + sleFees->setFieldU64(sfAccountCount, accIdx + 1); + sb.update(sleFees); + } + + // we'll fix this up at the end + sleDstAcc->setFieldAmount(sfBalance, STAmount{XRPAmount{0}}); + sb.insert(sleDstAcc); + } + + // if theres a minted uritoken the sender pays for that + if (ctx_.tx.isFieldPresent(sfMintURIToken)) + { + nativeRemit += objectReserve; + STObject const& mint = const_cast(ctx_.tx) + .getField(sfMintURIToken) + .downcast(); + + Blob const& mintURI = mint.getFieldVL(sfURI); + + std::optional mintDigest; + if (mint.isFieldPresent(sfDigest)) + mintDigest = mint.getFieldH256(sfDigest); + + Keylet kl = keylet::uritoken(srcAccID, mintURI); + + // check that it doesn't already exist + if (sb.exists(kl)) + { + JLOG(j.trace()) << "Remit: tried to creat duplicate URIToken. Tx: " + << ctx_.tx.getTransactionID(); + return tecDUPLICATE; + } + + auto sleMint = std::make_shared(kl); + + sleMint->setAccountID(sfOwner, dstAccID); + sleMint->setAccountID(sfIssuer, srcAccID); + + sleMint->setFieldVL(sfURI, mintURI); + + if (mint.isFieldPresent(sfDigest)) + sleMint->setFieldH256(sfDigest, mint.getFieldH256(sfDigest)); + + sleMint->setFieldU32( + sfFlags, + mint.isFieldPresent(sfFlags) ? mint.getFieldU32(sfFlags) : 0); + + auto const page = view().dirInsert( + keylet::ownerDir(dstAccID), kl, describeOwnerDir(dstAccID)); + + JLOG(j_.trace()) << "Adding URIToken to owner directory " + << to_string(kl.key) << ": " + << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + sleMint->setFieldU64(sfOwnerNode, *page); + sb.insert(sleMint); + + // ensure there is a deletion blocker against the issuer now + sleSrcAcc->setFieldU32( + sfFlags, sleSrcAcc->getFlags() | lsfURITokenIssuer); + + adjustOwnerCount(sb, sleDstAcc, 1, j); + } + + // iterate uritokens + if (ctx_.tx.isFieldPresent(sfURITokenIDs)) + { + STVector256 ids = ctx_.tx.getFieldV256(sfURITokenIDs); + for (uint256 const klRaw : ids) + { + Keylet kl = keylet::unchecked(klRaw); + auto sleU = sb.peek(kl); + + // does it exist + if (!sleU) + { + JLOG(j.warn()) << "Remit: one or more uritokens did not exist " + "on the source account."; + return tecUNFUNDED_PAYMENT; + } + + // is it a uritoken? + if (sleU->getFieldU16(sfLedgerEntryType) != ltURI_TOKEN) + { + JLOG(j.warn()) << "Remit: one or more supplied URITokenIDs was " + "not actually a uritoken."; + return tecNO_ENTRY; + } + + // is it our uritoken? + if (sleU->getAccountID(sfOwner) != srcAccID) + { + JLOG(j.warn()) << "Remit: one or more supplied URITokenIDs was " + "not owned by sender."; + return tecNO_PERMISSION; + } + + // erase current sale offers, if any + if (sleU->isFieldPresent(sfAmount)) + sleU->makeFieldAbsent(sfAmount); + if (sleU->isFieldPresent(sfDestination)) + sleU->makeFieldAbsent(sfDestination); + + // pay the reserve + nativeRemit += objectReserve; + + // remove from sender dir + { + auto const page = (*sleU)[sfOwnerNode]; + if (!sb.dirRemove( + keylet::ownerDir(srcAccID), page, kl.key, true)) + { + JLOG(j.fatal()) + << "Could not remove URIToken from owner directory"; + return tefBAD_LEDGER; + } + + adjustOwnerCount(sb, sleSrcAcc, -1, j); + } + + // add to dest dir + { + auto const page = sb.dirInsert( + keylet::ownerDir(dstAccID), kl, describeOwnerDir(dstAccID)); + + JLOG(j_.trace()) << "Adding URIToken to owner directory " + << to_string(kl.key) << ": " + << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + sleU->setFieldU64(sfOwnerNode, *page); + + adjustOwnerCount(sb, sleDstAcc, 1, j); + } + + // change the owner + sleU->setAccountID(sfOwner, dstAccID); + + sb.update(sleU); + } + } + + // iterate trustlines + if (ctx_.tx.isFieldPresent(sfAmounts)) + { + // process trustline remits + STArray const& sEntries(ctx_.tx.getFieldArray(sfAmounts)); + for (STObject const& sEntry : sEntries) + { + STAmount const amount = sEntry.getFieldAmount(sfAmount); + if (isXRP(amount)) + { + // since we have to pay for all the created objects including + // possibly the account itself this is paid right at the end, + // and only if there is balance enough to cover. + nativeRemit += amount.xrp(); + continue; + } + + AccountID const issuerAccID = amount.getIssuer(); + + // check permissions + if (issuerAccID == srcAccID || issuerAccID == dstAccID) + { + // no permission check needed when the issuer sends out or a + // subscriber sends back RH TODO: move this condition into + // trustTransferAllowed, guarded by an amendment + } + else if (TER canXfer = trustTransferAllowed( + sb, + std::vector{srcAccID, dstAccID}, + amount.issue(), + j); + canXfer != tesSUCCESS) + return canXfer; + + // compute the amount the source will need to send + // in remit the sender pays all transfer fees, so that + // the destination can always be assured they got the exact amount + // specified. therefore we need to compute the amount + transfer fee + auto const srcAmt = + issuerAccID != srcAccID && issuerAccID != dstAccID + ? multiply(amount, transferRate(sb, issuerAccID)) + : amount; + + auto const dstAmt = amount; + + STAmount availableFunds{ + accountFunds(sb, srcAccID, srcAmt, fhZERO_IF_FROZEN, j)}; + + if (availableFunds < srcAmt) + return tecUNFUNDED_PAYMENT; + + // if the target trustline doesn't exist we need to create it and + // pay its reserve + if (!sb.exists(keylet::line( + issuerAccID == dstAccID ? srcAccID : dstAccID, + issuerAccID, + amount.getCurrency()))) + nativeRemit += objectReserve; + + // action the transfer + STAmount sentAmt; + if (TER result = + rippleSend(sb, srcAccID, dstAccID, dstAmt, sentAmt, j); + result != tesSUCCESS) + return result; + + if (sentAmt != srcAmt) + return tecINTERNAL; + } + } + + if (nativeRemit < beast::zero) + return tecINTERNAL; + + if (nativeRemit > beast::zero) + { + // ensure the account can cover the native remit + if (mSourceBalance < nativeRemit) + return tecUNFUNDED_PAYMENT; + + // subtract the balance from the sender + { + STAmount bal = mSourceBalance; + bal -= nativeRemit; + if (bal < beast::zero || bal > mSourceBalance) + return tecINTERNAL; + sleSrcAcc->setFieldAmount(sfBalance, bal); + } + + // add the balance to the destination + { + STAmount bal = sleDstAcc->getFieldAmount(sfBalance); + STAmount prior = bal; + bal += nativeRemit; + if (bal < beast::zero || bal < prior) + return tecINTERNAL; + sleDstAcc->setFieldAmount(sfBalance, bal); + } + } + + // apply + sb.update(sleSrcAcc); + sb.update(sleDstAcc); + sb.apply(ctx_.rawView()); + + return tesSUCCESS; +} + +XRPAmount +Remit::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount extraFee{0}; + + if (tx.isFieldPresent(sfBlob)) + extraFee += + XRPAmount{static_cast(tx.getFieldVL(sfBlob).size())}; + + // RH TODO: add fees + + return Transactor::calculateBaseFee(view, tx) + extraFee; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/Remit.h b/src/ripple/app/tx/impl/Remit.h new file mode 100644 index 000000000..56ac494be --- /dev/null +++ b/src/ripple/app/tx/impl/Remit.h @@ -0,0 +1,54 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_SIMPLE_PAYMENT_H_INCLUDED +#define RIPPLE_TX_SIMPLE_PAYMENT_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +class Remit : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit Remit(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/SetAccount.cpp b/src/ripple/app/tx/impl/SetAccount.cpp index 9702212a3..085e36422 100644 --- a/src/ripple/app/tx/impl/SetAccount.cpp +++ b/src/ripple/app/tx/impl/SetAccount.cpp @@ -577,6 +577,14 @@ SetAccount::doApply() uFlagsOut |= lsfDisallowIncomingTrustline; else if (uClearFlag == asfDisallowIncomingTrustline) uFlagsOut &= ~lsfDisallowIncomingTrustline; + + if (ctx_.view().rules().enabled(featureRemit)) + { + if (uSetFlag == asfDisallowIncomingRemit) + uFlagsOut |= lsfDisallowIncomingRemit; + else if (uClearFlag == asfDisallowIncomingRemit) + uFlagsOut &= ~lsfDisallowIncomingRemit; + } } if (uFlagsIn != uFlagsOut) diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index ae2d35463..d94d2c0dc 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -164,6 +164,8 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttINVOKE: return invoke_preflight_helper(ctx); + case ttREMIT: + return invoke_preflight_helper(ctx); case ttURITOKEN_MINT: case ttURITOKEN_BURN: case ttURITOKEN_BUY: @@ -283,6 +285,8 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttINVOKE: return invoke_preclaim(ctx); + case ttREMIT: + return invoke_preclaim(ctx); case ttURITOKEN_MINT: case ttURITOKEN_BURN: case ttURITOKEN_BUY: @@ -364,6 +368,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return Import::calculateBaseFee(view, tx); case ttINVOKE: return Invoke::calculateBaseFee(view, tx); + case ttREMIT: + return Remit::calculateBaseFee(view, tx); case ttURITOKEN_MINT: case ttURITOKEN_BURN: case ttURITOKEN_BUY: @@ -543,6 +549,10 @@ invoke_apply(ApplyContext& ctx) Invoke p(ctx); return p(); } + case ttREMIT: { + Remit p(ctx); + return p(); + } case ttURITOKEN_MINT: case ttURITOKEN_BURN: case ttURITOKEN_BUY: diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 86ccf93d8..650212507 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -389,6 +389,18 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); +// Send regardless of limits. +// --> saAmount: Amount/currency/issuer to deliver to receiver. +// <-- saActual: Amount actually cost. Sender pays fees. +TER +rippleSend( + ApplyView& view, + AccountID const& uSenderID, + AccountID const& uReceiverID, + STAmount const& saAmount, + STAmount& saActual, + beast::Journal j); + [[nodiscard]] TER accountSend( ApplyView& view, @@ -632,9 +644,9 @@ trustTransferAllowed( std::is_same::value || std::is_same::value); typedef typename std::conditional< - std::is_same::value, - std::shared_ptr, - std::shared_ptr>::type SLEPtr; + std::is_same::value, + std::shared_ptr, + std::shared_ptr>::type SLEPtr; if (isBadCurrency(issue.currency)) return tecNO_PERMISSION; diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 060e8d1ab..7ecfb6100 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -1150,7 +1150,7 @@ rippleCredit( // Send regardless of limits. // --> saAmount: Amount/currency/issuer to deliver to receiver. // <-- saActual: Amount actually cost. Sender pays fees. -static TER +TER rippleSend( ApplyView& view, AccountID const& uSenderID, diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index e3ddeb632..6fc556a08 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 66; +static constexpr std::size_t numFeatures = 67; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -354,6 +354,7 @@ extern uint256 const featureImport; extern uint256 const featureXahauGenesis; extern uint256 const featureHooksUpdate1; extern uint256 const fixXahauV1; +extern uint256 const featureRemit; } // namespace ripple diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 0a34a84e6..6134a8f33 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -285,6 +285,8 @@ enum LedgerSpecificFlags { 0x20000000, // True, reject new trustlines (only if no issued assets) lsfURITokenIssuer = 0x40000000, // True, has minted tokens in the past + lsfDisallowIncomingRemit = // True, no remits allowed to this account + 0x80000000, // ltOFFER lsfPassive = 0x00010000, diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index cd5dda504..1f9d15368 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -552,6 +552,7 @@ extern SF_ACCOUNT const sfEmitCallback; // account (uncommon) extern SF_ACCOUNT const sfHookAccount; extern SF_ACCOUNT const sfNFTokenMinter; +extern SF_ACCOUNT const sfInform; // path set extern SField const sfPaths; @@ -562,6 +563,7 @@ extern SF_VECTOR256 const sfHashes; extern SF_VECTOR256 const sfAmendments; extern SF_VECTOR256 const sfNFTokenOffers; extern SF_VECTOR256 const sfHookNamespaces; +extern SF_VECTOR256 const sfURITokenIDs; // inner object // OBJECT/1 is reserved for end of object @@ -590,6 +592,8 @@ extern SField const sfHookGrant; extern SField const sfActiveValidator; extern SField const sfImportVLKey; extern SField const sfHookEmission; +extern SField const sfMintURIToken; +extern SField const sfAmountEntry; // array of objects (common) // ARRAY/1 is reserved for end of array @@ -617,6 +621,7 @@ extern SField const sfGenesisMints; extern SField const sfActiveValidators; extern SField const sfImportVLKeys; extern SField const sfHookEmissions; +extern SField const sfAmounts; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index 23f57e4cb..b27104a67 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -86,6 +86,7 @@ constexpr std::uint32_t asfDisallowIncomingNFTokenOffer = 12; constexpr std::uint32_t asfDisallowIncomingCheck = 13; constexpr std::uint32_t asfDisallowIncomingPayChan = 14; constexpr std::uint32_t asfDisallowIncomingTrustline = 15; +constexpr std::uint32_t asfDisallowIncomingRemit = 16; // OfferCreate flags: constexpr std::uint32_t tfPassive = 0x00010000; diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 7999ee5b4..2f287efe5 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -146,6 +146,10 @@ enum TxType : std::uint16_t ttURITOKEN_CREATE_SELL_OFFER = 48, ttURITOKEN_CANCEL_SELL_OFFER = 49, + /* A payment transactor that delivers only the exact amounts specified, creating accounts and TLs as needed + * that the sender pays for. */ + ttREMIT = 95, + /** This transaction can only be used by the genesis account, which is controlled exclusively by * rewards/governance hooks, to print new XRP to be delivered directly to an array of destinations, * according to reward schedule */ diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 6603685fb..bac11e0cb 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -460,7 +460,7 @@ REGISTER_FEATURE(Import, Supported::yes, VoteBehavior::De REGISTER_FEATURE(XahauGenesis, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FEATURE(HooksUpdate1, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixXahauV1, Supported::yes, VoteBehavior::DefaultNo); - +REGISTER_FEATURE(Remit, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index d98ad2056..c6d0340c1 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -141,6 +141,21 @@ InnerObjectFormats::InnerObjectFormats() {sfPublicKey, soeREQUIRED}, {sfAccount, soeOPTIONAL}, }); + + add(sfAmountEntry.jsonName.c_str(), + sfAmountEntry.getCode(), + { + {sfAmount, soeREQUIRED}, + {sfFlags, soeOPTIONAL}, + }); + + add(sfMintURIToken.jsonName.c_str(), + sfMintURIToken.getCode(), + { + {sfURI, soeREQUIRED}, + {sfDigest, soeOPTIONAL}, + {sfFlags, soeOPTIONAL}, + }); } InnerObjectFormats const& diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 889885442..a72208607 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -305,6 +305,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); +CONSTRUCT_TYPED_SFIELD(sfInform, "Inform", ACCOUNT, 99); // vector of 256-bit CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR256, 1, SField::sMD_Never); @@ -312,6 +313,7 @@ CONSTRUCT_TYPED_SFIELD(sfHashes, "Hashes", VECTOR25 CONSTRUCT_TYPED_SFIELD(sfAmendments, "Amendments", VECTOR256, 3); CONSTRUCT_TYPED_SFIELD(sfNFTokenOffers, "NFTokenOffers", VECTOR256, 4); CONSTRUCT_TYPED_SFIELD(sfHookNamespaces, "HookNamespaces", VECTOR256, 5); +CONSTRUCT_TYPED_SFIELD(sfURITokenIDs, "URITokenIDs", VECTOR256, 99); // path set CONSTRUCT_UNTYPED_SFIELD(sfPaths, "Paths", PATHSET, 1); @@ -346,6 +348,8 @@ CONSTRUCT_UNTYPED_SFIELD(sfGenesisMint, "GenesisMint", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfActiveValidator, "ActiveValidator", OBJECT, 95); CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, 94); CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93); +CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92); +CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91); // array of objects // ARRAY/1 is reserved for end of array @@ -370,6 +374,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfGenesisMints, "GenesisMints", ARRAY, CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, 95); CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94); CONSTRUCT_UNTYPED_SFIELD(sfHookEmissions, "HookEmissions", ARRAY, 93); +CONSTRUCT_UNTYPED_SFIELD(sfAmounts, "Amounts", ARRAY, 92); // clang-format on diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 61eec4762..6c38711ad 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -116,6 +116,21 @@ TxFormats::TxFormats() }, commonFields); + add(jss::Remit, + ttREMIT, + { + {sfDestination, soeREQUIRED}, + {sfAmounts, soeOPTIONAL}, + {sfURITokenIDs, soeOPTIONAL}, + {sfMintURIToken, soeOPTIONAL}, + {sfInvoiceID, soeOPTIONAL}, + {sfDestinationTag, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, + {sfBlob, soeOPTIONAL}, + {sfInform, soeOPTIONAL}, + }, + commonFields); + add(jss::EscrowCreate, ttESCROW_CREATE, { diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 699928f7e..3bcd4fd0f 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -117,6 +117,7 @@ JSS(Payment); // transaction type. JSS(PaymentChannelClaim); // transaction type. JSS(PaymentChannelCreate); // transaction type. JSS(PaymentChannelFund); // transaction type. +JSS(Remit); // transaction type. JSS(RippleState); // ledger type. JSS(SLE_hit_rate); // out: GetCounts. JSS(SetFee); // transaction type. diff --git a/src/ripple/rpc/handlers/ServerDefinitions.cpp b/src/ripple/rpc/handlers/ServerDefinitions.cpp index a7954b99a..d1448bad4 100644 --- a/src/ripple/rpc/handlers/ServerDefinitions.cpp +++ b/src/ripple/rpc/handlers/ServerDefinitions.cpp @@ -422,6 +422,7 @@ doServerDefinitions(RPC::JsonContext& context) uint32_t curLgrSeq = context.ledgerMaster.getValidatedLedger()->info().seq; // static values used for cache + static thread_local uint32_t lastGenerated = 0; // last ledger seq it was generated static thread_local Json::Value lastFeatures{ @@ -429,7 +430,6 @@ doServerDefinitions(RPC::JsonContext& context) static thread_local uint256 lastFeatureHash; // the hash of the features JSON last time // it was generated - // if a flag ledger has passed since it was last generated, regenerate it, // update the cache above if (curLgrSeq > ((lastGenerated >> 8) + 1) << 8 || lastGenerated == 0) diff --git a/src/test/app/Remit_test.cpp b/src/test/app/Remit_test.cpp new file mode 100644 index 000000000..443f3721e --- /dev/null +++ b/src/test/app/Remit_test.cpp @@ -0,0 +1,1222 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 XRPL-Labs + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// Destination Exists - Trust Line Exists +// otxn | dest | exists | native | token | tl exists | uris xfr | uri mint | +// A | B | Y | N | N | N | N | N | +// A | B | Y | Y | N | N | N | N | +// A | B | Y | N | Y | Y | N | N | +// A | B | Y | Y | Y | Y | N | N | +// A | B | Y | N | N | N | Y | N | +// A | B | Y | N | N | N | N | Y | +// A | B | Y | Y | N | N | Y | N | +// A | B | Y | Y | Y | Y | Y | N | +// A | B | Y | Y | N | N | Y | Y | +// A | B | Y | Y | Y | Y | Y | Y | + +/* +// Destination Exists - Trust Line DNE +// otxn | dest | exists | native | token | tl exists | uris xfr | uri mint | +// A | B | Y | N | Y | N | N | N | +// A | B | Y | Y | Y | N | N | N | +// A | B | Y | Y | Y | N | Y | N | +// A | B | Y | Y | Y | N | Y | Y | + +// Destination Does Not Exist - Trust Line Exists +// otxn | dest | exists | native | token | tl exists | uris xfr | uri mint | +// A | B | N | N | N | N | N | N | +// A | B | N | Y | N | N | N | N | +// A | B | N | N | Y | Y | N | N | +// A | B | N | Y | Y | Y | N | N | +// A | B | N | N | N | N | Y | N | +// A | B | N | N | N | N | N | Y | +// A | B | N | Y | N | N | Y | N | +// A | B | N | Y | Y | Y | Y | N | +// A | B | N | Y | N | N | Y | Y | +// A | B | N | Y | Y | Y | Y | Y | + +// Destination Exists - Trust Line DNE +// otxn | dest | exists | native | token | tl exists | uris xfr | uri mint | +// A | B | N | N | Y | N | N | N | +// A | B | N | Y | Y | N | N | N | +// A | B | N | Y | Y | N | Y | N | +// A | B | N | Y | Y | N | Y | Y | +*/ + +// Round Robin Test +// Multiple in one Ledger Test +// Forward and Backward remits +// Compute a uri token id inline with the send (a token that is being minted) + +namespace ripple { +namespace test { +struct Remit_test : public beast::unit_test::suite +{ + + // testDebug("PRE", env, { alice, bob }, {}); + void + testDebug( + std::string const& testNumber, + jtx::Env const& env, + std::vector const& accounts, + std::vector const& ious) + { + std::cout << "DEBUG: " << testNumber << "\n"; + for (std::size_t a = 0; a < accounts.size(); ++a) + { + auto const bal = env.balance(accounts[a]); + std::cout << "account: " << accounts[a].human() << "BAL: " << bal << "\n"; + for (std::size_t i = 0; i < ious.size(); ++i) + { + auto const iouBal = env.balance(accounts[a], ious[i]); + std::cout << "account: " << accounts[a].human() << "IOU: " << iouBal << "\n"; + } + } + } + + static bool + inOwnerDir( + ReadView const& view, + jtx::Account const& acct, + uint256 const& tid) + { + auto const uritSle = view.read({ltURI_TOKEN, tid}); + ripple::Dir const ownerDir(view, keylet::ownerDir(acct.id())); + return std::find(ownerDir.begin(), ownerDir.end(), uritSle) != + ownerDir.end(); + } + + static std::size_t + ownerDirCount(ReadView const& view, jtx::Account const& acct) + { + ripple::Dir const ownerDir(view, keylet::ownerDir(acct.id())); + return std::distance(ownerDir.begin(), ownerDir.end()); + }; + + static AccountID + tokenOwner(ReadView const& view, uint256 const& id) + { + auto const slep = view.read({ltURI_TOKEN, id}); + if (!slep) + return AccountID(); + return slep->getAccountID(sfOwner); + } + + static AccountID + tokenIsser(ReadView const& view, uint256 const& id) + { + auto const slep = view.read({ltURI_TOKEN, id}); + if (!slep) + return AccountID(); + return slep->getAccountID(sfIssuer); + } + + void + testEnabled(FeatureBitset features) + { + // 0D8BF22FF7570D58598D1EF19EBB6E142AD46E59A223FD3816262FBB69345BEA + + testcase("enabled"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + for (bool const withRemit : {true, false}) + { + auto const amend = + withRemit ? features : features - featureRemit; + + Env env{*this, amend}; + + env.fund(XRP(1000), alice, bob); + + auto const txResult = + withRemit ? ter(tesSUCCESS) : ter(temDISABLED); + + // REMIT + env(remit::remit(alice, bob), txResult); + env.close(); + } + } + + void + testDestExistsTLExists(FeatureBitset features) + { + testcase("dest exists and trustline exists"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // REMIT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env{*this, features}; + // Env env{*this, envconfig(), amend, nullptr, + // // beast::severities::kWarning + // beast::severities::kTrace + // }; + + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob); + env.close(); + + env(remit::remit(alice, bob), ter(tesSUCCESS)); + env.close(); + // auto const preAlice = env.balance(alice, USD.issue()); + } + + // REMIT: XAH + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + ter(tesSUCCESS)); + env.close(); + auto const postAlice = env.balance(alice); + auto const postBob = env.balance(bob); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops); + BEAST_EXPECT(postBob == preBob + XRP(1)); + } + + // REMIT: USD + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // setup + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // validate + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - feeDrops); + BEAST_EXPECT(postBob == preBob); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + } + + // REMIT: XAH + USD + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // setup + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // validate + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops); + BEAST_EXPECT(postBob == preBob + XRP(1)); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + } + + // REMIT: URITOKEN XFER + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob); + env.close(); + + // mint uri token + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid)); + + // remit with uritoken id + env(remit::remit(alice, bob), + remit::token_ids({ strHex(tid) }), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: URITOKEN MINT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob); + env.close(); + + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(remit::remit(alice, bob), remit::uri(uri), ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid) == alice.id()); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH + URITOKEN XFER + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob); + env.close(); + + // mint uri token + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // remit xah + uritoken id + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + remit::token_ids({ strHex(tid) }), + ter(tesSUCCESS)); + env.close(); + + // verify uri transfer + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + + // verify xah + auto const postAlice = env.balance(alice); + auto const postBob = env.balance(bob); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - feeReserve); + BEAST_EXPECT(postBob == preBob + XRP(1) + feeReserve); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: USD + URITOKEN XFER + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // mint uri token + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + remit::token_ids({ strHex(tid) }), + ter(tesSUCCESS)); + env.close(); + + // verify uri transfer + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + + // verify usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - feeDrops - feeReserve); + BEAST_EXPECT(postBob == preBob + feeReserve); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH/USD + URITOKEN XFER + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // mint uri token + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + remit::token_ids({ strHex(tid) }), + ter(tesSUCCESS)); + env.close(); + + // verify uri transfer + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + + // verify xah & usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - feeReserve); + BEAST_EXPECT(postBob == preBob + XRP(1) + feeReserve); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH + URITOKEN XFER + URITOKEN MINT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob); + env.close(); + + // mint uri token + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // remit xah/usd + uritoken id + uritoken mint + std::string const uri2(maxTokenURILength - 1, '?'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + remit::token_ids({ strHex(tid1) }), + remit::uri(uri2), + ter(tesSUCCESS)); + env.close(); + + // verify uri mint + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid2)); + BEAST_EXPECT(tokenOwner(*env.current(), tid2) == bob.id()); + + // verify uri transfer + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid1) == alice.id()); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + + // verify xah + auto const postAlice = env.balance(alice); + auto const postBob = env.balance(bob); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - (feeReserve * 2)); + BEAST_EXPECT(postBob == preBob + XRP(1) + (feeReserve * 2)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + + // REMIT: USD + URITOKEN XFER + URITOKEN MINT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // mint uri token + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + uritoken mint + std::string const uri2(maxTokenURILength - 1, '?'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + remit::token_ids({ strHex(tid1) }), + remit::uri(uri2), + ter(tesSUCCESS)); + env.close(); + + // verify uri mint + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid2)); + BEAST_EXPECT(tokenOwner(*env.current(), tid2) == bob.id()); + + // verify uri transfer + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid1) == alice.id()); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 3); + + // verify usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - feeDrops - (feeReserve * 2)); + BEAST_EXPECT(postBob == preBob + (feeReserve * 2)); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + + // REMIT: XAH/USD + URITOKEN XFER + URITOKEN MINT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // mint uri token + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + uritoken mint + std::string const uri2(maxTokenURILength - 1, '?'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + remit::token_ids({ strHex(tid1) }), + remit::uri(uri2), + ter(tesSUCCESS)); + env.close(); + + // verify uri mint + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid2)); + BEAST_EXPECT(tokenOwner(*env.current(), tid2) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid2) == alice.id()); + + // verify uri transfer + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid1) == alice.id()); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 3); + + // verify xah & usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - (feeReserve * 2)); + BEAST_EXPECT(postBob == preBob + XRP(1) + (feeReserve * 2)); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + } + + void + testDestExistsTLDNE(FeatureBitset features) + { + testcase("dest exists and trustline does not exist"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // REMIT: USD + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // setup + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(preAlice == XRP(1000)); + BEAST_EXPECT(preAliceUSD == USD(10000)); + BEAST_EXPECT(preBob == XRP(1000)); + BEAST_EXPECT(preBobUSD == USD(0)); + + // remit + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // validate + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - feeDrops - feeReserve); + BEAST_EXPECT(postBob == preBob + feeReserve); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + } + + // REMIT: XAH + USD + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // setup + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(preAlice == XRP(1000)); + BEAST_EXPECT(preAliceUSD == USD(10000)); + BEAST_EXPECT(preBob == XRP(1000)); + BEAST_EXPECT(preBobUSD == USD(0)); + + // remit + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // validate + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - feeReserve); + BEAST_EXPECT(postBob == preBob + XRP(1) + feeReserve); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + } + + // REMIT: USD + URITOKEN XFER + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // mint uri token + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + remit::token_ids({ strHex(tid) }), + ter(tesSUCCESS)); + env.close(); + + // verify uri transfer + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + + // verify xah & usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - feeDrops - (2 * feeReserve)); + BEAST_EXPECT(postBob == preBob + (2 * feeReserve)); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH/USD + URITOKEN XFER + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // mint uri token + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + remit::token_ids({ strHex(tid) }), + ter(tesSUCCESS)); + env.close(); + + // verify uri transfer + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid)); + BEAST_EXPECT(tokenOwner(*env.current(), tid) == bob.id()); + + // verify xah & usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - (2 * feeReserve)); + BEAST_EXPECT(postBob == preBob + XRP(1) + (2 * feeReserve)); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: USD + URITOKEN XFER + URITOKEN MINT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // mint uri token + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // remit xah/usd + uritoken id + uritoken mint + std::string const uri2(maxTokenURILength - 1, '?'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + remit::token_ids({ strHex(tid1) }), + remit::uri(uri2), + ter(tesSUCCESS)); + env.close(); + + // verify uri mint + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid2)); + BEAST_EXPECT(tokenOwner(*env.current(), tid2) == bob.id()); + + // verify uri transfer + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid1) == alice.id()); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 3); + + // verify xah + auto const postAlice = env.balance(alice); + auto const postBob = env.balance(bob); + BEAST_EXPECT(postAlice == preAlice - feeDrops - (feeReserve * 3)); + BEAST_EXPECT(postBob == preBob + (feeReserve * 3)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + + // REMIT: XAH/USD + URITOKEN XFER + URITOKEN MINT + { + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // mint uri token + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // remit xah/usd + uritoken id + uritoken mint + std::string const uri2(maxTokenURILength - 1, '?'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + remit::token_ids({ strHex(tid1) }), + remit::uri(uri2), + ter(tesSUCCESS)); + env.close(); + + // verify uri mint + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid2)); + BEAST_EXPECT(tokenOwner(*env.current(), tid2) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid2) == alice.id()); + + // verify uri transfer + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + BEAST_EXPECT(tokenIsser(*env.current(), tid1) == alice.id()); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 3); + + // verify xah & usd + auto const postAlice = env.balance(alice); + auto const postAliceUSD = env.balance(alice, USD.issue()); + auto const postBob = env.balance(bob); + auto const postBobUSD = env.balance(bob, USD.issue()); + BEAST_EXPECT(postAlice == preAlice - XRP(1) - feeDrops - (feeReserve * 3)); + BEAST_EXPECT(postBob == preBob + XRP(1) + (feeReserve * 3)); + BEAST_EXPECT(postAliceUSD == preAliceUSD - USD(1)); + BEAST_EXPECT(postBobUSD == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + } + + void + testDestDoesNotExists(FeatureBitset features) + { + testcase("dest does not exist"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + // Env env{*this, envconfig(), amend, nullptr, + // // beast::severities::kWarning + // beast::severities::kTrace + // }; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // REMIT No Amounts No URI Tokens + env(remit::remit(alice, bob), ter(tesSUCCESS)); + env.close(); + + // REMIT XAH + env(remit::remit(alice, bob), remit::amts({ XRP(1) }), ter(tesSUCCESS)); + env.close(); + + // // REMIT XAH + USD + // env(remit::remit(alice, bob), remit::amts({ XRP(1), USD(1) }), txResult); + // env.close(); + + // // MINT + // std::string const uri(maxTokenURILength, '?'); + // std::string const tid{strHex(uritoken::tokenid(alice, uri))}; + // env(uritoken::mint(alice, uri), txResult); + // env.close(); + + // // REMIT URI XFER + // env(remit::remit(alice, bob), remit::token_ids({ tid }), txResult); + // env.close(); + + // // REMIT 2 amount XAH + // env(remit::remit(alice, bob), txResult); + // env.close(); + } + + void + testTLDoesNotExists(FeatureBitset features) + { + testcase("trust line does not exist"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + Env env{*this, features}; + // Env env{*this, envconfig(), amend, nullptr, + // // beast::severities::kWarning + // beast::severities::kTrace + // }; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + // REMIT No Amounts No URI Tokens + env(remit::remit(alice, bob), ter(tesSUCCESS)); + env.close(); + + // REMIT XAH + env(remit::remit(alice, bob), remit::amts({ XRP(1) }), ter(tesSUCCESS)); + env.close(); + + // // REMIT XAH + USD + // env(remit::remit(alice, bob), remit::amts({ XRP(1), USD(1) }), txResult); + // env.close(); + + // // MINT + // std::string const uri(maxTokenURILength, '?'); + // std::string const tid{strHex(uritoken::tokenid(alice, uri))}; + // env(uritoken::mint(alice, uri), txResult); + // env.close(); + + // // REMIT URI XFER + // env(remit::remit(alice, bob), remit::token_ids({ tid }), txResult); + // env.close(); + + // // REMIT 2 amount XAH + // env(remit::remit(alice, bob), txResult); + // env.close(); + } + + void + testWithFeats(FeatureBitset features) + { + testEnabled(features); + testDestExistsTLExists(features); + testDestExistsTLDNE(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Remit, app, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/consensus/UNLReport_test.cpp b/src/test/consensus/UNLReport_test.cpp index e2a3021bd..5be39a236 100644 --- a/src/test/consensus/UNLReport_test.cpp +++ b/src/test/consensus/UNLReport_test.cpp @@ -60,7 +60,7 @@ namespace test { // * @param pass if the Tx should be applied successfully // * @return true if meet the expectation of apply result // */ -bool +inline bool applyAndTestUNLRResult( jtx::Env& env, OpenView& view, @@ -1324,4 +1324,4 @@ createUNLRTx( } } // namespace test -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/test/jtx.h b/src/test/jtx.h index 39d4a9662..0fcc62239 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/impl/acctdelete.cpp b/src/test/jtx/impl/acctdelete.cpp index f7d9aa9b6..ea104e22b 100644 --- a/src/test/jtx/impl/acctdelete.cpp +++ b/src/test/jtx/impl/acctdelete.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include diff --git a/src/test/jtx/impl/remit.cpp b/src/test/jtx/impl/remit.cpp new file mode 100644 index 000000000..8ae158eff --- /dev/null +++ b/src/test/jtx/impl/remit.cpp @@ -0,0 +1,84 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 XRPL Labs + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { +namespace remit { + +Json::Value +remit(jtx::Account const& account, jtx::Account const& dest) +{ + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = jss::Remit; + jv[jss::Account] = account.human(); + jv[jss::Destination] = dest.human(); + return jv; +} + +void +amts::operator()(Env& env, JTx& jt) const +{ + auto& ja = jt.jv[sfAmounts.getJsonName()]; + for (std::size_t i = 0; i < amts_.size(); ++i) + { + ja[i][sfAmountEntry.jsonName] = Json::Value{}; + ja[i][sfAmountEntry.jsonName][jss::Amount] = + amts_[i].getJson(JsonOptions::none); + } + jt.jv[sfAmounts.jsonName] = ja; +} + +void +blob::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfBlob.jsonName] = blob_; +} + +void +inform::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfInform.jsonName] = inform_.human(); +} + +void +token_ids::operator()(Env& env, JTx& jt) const +{ + for (std::size_t i = 0; i < token_ids_.size(); ++i) + { + jt.jv[sfURITokenIDs.jsonName] = Json::arrayValue; + jt.jv[sfURITokenIDs.jsonName][i] = token_ids_[i]; + } +} + +void +uri::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfMintURIToken.jsonName] = Json::Value{}; + jt.jv[sfMintURIToken.jsonName][sfURI.jsonName] = strHex(uri_);; +} + +} // namespace remit +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/remit.h b/src/test/jtx/remit.h new file mode 100644 index 000000000..4ad4bafbf --- /dev/null +++ b/src/test/jtx/remit.h @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 XRPL Labs + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_REMIT_H_INCLUDED +#define RIPPLE_TEST_JTX_REMIT_H_INCLUDED + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace remit { + +Json::Value +remit(jtx::Account const& account, jtx::Account const& dest); + +/** Sets the optional Amount on a JTx. */ +class amts +{ +private: + std::vector amts_; + +public: + explicit amts(std::vector const& amts) : amts_(amts) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Set the optional "Blob" on a JTx */ +class blob +{ +private: + std::string blob_; + +public: + explicit blob(std::string const& blob) : blob_(blob) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Sets the optional "Inform" on a JTx. */ +class inform +{ +private: + jtx::Account inform_; + +public: + explicit inform(jtx::Account const& inform) : inform_(inform) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Sets the optional "URITokenIDs" on a JTx. */ +class token_ids +{ +private: + std::vector token_ids_; + +public: + explicit token_ids(std::vector const& token_ids) : token_ids_(token_ids) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Set the optional "sfMintURIToken" on a JTx */ +class uri +{ +private: + std::string uri_; + +public: + explicit uri(std::string const& uri) : uri_(uri) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +} // namespace remit + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif // RIPPLE_TEST_JTX_REMIT_H_INCLUDED