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/hook/tts.h b/hook/tts.h index 304685c31..0973fa189 100644 --- a/hook/tts.h +++ b/hook/tts.h @@ -31,6 +31,7 @@ #define ttURITOKEN_BUY 47 #define ttURITOKEN_CREATE_SELL_OFFER 48 #define ttURITOKEN_CANCEL_SELL_OFFER 49 +#define ttREMIT 95 #define ttGENESIS_MINT 96 #define ttIMPORT 97 #define ttCLAIM_REWARD 98 diff --git a/release-builder.sh b/release-builder.sh index 53c683f4d..f4d14a78a 100755 --- a/release-builder.sh +++ b/release-builder.sh @@ -12,10 +12,13 @@ if [[ "$GITHUB_REPOSITORY" == "" ]]; then BUILD_CORES=8 fi +CONTAINER_NAME=xahaud_cached_builder_$(echo "$GITHUB_ACTOR" | awk '{print tolower($0)}') + echo "-- BUILD CORES: $BUILD_CORES" echo "-- GITHUB_REPOSITORY: $GITHUB_REPOSITORY" echo "-- GITHUB_SHA: $GITHUB_SHA" echo "-- GITHUB_RUN_NUMBER: $GITHUB_RUN_NUMBER" +echo "-- CONTAINER_NAME: $CONTAINER_NAME" which docker 2> /dev/null 2> /dev/null if [ "$?" -eq "1" ] @@ -31,13 +34,13 @@ then exit 1 fi -STATIC_CONTAINER=$(docker ps -a | grep xahaud_cached_builder |wc -l) +STATIC_CONTAINER=$(docker ps -a | grep $CONTAINER_NAME |wc -l) if [[ "$STATIC_CONTAINER" -gt "0" && "$GITHUB_REPOSITORY" != "" ]]; then echo "Static container, execute in static container to have max. cache" - docker start xahaud_cached_builder - docker exec -i xahaud_cached_builder /hbb_exe/activate-exec bash -x /io/build-core.sh "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$BUILD_CORES" "$GITHUB_RUN_NUMBER" - docker stop xahaud_cached_builder + docker start $CONTAINER_NAME + docker exec -i $CONTAINER_NAME /hbb_exe/activate-exec bash -x /io/build-core.sh "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$BUILD_CORES" "$GITHUB_RUN_NUMBER" + docker stop $CONTAINER_NAME else echo "No static container, build on temp container" rm -rf release-build; @@ -50,10 +53,10 @@ else else # GH Action, runner echo "GH Action, runner, clean & re-create create persistent container" - docker rm -f xahaud_cached_builder - docker run -di --user 0:$(id -g) --name xahaud_cached_builder -v /data/builds:/data/builds -v `pwd`:/io --network host ghcr.io/foobarwidget/holy-build-box-x64 /hbb_exe/activate-exec bash - docker exec -i xahaud_cached_builder /hbb_exe/activate-exec bash -x /io/build-full.sh "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$BUILD_CORES" "$GITHUB_RUN_NUMBER" - docker stop xahaud_cached_builder + docker rm -f $CONTAINER_NAME + docker run -di --user 0:$(id -g) --name $CONTAINER_NAME -v /data/builds:/data/builds -v `pwd`:/io --network host ghcr.io/foobarwidget/holy-build-box-x64 /hbb_exe/activate-exec bash + docker exec -i $CONTAINER_NAME /hbb_exe/activate-exec bash -x /io/build-full.sh "$GITHUB_REPOSITORY" "$GITHUB_SHA" "$BUILD_CORES" "$GITHUB_RUN_NUMBER" + docker stop $CONTAINER_NAME fi fi diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index 41a56a758..99a330764 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -70,6 +70,45 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv) switch (tt) { + case ttREMIT: { + if (destAcc) + ADD_TSH(*destAcc, tshSTRONG); + + if (tx.isFieldPresent(sfInform)) + { + auto const inform = tx.getAccountID(sfInform); + if (*otxnAcc != inform && *destAcc != inform) + ADD_TSH(inform, tshWEAK); + } + + if (tx.isFieldPresent(sfURITokenIDs)) + { + STVector256 tokenIds = tx.getFieldV256(sfURITokenIDs); + for (uint256 const klRaw : tokenIds) + { + Keylet const id{ltURI_TOKEN, klRaw}; + if (!rv.exists(id)) + continue; + + auto const ut = rv.read(id); + if (!ut || + ut->getFieldU16(sfLedgerEntryType) != ltURI_TOKEN) + continue; + + auto const owner = ut->getAccountID(sfOwner); + auto const issuer = ut->getAccountID(sfIssuer); + if (issuer != owner && issuer != *destAcc) + { + ADD_TSH( + issuer, + (ut->getFlags() & lsfBurnable) ? tshSTRONG + : tshWEAK); + } + } + } + break; + } + case ttIMPORT: { if (tx.isFieldPresent(sfIssuer)) ADD_TSH(tx.getAccountID(sfIssuer), fixV2 ? tshWEAK : tshSTRONG); @@ -256,14 +295,14 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv) { ADD_TSH(bo->getAccountID(sfOwner), tshSTRONG); if (bo->isFieldPresent(sfDestination)) - ADD_TSH(bo->getAccountID(sfDestination), tshWEAK); + ADD_TSH(bo->getAccountID(sfDestination), tshSTRONG); } if (so) { ADD_TSH(so->getAccountID(sfOwner), tshSTRONG); if (so->isFieldPresent(sfDestination)) - ADD_TSH(so->getAccountID(sfDestination), tshWEAK); + ADD_TSH(so->getAccountID(sfDestination), tshSTRONG); } break; @@ -279,7 +318,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv) auto const offer = getNFTOffer(offerID, rv); if (offer) { - ADD_TSH(offer->getAccountID(sfOwner), tshSTRONG); + ADD_TSH(offer->getAccountID(sfOwner), tshWEAK); if (offer->isFieldPresent(sfDestination)) ADD_TSH(offer->getAccountID(sfDestination), tshWEAK); 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..e8c527df3 --- /dev/null +++ b/src/ripple/app/tx/impl/Remit.cpp @@ -0,0 +1,629 @@ +//------------------------------------------------------------------------------ +/* + 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) +{ + XRPAmount native = ([&ctx]() -> XRPAmount { + if (!ctx.tx.isFieldPresent(sfAmounts)) + return beast::zero; + + STArray const& sEntries(ctx.tx.getFieldArray(sfAmounts)); + for (STObject const& sEntry : sEntries) + { + if (!sEntry.isFieldPresent(sfAmount)) + continue; + + STAmount const amount = sEntry.getFieldAmount(sfAmount); + if (isXRP(amount)) + return amount.xrp(); + } + return beast::zero; + })(); + return TxConsequences{ctx.tx, native}; +} + +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; + } + + if (ctx.tx.isFieldPresent(sfInform)) + { + AccountID const infID = ctx.tx.getAccountID(sfInform); + if (infID == dstID || srcID == infID) + { + JLOG(ctx.j.warn()) << "Malformed transaction: sfInform is same as " + "source or destination."; + return temMALFORMED; + } + } + + if (ctx.tx.isFieldPresent(sfBlob) && + ctx.tx.getFieldVL(sfBlob).size() > (128 * 1024)) + { + JLOG(ctx.j.warn()) << "Blob was more than 128kib " + << ctx.tx.getTransactionID(); + return temMALFORMED; + } + + // sanity check amounts + if (ctx.tx.isFieldPresent(sfAmounts)) + { + if (ctx.tx.getFieldArray(sfAmounts).size() > 32) + { + JLOG(ctx.j.warn()) << "Malformed: AmountEntry count exceeds `32`."; + return temMALFORMED; + } + + 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(); + + for (auto const& mintElement : mint) + { + auto const& name = mintElement.getFName(); + if (name != sfURI && name != sfFlags && name != sfDigest) + { + JLOG(ctx.j.trace()) << "Malformed transaction: sfMintURIToken " + "contains invalid field."; + return temMALFORMED; + } + } + if (!mint.isFieldPresent(sfURI)) + { + JLOG(ctx.j.warn()) + << "Malformed transaction: URI was not provided."; + return temMALFORMED; + } + 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(uri)) + { + JLOG(ctx.j.warn()) + << "Malformed transaction: Invalid UTF8 inside MintURIToken."; + return temMALFORMED; + } + + if (mint.isFieldPresent(sfFlags)) + { + if (mint.getFieldU32(sfFlags) & tfURITokenMintMask) + return temINVALID_FLAG; + } + } + + // sanity check uritokenids + if (ctx.tx.isFieldPresent(sfURITokenIDs)) + { + STVector256 ids = ctx.tx.getFieldV256(sfURITokenIDs); + if (ids.size() < 1 || ids.size() > 32) + { + JLOG(ctx.j.warn()) << "Malformed transaction: URITokenIDs Invalid."; + return temMALFORMED; + } + + 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() +{ + Sandbox sb(&ctx_.view()); + + if (!sb.rules().enabled(featureRemit)) + return temDISABLED; + + beast::Journal const& j = ctx_.journal; + + auto const srcAccID = ctx_.tx[sfAccount]; + + auto sleSrcAcc = sb.peek(keylet::account(srcAccID)); + if (!sleSrcAcc) + return terNO_ACCOUNT; + + if (ctx_.tx.isFieldPresent(sfInform)) + { + auto const informAcc = ctx_.tx.getAccountID(sfInform); + if (!sb.exists(keylet::account(informAcc))) + { + JLOG(j.warn()) << "Remit: sfInform account does not exist."; + return tecNO_TARGET; + } + } + + XRPAmount const accountReserve{sb.fees().accountReserve(0)}; + XRPAmount const objectReserve{sb.fees().accountReserve(1) - accountReserve}; + + // sanity check + if (accountReserve < beast::zero || objectReserve < beast::zero || + objectReserve > sb.fees().accountReserve(1)) + { + JLOG(j.warn()) + << "Remit: account or object reserve calculation not sane."; + return tecINTERNAL; + } + + // 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; + + // Check if the destination account requires deposit authorization. + bool const depositAuth{sb.rules().enabled(featureDepositAuth)}; + if (depositAuth && sleDstAcc && (flags & lsfDepositAuth)) + { + if (!sb.exists(keylet::depositPreauth(dstAccID, srcAccID))) + 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 + if (nativeRemit + accountReserve < nativeRemit) + return tecINTERNAL; + + 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)) + { + if (nativeRemit + objectReserve < nativeRemit) + return tecINTERNAL; + + 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 = 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; + + 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 tecNO_ENTRY; + } + + // 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 + if (nativeRemit + objectReserve < nativeRemit) + return tecINTERNAL; + + 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. + + // check for overflow + if (nativeRemit + amount.xrp() < nativeRemit) + return tecINTERNAL; + + 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; + + // sanity check this calculation + if (srcAmt < amount || srcAmt > amount + amount) + { + JLOG(j.warn()) << "Remit: srcAmt calculation not sane."; + return tecINTERNAL; + } + + 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(dstAccID, issuerAccID, amount.getCurrency()))) + { + if (nativeRemit + objectReserve < nativeRemit) + return tecINTERNAL; + + nativeRemit += objectReserve; + } + + // action the transfer + if (TER result = + accountSend(sb, srcAccID, dstAccID, amount, j, true); + result != tesSUCCESS) + return result; + } + } + + 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); + } + } + + auto hasSufficientReserve = [&](std::shared_ptr const& sle) -> bool { + std::uint32_t const uOwnerCount = sle->getFieldU32(sfOwnerCount); + return sle->getFieldAmount(sfBalance) >= + sb.fees().accountReserve(uOwnerCount); + }; + + // sanity check reserves + if (!hasSufficientReserve(sleSrcAcc)) + { + JLOG(j.warn()) << "Remit: sender " << srcAccID + << " lacks reserves to cover send."; + return tecINSUFFICIENT_RESERVE; + } + + // this isn't actually an error but we will print a warning + // this can occur if the destination was already below reserve level at the + // time assets were sent + if (!hasSufficientReserve(sleDstAcc)) + { + JLOG(j.warn()) << "Remit: destination has insufficient reserves."; + } + + // 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())}; + + 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/protocol/Feature.h b/src/ripple/protocol/Feature.h index 21886c4d6..d6e3c3eea 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 = 67; +static constexpr std::size_t numFeatures = 68; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -355,6 +355,7 @@ extern uint256 const featureXahauGenesis; extern uint256 const featureHooksUpdate1; extern uint256 const fixXahauV1; extern uint256 const fixXahauV2; +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 19aa0fca7..565aa0eab 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -461,6 +461,7 @@ REGISTER_FEATURE(XahauGenesis, Supported::yes, VoteBehavior::De REGISTER_FEATURE(HooksUpdate1, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixXahauV1, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixXahauV2, 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 aee747c2e..e52d6ff8f 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -143,6 +143,20 @@ InnerObjectFormats::InnerObjectFormats() {sfPublicKey, soeREQUIRED}, {sfAccount, soeOPTIONAL}, }); + + add(sfAmountEntry.jsonName.c_str(), + sfAmountEntry.getCode(), + { + {sfAmount, soeREQUIRED}, + }); + + 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 2847af8ad..31cf8385b 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/AccountInfo.cpp b/src/ripple/rpc/handlers/AccountInfo.cpp index ef130ef18..524777e38 100644 --- a/src/ripple/rpc/handlers/AccountInfo.cpp +++ b/src/ripple/rpc/handlers/AccountInfo.cpp @@ -91,13 +91,14 @@ doAccountInfo(RPC::JsonContext& context) {"requireDestinationTag", lsfRequireDestTag}}}; static constexpr std:: - array, 4> + array, 5> disallowIncomingFlags{ {{"disallowIncomingNFTokenOffer", lsfDisallowIncomingNFTokenOffer}, {"disallowIncomingCheck", lsfDisallowIncomingCheck}, {"disallowIncomingPayChan", lsfDisallowIncomingPayChan}, - {"disallowIncomingTrustline", lsfDisallowIncomingTrustline}}}; + {"disallowIncomingTrustline", lsfDisallowIncomingTrustline}, + {"disallowIncomingRemit", lsfDisallowIncomingRemit}}}; auto const sleAccepted = ledger->read(keylet::account(accountID)); if (sleAccepted) diff --git a/src/test/app/Remit_test.cpp b/src/test/app/Remit_test.cpp new file mode 100644 index 000000000..d71b4091e --- /dev/null +++ b/src/test/app/Remit_test.cpp @@ -0,0 +1,2838 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 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 + +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); + } + + static STAmount + lineBalance( + jtx::Env const& env, + jtx::Account const& account, + jtx::Account const& gw, + jtx::IOU const& iou) + { + auto const sle = env.le(keylet::line(account, gw, iou.currency)); + if (sle && sle->isFieldPresent(sfBalance)) + return (*sle)[sfBalance]; + return STAmount(iou, 0); + } + + static bool + validateSequence( + jtx::Env const& env, + jtx::Account const& account, + std::uint32_t const& sequence) + { + auto const sle = env.le(keylet::account(account)); + if (sle && sle->isFieldPresent(sfSequence)) + return (*sle)[sfSequence] == sequence; + return false; + } + + static std::pair> + uriTokenKeyAndSle( + ReadView const& view, + jtx::Account const& account, + std::string const& uri) + { + auto const k = keylet::uritoken(account, Blob(uri.begin(), uri.end())); + return {k.key, view.read(k)}; + } + + void + testEnabled(FeatureBitset features) + { + 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 + + testPreflightInvalid(FeatureBitset features) + { + testcase("preflight invalid"); + 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.fund(XRP(1000), alice, bob, gw); + env.close(); + + //---------------------------------------------------------------------- + // preflight + + // temINVALID_FLAG - Invalid flags set. + { + env(remit::remit(alice, bob), + txflags(tfBurnable), + ter(temINVALID_FLAG)); + env.close(); + } + + // temREDUNDANT - Remit to self. + { + env(remit::remit(alice, alice), ter(temREDUNDANT)); + env.close(); + } + + // temMALFORMED - blob was more than 128kib + { + ripple::Blob blob; + blob.resize(129 * 1024); + env(remit::remit(alice, bob), + remit::blob(strHex(blob)), + ter(temMALFORMED)); + } + + // temMALFORMED - AmountEntrys Exceeds Limit + { + std::vector amts_; // Remove the const qualifier + for (size_t i = 0; i < 33; i++) + { + auto const USD = gw["USD"]; + amts_.emplace_back(USD(1)); + } + + env(remit::remit(alice, bob), + remit::amts(amts_), + ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - Expected AmountEntry. + { + auto tx = remit::remit(alice, bob); + std::vector const amts_ = {XRP(1)}; + auto& ja = tx[sfAmounts.getJsonName()]; + for (std::size_t i = 0; i < amts_.size(); ++i) + { + ja[i][sfGenesisMint.jsonName] = Json::Value{}; + ja[i][sfGenesisMint.jsonName][jss::Amount] = + amts_[i].getJson(JsonOptions::none); + ja[i][sfGenesisMint.jsonName][jss::Destination] = bob.human(); + } + tx[sfAmounts.jsonName] = ja; + env(tx, ter(temMALFORMED)); + env.close(); + } + + // temBAD_AMOUNT - Bad amount + { + env(remit::remit(alice, bob), + remit::amts({XRP(-1)}), + ter(temBAD_AMOUNT)); + env.close(); + } + + // temBAD_CURRENCY - Bad currency. + { + auto const bXAH = gw["XaH"]; + env(remit::remit(alice, bob), + remit::amts({bXAH(100)}), + ter(temBAD_CURRENCY)); + env.close(); + } + + // temMALFORMED - Native Currency appears more than once + { + env(remit::remit(alice, bob), + remit::amts({XRP(1), XRP(1)}), + ter(temMALFORMED)); + env.close(); + + // not adjacent + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1), XRP(1)}), + ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - Issued Currency appears more than once. + { + env(remit::remit(alice, bob), + remit::amts({USD(1), USD(1)}), + ter(temMALFORMED)); + env.close(); + + // not adjacent + env(remit::remit(alice, bob), + remit::amts({USD(1), XRP(1), USD(1)}), + ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - URI field was not provided. + // DA: Template/Submit Failure + // { + // auto tx = remit::remit(alice, bob); + // tx[sfMintURIToken.jsonName] = Json::Value{}; + // tx[sfMintURIToken.jsonName][sfFlags.fieldName] = 1; + // env(tx, ter(temMALFORMED)); + // env.close(); + // } + + // temMALFORMED - Field found in disallowed location. + // DA: Template/Submit Failure + // { + // std::string const uri(0, '?'); + // auto tx = remit::remit(alice, bob); + // tx[sfMintURIToken.jsonName] = Json::Value{}; + // std::string const digestval = + // "C16E7263F07AA41261DCC955660AF4646ADBA414E37B6F5A5BA50F75153F5CCC"; + // tx[sfMintURIToken.jsonName][sfURI.fieldName] = strHex(uri); + // tx[sfMintURIToken.jsonName][sfHookOn.fieldName] = digestval; + // env(tx, ter(temMALFORMED)); + // env.close(); + // } + + // temMALFORMED - URI was too short/long. (short) + { + std::string const uri(0, '?'); + env(remit::remit(alice, bob), remit::uri(uri), ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - URI was too short/long. (long) + { + std::string const uri(maxTokenURILength + 1, '?'); + env(remit::remit(alice, bob), remit::uri(uri), ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - Invalid UTF8 inside MintURIToken. + + // temINVALID_FLAG - Invalid URI Mint Flag + { + std::string const uri(maxTokenURILength, '?'); + env(remit::remit(alice, bob), + remit::uri(uri, tfAllowXRP), + ter(temINVALID_FLAG)); + env.close(); + } + + // temMALFORMED - URITokenIDs < 1 + { + std::vector token_ids; + env(remit::remit(alice, bob), + remit::token_ids(token_ids), + ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - URITokenIDs Exceeds Limit + { + std::vector token_ids; + for (size_t i = 0; i < 33; i++) + { + std::string const uri(i, '?'); + auto const tid = uritoken::tokenid(alice, uri); + token_ids.emplace_back(strHex(tid)); + } + + env(remit::remit(alice, bob), + remit::token_ids(token_ids), + ter(temMALFORMED)); + env.close(); + } + + // temMALFORMED - Duplicate URITokenID. + { + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid1), strHex(tid1)}), + ter(temMALFORMED)); + env.close(); + + // not adj + std::string const uri2(maxTokenURILength, 'a'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid1), strHex(tid2), strHex(tid1)}), + ter(temMALFORMED)); + env.close(); + } + } + + void + testDoApplyInvalid(FeatureBitset features) + { + testcase("doApply invalid"); + 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"]; + + //---------------------------------------------------------------------- + // doApply + + // terNO_ACCOUNT - account doesnt exist + { + auto const carol = Account("carol"); + Env env{*this, features}; + env.memoize(carol); + auto tx = remit::remit(carol, bob); + tx[jss::Sequence] = 0; + env(tx, carol, ter(terNO_ACCOUNT)); + env.close(); + } + + // tecNO_TARGET - inform acct doesnt exist + { + auto const carol = Account("carol"); + Env env{*this, features}; + env.fund(XRP(1000), alice, bob); + env.close(); + + env.memoize(carol); + auto tx = remit::remit(alice, bob); + tx[sfInform.jsonName] = carol.human(); + env(tx, alice, ter(tecNO_TARGET)); + env.close(); + } + + // tecNO_PERMISSION - lsfDisallowIncomingRemit + // DA: see testAllowIncoming + + // tecDST_TAG_NEEDED + // DA: see testDstTag + + // tecDUPLICATE + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob); + env.close(); + + std::string const uri(maxTokenURILength, '?'); + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + env(remit::remit(alice, bob), remit::uri(uri), ter(tecDUPLICATE)); + env.close(); + } + + // tecDIR_FULL - account directory full + // DA: impossible test + + // tecNO_ENTRY + { + Env env{*this, features}; + 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::token_ids({strHex(tid)}), + ter(tecNO_ENTRY)); + env.close(); + } + + // tecINTERNAL - not actually a uritoken. + // DA: impossible test + + // tecNO_PERMISSION - not owned by sender. + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob); + env.close(); + + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(bob, uri); + env(uritoken::mint(bob, uri), ter(tesSUCCESS)); + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid)}), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tefBAD_LEDGER - invalid owner directory + { + + } + + // tecDIR_FULL - destination directory full + // DA: impossible test + + // tecUNFUNDED_PAYMENT + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env.close(); + + env(remit::remit(alice, bob), + remit::amts({USD(1001)}), + ter(tecUNFUNDED_PAYMENT)); + env.close(); + } + + // tecINTERNAL - negative XRP + // DA: impossible test + + // tecUNFUNDED_PAYMENT + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob); + env.close(); + + env(remit::remit(alice, bob), + remit::amts({XRP(1001)}), + ter(tecUNFUNDED_PAYMENT)); + env.close(); + } + + // tecINTERNAL + // DA: impossible test + + // tecINTERNAL + // DA: impossible test + + // tecINSUFFICIENT_RESERVE + { + Env env{*this, features}; + env.fund(XRP(250), alice, bob); + env.close(); + + std::string const uri(maxTokenURILength, '?'); + env(remit::remit(alice, bob), + remit::uri(uri), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + + void + testDisallowXRP(FeatureBitset features) + { + // auth amount defaults to balance if not present + testcase("disallow xrp"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + { + // Make a remit where dst disallows XRP + Env env(*this, features - featureDepositAuth); + env.fund(XRP(10000), alice, bob); + env(fset(bob, asfDisallowXRP)); + env.close(); + + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + ter(tesSUCCESS)); + } + { + // Make a remit where dst disallows XRP. Ignore that flag, + // since it's just advisory. + Env env{*this, features}; + env.fund(XRP(10000), alice, bob); + env(fset(bob, asfDisallowXRP)); + env.close(); + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + ter(tesSUCCESS)); + } + } + + void + testDstTag(FeatureBitset features) + { + // auth amount defaults to balance if not present + testcase("dest tag"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env(fset(bob, asfRequireDest)); + env.close(); + { + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + ter(tecDST_TAG_NEEDED)); + } + { + env(remit::remit(alice, bob, 1), + remit::amts({XRP(1)}), + ter(tesSUCCESS)); + } + } + + void + testDisallowIncoming(FeatureBitset features) + { + testcase("disallow incoming"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + { + Env env{*this, features - featureRemit}; + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env(fset(alice, asfDisallowIncomingRemit)); + env.close(); + auto const sle = env.le(alice); + uint32_t flags = sle->getFlags(); + BEAST_EXPECT(!(flags & lsfDisallowIncomingRemit)); + } + + // setup env + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // set flag on bob only + env(fset(bob, asfDisallowIncomingRemit)); + env.close(); + + // remit from alice to bob is not allowed + { + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + ter(tecNO_PERMISSION)); + } + + // set flag on alice also + env(fset(alice, asfDisallowIncomingRemit)); + env.close(); + + // remit from bob to alice is not allowed + { + env(remit::remit(bob, alice), + remit::amts({XRP(1)}), + ter(tecNO_PERMISSION)); + } + + // remove flag from bob + env(fclear(bob, asfDisallowIncomingRemit)); + env.close(); + + // now remit between alice and bob allowed + { + env(remit::remit(alice, bob), + remit::amts({XRP(1)}), + ter(tesSUCCESS)); + } + + // a remit from carol to alice is not allowed + { + env(remit::remit(carol, alice), + remit::amts({XRP(1)}), + ter(tecNO_PERMISSION)); + } + + // remove flag from alice + env(fclear(alice, asfDisallowIncomingRemit)); + env.close(); + + // now a remit from carol to alice is allowed + { + env(remit::remit(carol, alice), + remit::amts({XRP(1)}), + ter(tesSUCCESS)); + } + } + + void + testDestExistsTLExists(FeatureBitset features) + { + testcase("dest exists and trustline exists"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // REMIT + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + 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), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == preAlice - feeDrops); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // REMIT: XAH + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + 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(); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - feeDrops); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // REMIT: USD + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT(env.balance(alice) == preAlice - feeDrops); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + } + + // REMIT: XAH + USD + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - feeDrops); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + } + + // REMIT: URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + 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)); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // 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()); + + BEAST_EXPECT( + env.balance(alice) == preAlice - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + feeReserve); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: URITOKEN MINT + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(1000), alice, bob); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + 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()); + + BEAST_EXPECT( + env.balance(alice) == preAlice - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + feeReserve); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH + URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1) + feeReserve); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: USD + URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT( + env.balance(alice) == preAlice - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + feeReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH/USD + URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1) + feeReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH + URITOKEN XFER + URITOKEN MINT + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - (feeReserve * 2)); + BEAST_EXPECT( + env.balance(bob) == 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 + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT( + env.balance(alice) == preAlice - feeDrops - (feeReserve * 2)); + BEAST_EXPECT(env.balance(bob) == preBob + (feeReserve * 2)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == 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 + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - (feeReserve * 2)); + BEAST_EXPECT( + env.balance(bob) == preBob + XRP(1) + (feeReserve * 2)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + } + + void + testDestExistsTLNotExist(FeatureBitset features) + { + testcase("dest exists and trustline does not exist"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // REMIT: USD + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT( + env.balance(alice) == preAlice - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + feeReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + } + + // REMIT: XAH + USD + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const feeReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - feeReserve); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1) + feeReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + } + } + + void + testDestNotExistTLNotExist(FeatureBitset features) + { + testcase("dest does not exist and trustline does not exist"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // REMIT + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.memoize(bob); + + env.fund(XRP(1000), alice); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + env(remit::remit(alice, bob), ter(tesSUCCESS)); + env.close(); + + bool const withXahau = + env.current()->rules().enabled(featureXahauGenesis); + BEAST_EXPECT(validateSequence( + env, bob, withXahau ? 10 : env.closed()->info().seq)); + + BEAST_EXPECT( + env.balance(alice) == preAlice - feeDrops - accountReserve); + BEAST_EXPECT(env.balance(bob) == preBob + accountReserve); + } + + // REMIT: XAH + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.memoize(bob); + + env.fund(XRP(1000), alice); + 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(); + + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - accountReserve); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1) + accountReserve); + } + + // REMIT: USD + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.memoize(bob); + + env.fund(XRP(1000), alice, 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()); + + // remit + env(remit::remit(alice, bob), + remit::amts({USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // validate + BEAST_EXPECT( + env.balance(alice) == + preAlice - feeDrops - accountReserve - increment); + BEAST_EXPECT( + env.balance(bob) == preBob + accountReserve + increment); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + } + + // REMIT: XAH + USD + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.memoize(bob); + + env.fund(XRP(1000), alice, 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()); + + // remit + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // validate + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - accountReserve - increment); + BEAST_EXPECT( + env.balance(bob) == + preBob + XRP(1) + accountReserve + increment); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + } + + // REMIT: URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.memoize(bob); + + env.fund(XRP(1000), alice); + 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)); + + // setup + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // remit with uritoken id + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid)}), + ter(tesSUCCESS)); + env.close(); + + // validate + BEAST_EXPECT( + env.balance(alice) == + preAlice - feeDrops - accountReserve - increment); + BEAST_EXPECT( + env.balance(bob) == preBob + accountReserve + increment); + + 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 + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.memoize(bob); + + env.fund(XRP(1000), alice); + env.close(); + + // setup + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + 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( + env.balance(alice) == + preAlice - feeDrops - accountReserve - increment); + BEAST_EXPECT( + env.balance(bob) == preBob + accountReserve + increment); + + 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 + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.memoize(bob); + + env.fund(XRP(1000), alice); + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - accountReserve - increment); + BEAST_EXPECT( + env.balance(bob) == + preBob + XRP(1) + accountReserve + increment); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: USD + URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.memoize(bob); + + env.fund(XRP(1000), alice, 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 usd + BEAST_EXPECT( + env.balance(alice) == + preAlice - feeDrops - (increment * 2) - accountReserve); + BEAST_EXPECT( + env.balance(bob) == preBob + (increment * 2) + accountReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH/USD + URITOKEN XFER + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.memoize(bob); + + env.fund(XRP(1000), alice, 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - (increment * 2) - + accountReserve); + BEAST_EXPECT( + env.balance(bob) == + preBob + XRP(1) + (increment * 2) + accountReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // REMIT: XAH + URITOKEN XFER + URITOKEN MINT + { + // setup env + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.memoize(bob); + + env.fund(XRP(1000), alice); + 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - (increment * 2) - + accountReserve); + BEAST_EXPECT( + env.balance(bob) == + preBob + XRP(1) + (increment * 2) + accountReserve); + + // 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 + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.memoize(bob); + + env.fund(XRP(1000), alice, 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({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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - feeDrops - (increment * 3) - accountReserve); + BEAST_EXPECT( + env.balance(bob) == preBob + (increment * 3) + accountReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == 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 + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + auto const increment = env.current()->fees().increment; + auto const accountReserve = env.current()->fees().accountReserve(0); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.memoize(bob); + + env.fund(XRP(1000), alice, 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 + BEAST_EXPECT( + env.balance(alice) == + preAlice - XRP(1) - feeDrops - (increment * 3) - + accountReserve); + BEAST_EXPECT( + env.balance(bob) == + preBob + XRP(1) + (increment * 3) + accountReserve); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(1)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(1)); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + } + + void + testGateway(FeatureBitset features) + { + testcase("gateway"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + bool hasTrustline; + bool negative; + }; + + std::array gwSrcTests = {{ + // src > dst && src > issuer && dst no trustline + {Account("gw0"), Account{"alice2"}, false, true}, + // // src < dst && src < issuer && dst no trustline + {Account("gw1"), Account{"carol0"}, false, false}, + // // // // dst > src && dst > issuer && dst no trustline + {Account("gw0"), Account{"dan1"}, false, true}, + // // // // dst < src && dst < issuer && dst no trustline + {Account("gw1"), Account{"bob0"}, false, false}, + // // // src > dst && src > issuer && dst has trustline + {Account("gw0"), Account{"alice2"}, true, true}, + // // // src < dst && src < issuer && dst has trustline + {Account("gw1"), Account{"carol0"}, true, false}, + // // // dst > src && dst > issuer && dst has trustline + {Account("gw0"), Account{"dan1"}, true, true}, + // // // dst < src && dst < issuer && dst has trustline + {Account("gw1"), Account{"bob0"}, true, false}, + }}; + + for (auto const& t : gwSrcTests) + { + Env env{*this, features}; + auto const USD = t.src["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env.close(); + + if (t.hasTrustline) + { + env.trust(USD(100000), t.dst); + env.close(); + } + + if (t.hasTrustline) + { + env(pay(t.src, t.dst, USD(10000))); + env.close(); + } + + auto const preAmount = t.hasTrustline ? 10000 : 0; + auto const preDst = lineBalance(env, t.dst, t.src, USD); + + // issuer can remit + env(remit::remit(t.src, t.dst), + remit::amts({USD(100)}), + ter(tesSUCCESS)); + env.close(); + + auto const postAmount = t.hasTrustline ? 10100 : 100; + BEAST_EXPECT( + preDst == (t.negative ? -USD(preAmount) : USD(preAmount))); + BEAST_EXPECT( + lineBalance(env, t.dst, t.src, USD) == + (t.negative ? -USD(postAmount) : USD(postAmount))); + BEAST_EXPECT(lineBalance(env, t.src, t.src, USD) == USD(0)); + } + + std::array gwDstTests = {{ + // // // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, true, true}, + // // // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, false, true}, + // // // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, true, false}, + // // // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, false, false}, + // // // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true, true}, + // // // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, false, true}, + // // // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true, false}, + // // // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, false, false}, + }}; + + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.src, t.dst); + env.close(); + + if (t.hasTrustline) + { + env.trust(USD(100000), t.src); + env.close(); + } + + if (t.hasTrustline) + { + env(pay(t.dst, t.src, USD(10000))); + env.close(); + } + + auto const preSrc = lineBalance(env, t.src, t.dst, USD); + + // src can remit to issuer + env(remit::remit(t.src, t.dst), + remit::amts({USD(100)}), + t.hasTrustline ? ter(tesSUCCESS) : ter(tecUNFUNDED_PAYMENT)); + env.close(); + + if (t.hasTrustline) + { + auto const preAmount = 10000; + BEAST_EXPECT( + preSrc == (t.negative ? -USD(preAmount) : USD(preAmount))); + auto const postAmount = 9900; + BEAST_EXPECT( + lineBalance(env, t.src, t.dst, USD) == + (t.negative ? -USD(postAmount) : USD(postAmount))); + BEAST_EXPECT(lineBalance(env, t.dst, t.dst, USD) == USD(0)); + } + else + { + BEAST_EXPECT(preSrc == USD(0)); + BEAST_EXPECT(lineBalance(env, t.src, t.dst, USD) == USD(0)); + BEAST_EXPECT(lineBalance(env, t.dst, t.dst, USD) == USD(0)); + } + } + } + + void + testTransferRate(FeatureBitset features) + { + testcase("transfer rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + struct TestRateData + { + double rate; + STAmount delta; + std::string result; + TER code; + }; + std::array testCases = {{ + {0.0, USD(100), "900", tesSUCCESS}, + {-1.0, USD(100), "900", temBAD_TRANSFER_RATE}, + {0.9, USD(100), "900", temBAD_TRANSFER_RATE}, + {1.0, USD(100), "900", tesSUCCESS}, + {1.1, USD(100), "890", tesSUCCESS}, + {1.0005, USD(100), "899.95", tesSUCCESS}, + {1.005, USD(100), "899.5000001", tesSUCCESS}, + {1.25, USD(100), "875", tesSUCCESS}, + {2.0, USD(100), "800", tesSUCCESS}, + {2.1, USD(100), "900", temBAD_TRANSFER_RATE}, + }}; + + for (auto const& tc : testCases) + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, tc.rate), ter(tc.code)); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env.close(); + + auto const preBob = env.balance(bob, USD.issue()); + + auto const delta = USD(100); + env(remit::remit(alice, bob), remit::amts({delta})); + env.close(); + auto xferRate = transferRate(*env.current(), gw); + auto const postAlice = env.balance(alice, USD.issue()); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + BEAST_EXPECT(to_string(postAlice.value()) == tc.result); + } + + // test rate change + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.25)); + 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 delta = USD(100); + auto preAlice = env.balance(alice, USD.issue()); + auto preBob = env.balance(bob, USD.issue()); + + // remit + env(remit::remit(alice, bob), remit::amts({delta})); + env.close(); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - delta - USD(25)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + preAlice = env.balance(alice, USD.issue()); + preBob = env.balance(bob, USD.issue()); + + // remit + env(remit::remit(alice, bob), remit::amts({delta})); + env.close(); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + } + + // test issuer doesnt pay own rate + { + Env env{*this, features}; + env.fund(XRP(10000), alice, gw); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + + auto const delta = USD(100); + auto const preAlice = env.balance(alice, USD.issue()); + + // remit + env(remit::remit(gw, alice), remit::amts({delta})); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + delta); + } + } + + void + testRequireAuth(FeatureBitset features) + { + testcase("require auth"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + // test asfRequireAuth + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, gw); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10000)), txflags(tfSetfAuth)); + env(trust(alice, USD(10000))); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + // alice cannot remit because bob's trustline is not authorized + // all parties must be authorized + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tecNO_AUTH)); + env.close(); + + env(trust(gw, bobUSD(10000)), txflags(tfSetfAuth)); + env(trust(bob, USD(10000))); + env.close(); + env(pay(gw, bob, USD(1000))); + env.close(); + + // alice can now remit because bob's trustline is authorized + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + } + } + + void + testDepositAuth(FeatureBitset features) + { + testcase("deposit authorization"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + { + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(10000), 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(); + + auto const preBobXrp = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + auto const preAliceXrp = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + + env(fset(bob, asfDepositAuth)); + env.close(); + + // Since alice is not preauthorized, remit fails. + env(remit::remit(alice, bob), + remit::amts({USD(100)}), + ter(tecNO_PERMISSION)); + env.close(); + + // Bob preauthorizes alice for deposit, remit success. + env(deposit::auth(bob, alice)); + env.close(); + + env(remit::remit(alice, bob), + remit::amts({USD(100)}), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == preAliceXrp - (2 * feeDrops)); + BEAST_EXPECT(env.balance(bob) == preBobXrp - (2 * feeDrops)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(100)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(100)); + + // bob removes preauthorization of alice. + env(deposit::unauth(bob, alice)); + env.close(); + + // alice remits and fails since she is no longer preauthorized. + env(remit::remit(alice, bob), + remit::amts({USD(100)}), + ter(tecNO_PERMISSION)); + env.close(); + + // bob clears lsfDepositAuth. + env(fclear(bob, asfDepositAuth)); + env.close(); + + // alice remits successfully. + env(remit::remit(alice, bob), + remit::amts({USD(100)}), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == preAliceXrp - (4 * feeDrops)); + BEAST_EXPECT(env.balance(bob) == preBobXrp - (4 * feeDrops)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD - USD(200)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD + USD(200)); + } + } + + void + testTLFreeze(FeatureBitset features) + { + testcase("trustline freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + // test Global Freeze + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + env(fset(gw, asfGlobalFreeze)); + env.close(); + + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tecFROZEN)); + env.close(); + + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + } + + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env(trust(alice, USD(100000))); + env(trust(bob, USD(100000))); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(100000), alice, tfSetFreeze)); + env.close(); + + // remit fails - frozen trustline + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(100000), alice, tfClearFreeze)); + env.close(); + + // remit success + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + } + } + + void + validateNoRipple( + jtx::Env& env, + jtx::Account const& acct, + jtx::Account const& peer, + bool const& result) + { + Json::Value params; + params[jss::account] = acct.human(); + params[jss::peer] = peer.human(); + + auto lines = env.rpc("json", "account_lines", to_string(params)); + auto const& line = lines[jss::result][jss::lines][0u]; + BEAST_EXPECT(line[jss::no_ripple_peer].asBool() == result); + } + + void + testRippling(FeatureBitset features) + { + testcase("rippling"); + using namespace test::jtx; + using namespace std::literals; + + // rippling enabled + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const USDA = alice["USD"]; + auto const USDB = bob["USD"]; + auto const USDC = carol["USD"]; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // alice trusts USD bob & carol + env(trust(alice, USDB(100))); + env(trust(alice, USDC(100))); + // bob trusts USD alice & carol + env(trust(bob, USDA(100))); + env(trust(bob, USDC(100))); + // carol trusts USD alice & bob + env(trust(carol, USDA(100))); + env(trust(carol, USDB(100))); + env.close(); + // alice pays bob USDA + env(pay(alice, bob, USDA(10))); + // carol pays alice USDC + env(pay(carol, alice, USDC(10))); + env.close(); + + BEAST_EXPECT(env.balance(alice, USDA) == USDA(0)); + BEAST_EXPECT(env.balance(alice, USDB) == USDB(-10)); + BEAST_EXPECT(env.balance(alice, USDC) == USDC(10)); + BEAST_EXPECT(env.balance(bob, USDA) == USDA(10)); + BEAST_EXPECT(env.balance(bob, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(bob, USDC) == USDC(0)); + BEAST_EXPECT(env.balance(carol, USDA) == USDA(-10)); + BEAST_EXPECT(env.balance(carol, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(carol, USDC) == USDC(0)); + + validateNoRipple(env, alice, bob, false); + validateNoRipple(env, alice, carol, false); + validateNoRipple(env, bob, alice, false); + validateNoRipple(env, bob, carol, false); + validateNoRipple(env, carol, alice, false); + validateNoRipple(env, carol, bob, false); + + // alice cannot create to carol with USDB + env(remit::remit(alice, carol), + remit::amts({USDB(10)}), + ter(tecUNFUNDED_PAYMENT)); + env.close(); + + // negative direction destination + // bob can remit to carol with USDA + env(remit::remit(bob, carol), + remit::amts({USDA(10)}), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USDA) == USDA(0)); + BEAST_EXPECT(env.balance(alice, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(alice, USDC) == USDC(0)); + BEAST_EXPECT(env.balance(bob, USDA) == USDA(0)); + BEAST_EXPECT(env.balance(bob, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(bob, USDC) == USDC(0)); + BEAST_EXPECT(env.balance(carol, USDA) == USDA(0)); + BEAST_EXPECT(env.balance(carol, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(carol, USDC) == USDC(0)); + } + + // rippling not enabled + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const USDA = alice["USD"]; + auto const USDB = bob["USD"]; + auto const USDC = carol["USD"]; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // alice trusts USD bob & carol + env(trust(alice, USDB(100), bob, tfSetNoRipple)); + env(trust(alice, USDC(100), carol, tfSetNoRipple)); + // bob trusts USD alice & carol + env(trust(bob, USDA(100), alice, tfSetNoRipple)); + env(trust(bob, USDC(100), carol, tfSetNoRipple)); + // carol trusts USD alice & bob + env(trust(carol, USDA(100), alice, tfSetNoRipple)); + env(trust(carol, USDB(100), bob, tfSetNoRipple)); + env.close(); + // alice pays bob USDA + env(pay(alice, bob, USDA(10))); + // carol pays alice USDC + env(pay(carol, alice, USDC(10))); + env.close(); + + BEAST_EXPECT(env.balance(alice, USDA) == USDA(0)); + BEAST_EXPECT(env.balance(alice, USDB) == USDB(-10)); + BEAST_EXPECT(env.balance(alice, USDC) == USDC(10)); + BEAST_EXPECT(env.balance(bob, USDA) == USDA(10)); + BEAST_EXPECT(env.balance(bob, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(bob, USDC) == USDC(0)); + BEAST_EXPECT(env.balance(carol, USDA) == USDA(-10)); + BEAST_EXPECT(env.balance(carol, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(carol, USDC) == USDC(0)); + + validateNoRipple(env, alice, bob, true); + validateNoRipple(env, alice, carol, true); + validateNoRipple(env, bob, alice, true); + validateNoRipple(env, bob, carol, true); + validateNoRipple(env, carol, alice, true); + validateNoRipple(env, carol, bob, true); + + // alice cannot create to carol with USDB + env(remit::remit(alice, carol), + remit::amts({USDB(10)}), + ter(tecPATH_DRY)); + env.close(); + + // negative direction destination + // bob can not remit to carol with USDA + env(remit::remit(bob, carol), + remit::amts({USDA(10)}), + ter(tecPATH_DRY)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USDA) == USDA(0)); + BEAST_EXPECT(env.balance(alice, USDB) == USDB(-10)); + BEAST_EXPECT(env.balance(alice, USDC) == USDC(10)); + BEAST_EXPECT(env.balance(bob, USDA) == USDA(10)); + BEAST_EXPECT(env.balance(bob, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(bob, USDC) == USDC(0)); + BEAST_EXPECT(env.balance(carol, USDA) == USDA(-10)); + BEAST_EXPECT(env.balance(carol, USDB) == USDB(0)); + BEAST_EXPECT(env.balance(carol, USDC) == USDC(0)); + } + } + + void + testURIToken(FeatureBitset features) + { + testcase("uritoken"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + Env env{*this, features}; + env.fund(XRP(1000), alice, bob); + env.close(); + + // cannot mint and transfer same token + { + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + + env(remit::remit(alice, bob), + remit::uri(uri), + remit::token_ids({strHex(tid)}), + ter(tecNO_PERMISSION)); + env.close(); + } + + // mint and xfer in same ledger + { + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + + // mint uritoken + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + + // remit + 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(); + } + + // confirm offer (amount/dest) is removed on xfer + { + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + + // mint uritoken + env(uritoken::mint(alice, uri), ter(tesSUCCESS)); + + // sell offer + env(uritoken::sell(alice, strHex(tid)), + uritoken::amt(XRP(1)), + uritoken::dest(bob), + ter(tesSUCCESS)); + env.close(); + + // verify amount and destination + auto const [urikey1, uriSle1] = + uriTokenKeyAndSle(*env.current(), alice, uri); + BEAST_EXPECT(uriSle1->getAccountID(sfDestination) == bob.id()); + BEAST_EXPECT((*uriSle1)[sfAmount] == XRP(1)); + + // xfer the uritoken + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid)}), + ter(tesSUCCESS)); + env.close(); + + // verify amount and destination was removed + auto const [urikey2, uriSle2] = + uriTokenKeyAndSle(*env.current(), alice, uri); + BEAST_EXPECT(uriSle2->isFieldPresent(sfDestination) == false); + BEAST_EXPECT(uriSle2->isFieldPresent(sfAmount) == false); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // test digest + { + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(alice, uri); + + std::string const digestval = + "C16E7263F07AA41261DCC955660AF4646ADBA414E37B6F5A5BA50F75153F5C" + "CC"; + + // mint the uritoken w/ digest + env(remit::remit(alice, bob), + remit::uri(uri, 0, digestval), + ter(tesSUCCESS)); + env.close(); + + auto const [urikey, uriSle] = + uriTokenKeyAndSle(*env.current(), alice, uri); + BEAST_EXPECT( + to_string(uriSle->getFieldH256(sfDigest)) == digestval); + + // clean up test + env(uritoken::burn(bob, strHex(tid))); + env.close(); + } + + // test xfer multiple + { + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + std::string const uri2(maxTokenURILength - 1, '?'); + auto const tid2 = uritoken::tokenid(alice, uri2); + env(uritoken::mint(alice, uri2), ter(tesSUCCESS)); + env.close(); + + // xfer multiple tokens + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid1), strHex(tid2)}), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid2)); + BEAST_EXPECT(tokenOwner(*env.current(), tid2) == bob.id()); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env(uritoken::burn(bob, strHex(tid2))); + env.close(); + } + + // test sell/xfer in same ledger + { + std::string const uri1(maxTokenURILength, '?'); + auto const tid1 = uritoken::tokenid(alice, uri1); + + // mint uritoken + env(uritoken::mint(alice, uri1), ter(tesSUCCESS)); + env.close(); + + // sell uritoken + env(uritoken::sell(alice, strHex(tid1)), + uritoken::amt(XRP(1)), + uritoken::dest(bob), + ter(tesSUCCESS)); + + // buy uritoken + env(uritoken::buy(bob, strHex(tid1)), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // xfer uritoken + env(remit::remit(alice, bob), + remit::token_ids({strHex(tid1)}), + ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid1)); + BEAST_EXPECT(tokenOwner(*env.current(), tid1) == bob.id()); + + // clean up test + env(uritoken::burn(bob, strHex(tid1))); + env.close(); + } + } + + void + testOptionals(FeatureBitset features) + { + testcase("optionals"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + Env env{*this, features}; + auto const feeDrops = env.current()->fees().base; + + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // inform + { + env(remit::remit(alice, bob), + remit::inform(carol), + ter(tesSUCCESS)); + env.close(); + } + + // blob + { + ripple::Blob blob; + blob.resize(128 * 1024); + XRPAmount const extraFee = + XRPAmount{static_cast(blob.size())}; + env(remit::remit(alice, bob), + remit::blob(strHex(blob)), + fee(feeDrops + extraFee), + ter(tesSUCCESS)); + env.close(); + } + + // invoice + { + env(remit::remit(alice, bob), + invoice_id(uint256{4}), + ter(tesSUCCESS)); + env.close(); + } + } + + void + testWithFeats(FeatureBitset features) + { + testEnabled(features); + testPreflightInvalid(features); + testDoApplyInvalid(features); + testDisallowXRP(features); + testDstTag(features); + testDisallowIncoming(features); + testDestExistsTLExists(features); + testDestExistsTLNotExist(features); + testDestNotExistTLNotExist(features); + testGateway(features); + testTransferRate(features); + testRequireAuth(features); + testDepositAuth(features); + testTLFreeze(features); + testRippling(features); + testURIToken(features); + testOptionals(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testWithFeats(sa - featureXahauGenesis); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Remit, app, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/app/SetHookTSH_test.cpp b/src/test/app/SetHookTSH_test.cpp index 1024164da..3e209c0fb 100644 --- a/src/test/app/SetHookTSH_test.cpp +++ b/src/test/app/SetHookTSH_test.cpp @@ -292,6 +292,215 @@ struct SetHookTSH_test : public beast::unit_test::suite 0x00U, 0x73U, 0x21U, 0x00U, 0x41U, 0x9BU, 0x0AU, 0x0BU, 0x02U, 0x81U, 0x14U, 0x00U, 0x41U, 0xB1U, 0x0AU, 0x0BU, 0x02U, 0x83U, 0x14U}; + const std::vector EmitTenHook = { + 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x35U, + 0x08U, 0x60U, 0x05U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, + 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, + 0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU, + 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, + 0x01U, 0x7EU, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, + 0x7FU, 0x01U, 0x7EU, 0x02U, 0xBEU, 0x01U, 0x0CU, 0x03U, 0x65U, 0x6EU, + 0x76U, 0x05U, 0x74U, 0x72U, 0x61U, 0x63U, 0x65U, 0x00U, 0x00U, 0x03U, + 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, + 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x01U, 0x03U, 0x65U, + 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x72U, 0x65U, + 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, + 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, + 0x09U, 0x74U, 0x72U, 0x61U, 0x63U, 0x65U, 0x5FU, 0x6EU, 0x75U, 0x6DU, + 0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x75U, 0x74U, 0x69U, + 0x6CU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x69U, 0x64U, 0x00U, 0x05U, 0x03U, + 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, 0x72U, + 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x06U, 0x03U, 0x65U, 0x6EU, 0x76U, + 0x0AU, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x66U, 0x69U, 0x65U, 0x6CU, + 0x64U, 0x00U, 0x07U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U, + 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U, + 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x65U, 0x74U, 0x78U, + 0x6EU, 0x5FU, 0x66U, 0x65U, 0x65U, 0x5FU, 0x62U, 0x61U, 0x73U, 0x65U, + 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x65U, 0x6DU, 0x69U, + 0x74U, 0x00U, 0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, + 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x04U, 0x03U, 0x02U, 0x01U, 0x02U, + 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x31U, 0x08U, 0x7FU, 0x01U, + 0x41U, 0xD0U, 0x8DU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0xB0U, 0x0BU, + 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, + 0xCBU, 0x0DU, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, + 0x00U, 0x41U, 0xD0U, 0x8DU, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x00U, + 0x0BU, 0x7FU, 0x00U, 0x41U, 0x01U, 0x0BU, 0x07U, 0x08U, 0x01U, 0x04U, + 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x0CU, 0x0AU, 0x9AU, 0x89U, 0x00U, + 0x01U, 0x96U, 0x89U, 0x00U, 0x02U, 0x02U, 0x7FU, 0x01U, 0x7EU, 0x23U, + 0x00U, 0x41U, 0x80U, 0x01U, 0x6BU, 0x22U, 0x01U, 0x24U, 0x00U, 0x20U, + 0x01U, 0x20U, 0x00U, 0x36U, 0x02U, 0x7CU, 0x41U, 0x9AU, 0x0BU, 0x41U, + 0x14U, 0x41U, 0x87U, 0x0BU, 0x41U, 0x13U, 0x41U, 0x00U, 0x10U, 0x00U, + 0x1AU, 0x41U, 0xADU, 0x0CU, 0x41U, 0x14U, 0x10U, 0x01U, 0x1AU, 0x41U, + 0x0AU, 0x10U, 0x02U, 0x1AU, 0x20U, 0x01U, 0x42U, 0x00U, 0x37U, 0x03U, + 0x58U, 0x03U, 0x40U, 0x41U, 0xB9U, 0x80U, 0x80U, 0x80U, 0x78U, 0x41U, + 0x0BU, 0x10U, 0x03U, 0x1AU, 0x02U, 0x40U, 0x20U, 0x01U, 0x29U, 0x03U, + 0x58U, 0x42U, 0x09U, 0x55U, 0x0DU, 0x00U, 0x41U, 0xD6U, 0x08U, 0x41U, + 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0x58U, 0x10U, 0x04U, 0x1AU, 0x20U, + 0x01U, 0x29U, 0x03U, 0x58U, 0x22U, 0x03U, 0xA7U, 0x21U, 0x00U, 0x20U, + 0x03U, 0x42U, 0x09U, 0x58U, 0x04U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, + 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, + 0x02U, 0x40U, 0x02U, 0x40U, 0x02U, 0x40U, 0x20U, 0x00U, 0x41U, 0x01U, + 0x6BU, 0x0EU, 0x09U, 0x01U, 0x02U, 0x03U, 0x04U, 0x05U, 0x06U, 0x07U, + 0x08U, 0x09U, 0x00U, 0x0BU, 0x41U, 0xC3U, 0x0CU, 0x41U, 0x14U, 0x41U, + 0xB3U, 0x08U, 0x41U, 0x23U, 0x10U, 0x05U, 0x1AU, 0x0BU, 0x41U, 0xC3U, + 0x0CU, 0x41U, 0x14U, 0x41U, 0xE4U, 0x09U, 0x41U, 0x23U, 0x10U, 0x05U, + 0x1AU, 0x0BU, 0x41U, 0xC3U, 0x0CU, 0x41U, 0x14U, 0x41U, 0xFBU, 0x08U, + 0x41U, 0x23U, 0x10U, 0x05U, 0x1AU, 0x0BU, 0x41U, 0xC3U, 0x0CU, 0x41U, + 0x14U, 0x41U, 0xCDU, 0x0AU, 0x41U, 0x23U, 0x10U, 0x05U, 0x1AU, 0x0BU, + 0x41U, 0xC3U, 0x0CU, 0x41U, 0x14U, 0x41U, 0xD8U, 0x08U, 0x41U, 0x23U, + 0x10U, 0x05U, 0x1AU, 0x0BU, 0x41U, 0xC3U, 0x0CU, 0x41U, 0x14U, 0x41U, + 0xAAU, 0x0AU, 0x41U, 0x23U, 0x10U, 0x05U, 0x1AU, 0x0BU, 0x41U, 0xC3U, + 0x0CU, 0x41U, 0x14U, 0x41U, 0x8CU, 0x08U, 0x41U, 0x23U, 0x10U, 0x05U, + 0x1AU, 0x0BU, 0x41U, 0xC3U, 0x0CU, 0x41U, 0x14U, 0x41U, 0xC1U, 0x09U, + 0x41U, 0x23U, 0x10U, 0x05U, 0x1AU, 0x0BU, 0x41U, 0xC3U, 0x0CU, 0x41U, + 0x14U, 0x41U, 0x9EU, 0x09U, 0x41U, 0x23U, 0x10U, 0x05U, 0x1AU, 0x0BU, + 0x41U, 0xC3U, 0x0CU, 0x41U, 0x14U, 0x41U, 0x87U, 0x0AU, 0x41U, 0x23U, + 0x10U, 0x05U, 0x1AU, 0x0BU, 0x20U, 0x01U, 0x10U, 0x06U, 0xA7U, 0x41U, + 0x01U, 0x6AU, 0x36U, 0x02U, 0x54U, 0x41U, 0xC4U, 0x0BU, 0x20U, 0x01U, + 0x28U, 0x02U, 0x54U, 0x22U, 0x00U, 0x41U, 0xFFU, 0x01U, 0x71U, 0x41U, + 0x18U, 0x74U, 0x20U, 0x00U, 0x41U, 0x80U, 0xFEU, 0x03U, 0x71U, 0x41U, + 0x08U, 0x74U, 0x72U, 0x20U, 0x00U, 0x41U, 0x80U, 0x80U, 0xFCU, 0x07U, + 0x71U, 0x41U, 0x08U, 0x76U, 0x72U, 0x20U, 0x00U, 0x41U, 0x80U, 0x80U, + 0x80U, 0x78U, 0x71U, 0x41U, 0x18U, 0x76U, 0x72U, 0x36U, 0x02U, 0x00U, + 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x54U, 0x41U, 0x04U, 0x6AU, + 0x36U, 0x02U, 0x50U, 0x41U, 0xCAU, 0x0BU, 0x20U, 0x01U, 0x28U, 0x02U, + 0x50U, 0x22U, 0x00U, 0x41U, 0xFFU, 0x01U, 0x71U, 0x41U, 0x18U, 0x74U, + 0x20U, 0x00U, 0x41U, 0x80U, 0xFEU, 0x03U, 0x71U, 0x41U, 0x08U, 0x74U, + 0x72U, 0x20U, 0x00U, 0x41U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x71U, 0x41U, + 0x08U, 0x76U, 0x72U, 0x20U, 0x00U, 0x41U, 0x80U, 0x80U, 0x80U, 0x78U, + 0x71U, 0x41U, 0x18U, 0x76U, 0x72U, 0x36U, 0x02U, 0x00U, 0x20U, 0x01U, + 0x42U, 0xC0U, 0x84U, 0x3DU, 0x37U, 0x03U, 0x48U, 0x20U, 0x01U, 0x41U, + 0xCFU, 0x0BU, 0x36U, 0x02U, 0x44U, 0x20U, 0x01U, 0x29U, 0x03U, 0x48U, + 0x42U, 0x38U, 0x88U, 0x42U, 0x3FU, 0x83U, 0x42U, 0x40U, 0x7DU, 0xA7U, + 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x44U, 0x22U, + 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, 0x44U, 0x20U, 0x02U, 0x20U, + 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x48U, 0x42U, + 0x30U, 0x88U, 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, + 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x44U, 0x22U, 0x02U, 0x41U, 0x01U, + 0x6AU, 0x36U, 0x02U, 0x44U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, + 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x48U, 0x42U, 0x28U, 0x88U, 0x42U, + 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, + 0x28U, 0x02U, 0x44U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, + 0x44U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, + 0x29U, 0x03U, 0x48U, 0x42U, 0x20U, 0x88U, 0x42U, 0xFFU, 0x01U, 0x83U, + 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x44U, + 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, 0x44U, 0x20U, 0x02U, + 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x48U, + 0x42U, 0x18U, 0x88U, 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, + 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x44U, 0x22U, 0x02U, 0x41U, + 0x01U, 0x6AU, 0x36U, 0x02U, 0x44U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, + 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x48U, 0x42U, 0x10U, 0x88U, + 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, + 0x01U, 0x28U, 0x02U, 0x44U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, + 0x02U, 0x44U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, + 0x01U, 0x29U, 0x03U, 0x48U, 0x42U, 0x08U, 0x88U, 0x42U, 0xFFU, 0x01U, + 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, + 0x44U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, 0x44U, 0x20U, + 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, + 0x48U, 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, + 0x20U, 0x01U, 0x28U, 0x02U, 0x44U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, + 0x36U, 0x02U, 0x44U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, + 0x41U, 0xBEU, 0x0BU, 0x41U, 0x04U, 0x41U, 0x83U, 0x80U, 0x08U, 0x10U, + 0x07U, 0x42U, 0x04U, 0x51U, 0x04U, 0x40U, 0x41U, 0xBDU, 0x0BU, 0x41U, + 0x2EU, 0x3AU, 0x00U, 0x00U, 0x0BU, 0x41U, 0xD7U, 0x0CU, 0x41U, 0xF4U, + 0x00U, 0x10U, 0x08U, 0x1AU, 0x20U, 0x01U, 0x41U, 0xB0U, 0x0BU, 0x41U, + 0x9BU, 0x02U, 0x10U, 0x09U, 0x37U, 0x03U, 0x38U, 0x20U, 0x01U, 0x41U, + 0x80U, 0x0CU, 0x36U, 0x02U, 0x34U, 0x20U, 0x01U, 0x29U, 0x03U, 0x38U, + 0x42U, 0x38U, 0x87U, 0x42U, 0x3FU, 0x83U, 0x42U, 0x40U, 0x7DU, 0xA7U, + 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x34U, 0x22U, + 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, 0x34U, 0x20U, 0x02U, 0x20U, + 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x38U, 0x42U, + 0x30U, 0x87U, 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, + 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x34U, 0x22U, 0x02U, 0x41U, 0x01U, + 0x6AU, 0x36U, 0x02U, 0x34U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, + 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x38U, 0x42U, 0x28U, 0x87U, 0x42U, + 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, + 0x28U, 0x02U, 0x34U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, + 0x34U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, + 0x29U, 0x03U, 0x38U, 0x42U, 0x20U, 0x87U, 0x42U, 0xFFU, 0x01U, 0x83U, + 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x34U, + 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, 0x34U, 0x20U, 0x02U, + 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x38U, + 0x42U, 0x18U, 0x87U, 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, + 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, 0x34U, 0x22U, 0x02U, 0x41U, + 0x01U, 0x6AU, 0x36U, 0x02U, 0x34U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, + 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x38U, 0x42U, 0x10U, 0x87U, + 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, + 0x01U, 0x28U, 0x02U, 0x34U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, + 0x02U, 0x34U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, + 0x01U, 0x29U, 0x03U, 0x38U, 0x42U, 0x08U, 0x87U, 0x42U, 0xFFU, 0x01U, + 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, 0x20U, 0x01U, 0x28U, 0x02U, + 0x34U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x36U, 0x02U, 0x34U, 0x20U, + 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, + 0x38U, 0x42U, 0xFFU, 0x01U, 0x83U, 0xA7U, 0x21U, 0x00U, 0x20U, 0x01U, + 0x20U, 0x01U, 0x28U, 0x02U, 0x34U, 0x22U, 0x02U, 0x41U, 0x01U, 0x6AU, + 0x36U, 0x02U, 0x34U, 0x20U, 0x02U, 0x20U, 0x00U, 0x3AU, 0x00U, 0x00U, + 0x41U, 0xAFU, 0x08U, 0x41U, 0x03U, 0x41U, 0xB0U, 0x0BU, 0x41U, 0x9BU, + 0x02U, 0x41U, 0x01U, 0x10U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x20U, 0x01U, + 0x41U, 0x10U, 0x6AU, 0x41U, 0x20U, 0x41U, 0xB0U, 0x0BU, 0x41U, 0x9BU, + 0x02U, 0x10U, 0x0AU, 0x37U, 0x03U, 0x08U, 0x41U, 0x80U, 0x08U, 0x41U, + 0x0BU, 0x20U, 0x01U, 0x29U, 0x03U, 0x08U, 0x10U, 0x04U, 0x1AU, 0x20U, + 0x01U, 0x20U, 0x01U, 0x29U, 0x03U, 0x58U, 0x42U, 0x01U, 0x7CU, 0x37U, + 0x03U, 0x58U, 0x0CU, 0x01U, 0x0BU, 0x0BU, 0x41U, 0xF0U, 0x0AU, 0x41U, + 0x17U, 0x42U, 0x86U, 0x01U, 0x10U, 0x0BU, 0x1AU, 0x20U, 0x01U, 0x41U, + 0x80U, 0x01U, 0x6AU, 0x24U, 0x00U, 0x42U, 0x00U, 0x0BU, 0x0BU, 0xA6U, + 0x04U, 0x04U, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0xAEU, 0x03U, 0x65U, + 0x6DU, 0x69U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, + 0x00U, 0x72U, 0x4BU, 0x77U, 0x58U, 0x70U, 0x69U, 0x48U, 0x47U, 0x63U, + 0x51U, 0x53U, 0x7AU, 0x4BU, 0x45U, 0x47U, 0x58U, 0x63U, 0x77U, 0x65U, + 0x6AU, 0x35U, 0x45U, 0x71U, 0x32U, 0x43U, 0x51U, 0x74U, 0x4EU, 0x36U, + 0x48U, 0x6FU, 0x54U, 0x44U, 0x72U, 0x00U, 0x74U, 0x78U, 0x6EU, 0x00U, + 0x72U, 0x47U, 0x31U, 0x51U, 0x51U, 0x76U, 0x32U, 0x6EU, 0x68U, 0x32U, + 0x67U, 0x72U, 0x37U, 0x52U, 0x43U, 0x5AU, 0x31U, 0x50U, 0x38U, 0x59U, + 0x59U, 0x63U, 0x42U, 0x55U, 0x4BU, 0x43U, 0x43U, 0x4EU, 0x36U, 0x33U, + 0x33U, 0x6AU, 0x43U, 0x6EU, 0x00U, 0x69U, 0x00U, 0x72U, 0x4CU, 0x38U, + 0x78U, 0x65U, 0x51U, 0x53U, 0x47U, 0x78U, 0x54U, 0x6DU, 0x4BU, 0x58U, + 0x58U, 0x79U, 0x4CU, 0x55U, 0x46U, 0x52U, 0x75U, 0x41U, 0x41U, 0x4BU, + 0x73U, 0x57U, 0x4DU, 0x59U, 0x31U, 0x42U, 0x4DU, 0x70U, 0x51U, 0x62U, + 0x65U, 0x00U, 0x72U, 0x48U, 0x34U, 0x4BU, 0x45U, 0x63U, 0x47U, 0x39U, + 0x64U, 0x45U, 0x77U, 0x47U, 0x77U, 0x70U, 0x6EU, 0x36U, 0x41U, 0x79U, + 0x6FU, 0x57U, 0x4BU, 0x39U, 0x63U, 0x5AU, 0x50U, 0x4CU, 0x4CU, 0x34U, + 0x52U, 0x4CU, 0x53U, 0x6DU, 0x57U, 0x57U, 0x00U, 0x72U, 0x68U, 0x57U, + 0x35U, 0x68U, 0x67U, 0x32U, 0x78U, 0x45U, 0x37U, 0x77U, 0x32U, 0x65U, + 0x77U, 0x65U, 0x70U, 0x62U, 0x68U, 0x57U, 0x74U, 0x67U, 0x5AU, 0x57U, + 0x57U, 0x38U, 0x53U, 0x75U, 0x4EU, 0x36U, 0x75U, 0x57U, 0x39U, 0x37U, + 0x53U, 0x00U, 0x72U, 0x4DU, 0x63U, 0x58U, 0x75U, 0x59U, 0x73U, 0x51U, + 0x33U, 0x4DU, 0x6AU, 0x46U, 0x65U, 0x52U, 0x52U, 0x71U, 0x69U, 0x31U, + 0x47U, 0x76U, 0x61U, 0x73U, 0x4AU, 0x7AU, 0x54U, 0x64U, 0x4CU, 0x37U, + 0x77U, 0x75U, 0x4CU, 0x33U, 0x68U, 0x4EU, 0x00U, 0x72U, 0x50U, 0x4DU, + 0x68U, 0x37U, 0x50U, 0x69U, 0x39U, 0x63U, 0x74U, 0x36U, 0x39U, 0x39U, + 0x69U, 0x5AU, 0x55U, 0x54U, 0x57U, 0x61U, 0x79U, 0x74U, 0x4AU, 0x55U, + 0x6FU, 0x48U, 0x63U, 0x4AU, 0x37U, 0x63U, 0x67U, 0x79U, 0x7AU, 0x69U, + 0x4BU, 0x00U, 0x72U, 0x70U, 0x67U, 0x67U, 0x78U, 0x47U, 0x73U, 0x34U, + 0x79U, 0x46U, 0x61U, 0x46U, 0x45U, 0x47U, 0x54U, 0x68U, 0x78U, 0x42U, + 0x45U, 0x62U, 0x68U, 0x69U, 0x72U, 0x37U, 0x54U, 0x39U, 0x46U, 0x62U, + 0x35U, 0x4CU, 0x51U, 0x4BU, 0x4CU, 0x43U, 0x00U, 0x72U, 0x44U, 0x76U, + 0x4AU, 0x64U, 0x79U, 0x4AU, 0x58U, 0x45U, 0x4DU, 0x63U, 0x4CU, 0x42U, + 0x79U, 0x59U, 0x37U, 0x79U, 0x38U, 0x66U, 0x50U, 0x58U, 0x4BU, 0x6EU, + 0x4BU, 0x36U, 0x67U, 0x39U, 0x42U, 0x70U, 0x41U, 0x79U, 0x68U, 0x62U, + 0x39U, 0x00U, 0x72U, 0x47U, 0x57U, 0x43U, 0x31U, 0x34U, 0x79U, 0x54U, + 0x33U, 0x55U, 0x38U, 0x62U, 0x6BU, 0x75U, 0x41U, 0x78U, 0x32U, 0x63U, + 0x38U, 0x74U, 0x41U, 0x31U, 0x61U, 0x6AU, 0x4CU, 0x55U, 0x4AU, 0x43U, + 0x61U, 0x6EU, 0x64U, 0x70U, 0x35U, 0x39U, 0x00U, 0x65U, 0x6DU, 0x69U, + 0x74U, 0x74U, 0x65U, 0x6EU, 0x2EU, 0x63U, 0x3AU, 0x20U, 0x53U, 0x75U, + 0x63U, 0x63U, 0x65U, 0x73U, 0x73U, 0x66U, 0x75U, 0x6CU, 0x2EU, 0x00U, + 0x65U, 0x6DU, 0x69U, 0x74U, 0x74U, 0x65U, 0x6EU, 0x2EU, 0x63U, 0x3AU, + 0x20U, 0x43U, 0x61U, 0x6CU, 0x6CU, 0x65U, 0x64U, 0x2EU, 0x00U, 0x22U, + 0x65U, 0x6DU, 0x69U, 0x74U, 0x74U, 0x65U, 0x6EU, 0x2EU, 0x63U, 0x3AU, + 0x20U, 0x43U, 0x61U, 0x6CU, 0x6CU, 0x65U, 0x64U, 0x2EU, 0x22U, 0x00U, + 0x41U, 0xB0U, 0x0BU, 0x0BU, 0x5AU, 0x12U, 0x00U, 0x00U, 0x22U, 0x80U, + 0x00U, 0x00U, 0x00U, 0x24U, 0x00U, 0x00U, 0x00U, 0x00U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U, 0x20U, + 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x61U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, 0x99U, + 0x99U, 0x99U, 0x99U, 0x99U, 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x73U, 0x21U, 0x00U, 0x41U, 0xABU, 0x0CU, 0x0BU, + 0x02U, 0x81U, 0x14U, 0x00U, 0x41U, 0xC1U, 0x0CU, 0x0BU, 0x02U, 0x83U, + 0x14U}; + const std::vector TshHook = { 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x28U, 0x06U, 0x60U, 0x05U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, @@ -4931,6 +5140,347 @@ struct SetHookTSH_test : public beast::unit_test::suite } } + // Remit + // | otxn | tsh | remit w/amt | + // | A | A | S | + // | A | D | S | + // | A | I | W | + + // | otxn | tsh | burnable | remit w/uri | + // | A | I | F | W | + // | A | I | T | S | + + void + testRemitTSH(FeatureBitset features) + { + testcase("remit tsh"); + + using namespace test::jtx; + using namespace std::literals; + /* + sfAmounts + */ + + // otxn: account + // tsh account + // w/s: strong + for (bool const testStrong : {true, false}) + { + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const account = Account("alice"); + auto const dest = Account{"bob"}; + env.fund(XRP(1000), account, dest); + env.close(); + + // set tsh collect + if (!testStrong) + addWeakTSH(env, account); + + // set tsh hook + setTSHHook(env, account, testStrong); + + // payment + env(remit::remit(account, dest), fee(XRP(1)), ter(tesSUCCESS)); + env.close(); + + // verify tsh hook triggered + testTSHStrongWeak(env, tshSTRONG, __LINE__); + } + + // otxn: account + // tsh dest + // w/s: strong + for (bool const testStrong : {true, false}) + { + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const account = Account("alice"); + auto const dest = Account{"bob"}; + env.fund(XRP(1000), account, dest); + env.close(); + + // set tsh collect + if (!testStrong) + addWeakTSH(env, dest); + + // set tsh hook + setTSHHook(env, dest, testStrong); + + // payment + env(remit::remit(account, dest), fee(XRP(1)), ter(tesSUCCESS)); + env.close(); + + // verify tsh hook triggered + testTSHStrongWeak(env, tshSTRONG, __LINE__); + } + + // otxn: account + // tsh inform + // w/s: weak + for (bool const testStrong : {true, false}) + { + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const account = Account("alice"); + auto const dest = Account("bob"); + auto const inform = Account("carol"); + env.fund(XRP(1000), account, dest, inform); + env.close(); + + // set tsh collect + if (!testStrong) + addWeakTSH(env, inform); + + // set tsh hook + setTSHHook(env, inform, testStrong); + + // payment + env(remit::remit(account, dest), + remit::inform(inform), + fee(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify tsh hook triggered + auto const expected = testStrong ? tshNONE : tshWEAK; + testTSHStrongWeak(env, expected, __LINE__); + } + + /* + sfURITokenIDs + */ + + // otxn: account + // tsh issuer + // burnable: true + // w/s: strong + for (bool const testStrong : {true, false}) + { + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const account = Account("alice"); + auto const dest = Account("bob"); + auto const issuer = Account("carol"); + env.fund(XRP(1000), account, dest, issuer); + env.close(); + + // mint uritoken + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + env(uritoken::mint(issuer, uri), + txflags(tfBurnable), + ter(tesSUCCESS)); + + // sell uritoken + env(uritoken::sell(issuer, strHex(tid)), + uritoken::amt(XRP(1)), + uritoken::dest(account), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(account, strHex(tid)), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // set tsh collect + if (!testStrong) + addWeakTSH(env, issuer); + + // set tsh hook + setTSHHook(env, issuer, testStrong); + + // payment + env(remit::remit(account, dest), + remit::token_ids({strHex(tid)}), + fee(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify tsh hook triggered + auto const expected = testStrong ? tshSTRONG : tshSTRONG; + testTSHStrongWeak(env, expected, __LINE__); + } + + // otxn: account + // tsh issuer + // burnable: false + // w/s: weak + for (bool const testStrong : {true, false}) + { + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const account = Account("alice"); + auto const dest = Account("bob"); + auto const issuer = Account("carol"); + env.fund(XRP(1000), account, dest, issuer); + env.close(); + + // mint uritoken + std::string const uri(maxTokenURILength, '?'); + auto const tid = uritoken::tokenid(issuer, uri); + env(uritoken::mint(issuer, uri), ter(tesSUCCESS)); + + // sell uritoken + env(uritoken::sell(issuer, strHex(tid)), + uritoken::amt(XRP(1)), + uritoken::dest(account), + ter(tesSUCCESS)); + env.close(); + + // buy uritoken + env(uritoken::buy(account, strHex(tid)), + uritoken::amt(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // set tsh collect + if (!testStrong) + addWeakTSH(env, issuer); + + // set tsh hook + setTSHHook(env, issuer, testStrong); + + // payment + env(remit::remit(account, dest), + remit::token_ids({strHex(tid)}), + fee(XRP(1)), + ter(tesSUCCESS)); + env.close(); + + // verify tsh hook triggered + auto const expected = testStrong ? tshNONE : tshWEAK; + testTSHStrongWeak(env, expected, __LINE__); + } + } + + void + testEmissionOrdering(FeatureBitset features) + { + testcase("emission ordering"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{ + *this, + network::makeNetworkConfig(21337, "10", "1000000", "200000"), + features}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + auto const frank = Account("frank"); + auto const grace = Account("grace"); + auto const heidi = Account("heidi"); + auto const ivan = Account("ivan"); + auto const judy = Account("judy"); + env.fund( + XRP(1000), + alice, + bob, + carol, + dave, + elsa, + frank, + grace, + heidi, + ivan, + judy); + env.close(); + + // set tsh hook + env(hook(alice, {{hso(EmitTenHook, overrideFlag)}}, 0), + fee(XRP(2)), + ter(tesSUCCESS)); + env.close(); + + // invoke + env(invoke::invoke(alice), fee(XRP(2)), ter(tesSUCCESS)); + env.close(); + + // validate the emitted txn ids + std::vector const txIds = { + "9610F73CDD6590EB6B3C82E5EC55D4B4C80CD7128B98AA556F7EC9DD96AE7056", + "2F4582A29272390C0C25A80D4A3BCE5A14ACE6D86D8D0CB2C57719EB6FA881AE", + "89A301CFEF0DD781AB9032A6A2DCE0937BC0119D2CDD06033B8B2FD80968E519", + "DD8721B59024E168480B4DF8F8E93778601F0BD2E77FC991F3DA1182F5AD8B1E", + "5D735C2EE3CB8289F8E11621FDC9565F9D6D67F3AE59D65332EACE591D67945F", + "F02470E01731C968881AF4CBDEC90BB9E1F7AB0BE1CC22AF15451FB6D191096D", + "8AD65E541DECD49B1693F8C17DFD8A2B906F49C673C4FD2034FF772E2BE50C30", + "9F225229059CCC6257814D03C107884CF588C1C246A89ADFC16E50DF671B834C", + "13C2A54A14BADF3648CED05175E1CCAD713F7E5EA56D9735CF8813CD5551F281", + "87C60F41A96554587CED289F83F52DEE3CF670EEB189B067E6066B9A06056ADF", + }; + Json::Value params; + params[jss::transaction] = + env.tx()->getJson(JsonOptions::none)[jss::hash]; + auto const jrr = env.rpc("json", "tx", to_string(params)); + auto const meta = jrr[jss::result][jss::meta]; + auto const emissions = meta[sfHookEmissions.jsonName]; + for (size_t i = 0; i <= 9; i++) + { + auto const emitted = emissions[i][sfHookEmission.jsonName]; + BEAST_EXPECT(emitted[sfEmittedTxnID.jsonName] == txIds[i]); + } + env.close(); + + // NOTE: emitted txns are emitted in reverse + // LIFO: Last In First Out + std::vector const accounts = { + judy, + ivan, + heidi, + grace, + frank, + elsa, + dave, + carol, + bob, + alice, + }; + + // verify emitted txns + for (size_t i = 0; i <= 9; i++) + { + Json::Value params; + params[jss::transaction] = txIds[i]; + auto const jrr = env.rpc("json", "tx", to_string(params)); + auto const meta = jrr[jss::result][jss::meta]; + for (auto const& node : meta[sfAffectedNodes.jsonName]) + { + auto const nodeType = node[sfLedgerEntryType.jsonName]; + if (nodeType == ltEMITTED_TXN) + { + auto const& nf = node[sfFinalFields.jsonName]; + auto const& et = nf[sfEmittedTxn.jsonName]; + BEAST_EXPECT( + et[sfDestination.jsonName] == accounts[i].human()); + break; + } + } + } + } + void testTSH(FeatureBitset features) { @@ -4965,6 +5515,7 @@ struct SetHookTSH_test : public beast::unit_test::suite testURITokenBuyTSH(features); testURITokenCancelSellOfferTSH(features); testURITokenCreateSellOfferTSH(features); + testRemitTSH(features); } void @@ -4972,6 +5523,7 @@ struct SetHookTSH_test : public beast::unit_test::suite { testEmittedTxnReliability(features); testEmittedFlags(features); + testEmissionOrdering(features); } public: diff --git a/src/test/app/URIToken_test.cpp b/src/test/app/URIToken_test.cpp index 79088f579..1703acde3 100644 --- a/src/test/app/URIToken_test.cpp +++ b/src/test/app/URIToken_test.cpp @@ -362,6 +362,7 @@ struct URIToken_test : public beast::unit_test::suite // tecNO_PERMISSION - no permission env(uritoken::burn(carol, hexid), ter(tecNO_PERMISSION)); env.close(); + // tefBAD_LEDGER - could not remove object } @@ -454,10 +455,7 @@ struct URIToken_test : public beast::unit_test::suite using namespace std::literals::chrono_literals; // setup env - Env env{ - *this, envconfig(), features, nullptr, beast::severities::kWarning - // beast::severities::kTrace - }; + Env env{*this, envconfig(), features, nullptr}; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); @@ -508,7 +506,7 @@ struct URIToken_test : public beast::unit_test::suite ter(temBAD_CURRENCY)); env.close(); - // tecINSUFFICIENT_PAYMENT - insuficient buy offer amount + // tecINSUFFICIENT_PAYMENT - insufficient buy offer amount env(uritoken::buy(bob, hexid), uritoken::amt(USD(9)), ter(tecINSUFFICIENT_PAYMENT)); @@ -518,10 +516,13 @@ struct URIToken_test : public beast::unit_test::suite env(uritoken::sell(alice, hexid), uritoken::amt(XRP(10000))); env.close(); - // tecINSUFFICIENT_FUNDS - insuficient xrp - fees - env(uritoken::buy(bob, hexid), - uritoken::amt(XRP(1000)), - ter(tecINSUFFICIENT_PAYMENT)); + // tecINSUFFICIENT_FUNDS - insufficient xrp - fees + // fixXahauV1 - fix checking wrong account for insufficient xrp + env(pay(env.master, alice, XRP(10000))); + auto const txResult = env.current()->rules().enabled(fixXahauV1) + ? ter(tecINSUFFICIENT_FUNDS) + : ter(tecINTERNAL); + env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10000)), txResult); env.close(); // clear sell and reset new sell @@ -529,10 +530,10 @@ struct URIToken_test : public beast::unit_test::suite env(uritoken::sell(alice, hexid), uritoken::amt(USD(10000))); env.close(); - // tecINSUFFICIENT_FUNDS - insuficient amount + // tecINSUFFICIENT_FUNDS - insufficient amount env(uritoken::buy(bob, hexid), - uritoken::amt(USD(1000)), - ter(tecINSUFFICIENT_PAYMENT)); + uritoken::amt(USD(10000)), + ter(tecINSUFFICIENT_FUNDS)); env.close(); //---------------------------------------------------------------------- @@ -571,15 +572,18 @@ struct URIToken_test : public beast::unit_test::suite env(uritoken::sell(alice, hexid), uritoken::amt(XRP(1000))); env.close(); - // tecINSUFFICIENT_PAYMENT - insuficient xrp sent + // tecINSUFFICIENT_PAYMENT - insufficient xrp sent env(uritoken::buy(bob, hexid), uritoken::amt(XRP(900)), ter(tecINSUFFICIENT_PAYMENT)); env.close(); - // tecINSUFFICIENT_FUNDS - insuficient xrp - fees - env(uritoken::buy(bob, hexid), - uritoken::amt(XRP(1000)), - ter(tecINSUFFICIENT_FUNDS)); + + // tecINSUFFICIENT_FUNDS - insufficient xrp - fees + // fixXahauV1 - fix checking wrong account for insufficient xrp + auto const txResult1 = env.current()->rules().enabled(fixXahauV1) + ? ter(tecINSUFFICIENT_FUNDS) + : ter(tecINTERNAL); + env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1000)), txResult1); env.close(); // clear sell and set usd sell @@ -587,18 +591,19 @@ struct URIToken_test : public beast::unit_test::suite env(uritoken::sell(alice, hexid), uritoken::amt(USD(1000))); env.close(); - // tecINSUFFICIENT_PAYMENT - insuficient amount sent + // tecINSUFFICIENT_PAYMENT - insufficient amount sent env(uritoken::buy(bob, hexid), uritoken::amt(USD(900)), ter(tecINSUFFICIENT_PAYMENT)); env.close(); - // tecINSUFFICIENT_FUNDS - insuficient amount sent + // tecINSUFFICIENT_FUNDS - insufficient amount sent env(uritoken::buy(bob, hexid), uritoken::amt(USD(10000)), ter(tecINSUFFICIENT_FUNDS)); env.close(); - // tecNO_LINE_INSUF_RESERVE - insuficient xrp to create line + + // tecNO_LINE_INSUF_RESERVE - insufficient xrp to create line { // fund echo 251 xrp (not enough for line reserve) env.fund(XRP(251), echo); @@ -617,7 +622,7 @@ struct URIToken_test : public beast::unit_test::suite env(uritoken::sell(echo, hexid), uritoken::amt(USD(1))); env.close(); - // tecNO_LINE_INSUF_RESERVE - insuficient xrp to create line + // tecNO_LINE_INSUF_RESERVE - insufficient xrp to create line auto const txResult = env.current()->rules().enabled(fixXahauV1) ? ter(tecINSUF_RESERVE_SELLER) : ter(tecNO_LINE_INSUF_RESERVE); @@ -1974,13 +1979,16 @@ struct URIToken_test : public beast::unit_test::suite std::string multiply; std::string divide; }; - std::array testCases = {{ + std::array testCases = {{ {1, USD(100), "1100", "1100"}, {1.1, USD(100), "1110", "1090.909090909091"}, {1.0005, USD(100), "1100.05", "1099.950024987506"}, {1.005, USD(100), "1100.4999999", "1099.502487661197"}, {1.25, USD(100), "1125", "1080"}, {2, USD(100), "1200", "1050"}, + {1.25, USD(1), "1001.25", "1000.8"}, + {1.25, USD(0.1), "1000.125", "1000.08"}, + {1.25, USD(0.0001), "1000.000125", "1000.00008"}, }}; for (auto const& tc : testCases) @@ -2001,14 +2009,13 @@ struct URIToken_test : public beast::unit_test::suite // setup mint std::string const uri(maxTokenURILength, '?'); std::string const id{strHex(uritoken::tokenid(alice, uri))}; - auto const delta = USD(100); + auto const delta = tc.delta; env(uritoken::mint(alice, uri)); env(uritoken::sell(alice, id), uritoken::amt(delta)); env.close(); env(uritoken::buy(bob, id), uritoken::amt(delta)); env.close(); - auto xferRate = transferRate(*env.current(), gw); auto const postAlice = env.balance(alice, USD.issue()); if (!env.current()->rules().enabled(fixXahauV1)) { @@ -2021,6 +2028,52 @@ struct URIToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta); } + // test dust amount + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.50)); + env.close(); + env.trust(USD(100000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env.close(); + + auto const preAlice = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob, USD.issue()); + + // setup mint + std::string const uri(maxTokenURILength, '?'); + std::string const id{strHex(uritoken::tokenid(alice, uri))}; + env(uritoken::mint(alice, uri)); + + auto sellTx = uritoken::sell(alice, id); + sellTx[jss::Amount][jss::issuer] = gw.human(); + sellTx[jss::Amount][jss::currency] = "USD"; + sellTx[jss::Amount][jss::value] = "1e-81"; + env(sellTx); + env.close(); + + auto buyTx = uritoken::buy(bob, id); + buyTx[jss::Amount][jss::issuer] = gw.human(); + buyTx[jss::Amount][jss::currency] = "USD"; + buyTx[jss::Amount][jss::value] = "1e-81"; + env(buyTx); + env.close(); + auto const postAlice = env.balance(alice, USD.issue()); + + if (!env.current()->rules().enabled(fixXahauV1)) + { + BEAST_EXPECT(postAlice.value() == preAlice); + } + else + { + BEAST_EXPECT(postAlice.value() == preAlice); + } + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - USD(0)); + } + // test rate change { Env env{*this, features}; diff --git a/src/test/jtx.h b/src/test/jtx.h index 39d4a9662..a91ae3b2f 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -57,6 +57,7 @@ #include #include #include +#include #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..7a2704eaa --- /dev/null +++ b/src/test/jtx/impl/remit.cpp @@ -0,0 +1,97 @@ +//------------------------------------------------------------------------------ +/* + 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, + std::optional const& dstTag) +{ + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = jss::Remit; + jv[jss::Account] = account.human(); + jv[jss::Destination] = dest.human(); + if (dstTag) + jv[sfDestinationTag.jsonName] = *dstTag; + 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 +{ + jt.jv[sfURITokenIDs.jsonName] = Json::arrayValue; + for (std::size_t i = 0; i < token_ids_.size(); ++i) + { + jt.jv[sfURITokenIDs.jsonName].append(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_); + if (flags_) + { + jt.jv[sfMintURIToken.jsonName][sfFlags.jsonName] = *flags_; + } + if (digest_) + { + jt.jv[sfMintURIToken.jsonName][sfDigest.fieldName] = *digest_; + } +} + +} // 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..0ac5bc750 --- /dev/null +++ b/src/test/jtx/remit.h @@ -0,0 +1,128 @@ +//------------------------------------------------------------------------------ +/* + 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, + std::optional const& dstTag = std::nullopt); + +/** 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_; + std::optional flags_; + std::optional digest_; + +public: + explicit uri( + std::string const& uri, + std::optional const& flags = std::nullopt, + std::optional const& digest = std::nullopt) + : uri_(uri), flags_(flags), digest_(digest) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +} // namespace remit + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif // RIPPLE_TEST_JTX_REMIT_H_INCLUDED \ No newline at end of file diff --git a/src/test/rpc/AccountInfo_test.cpp b/src/test/rpc/AccountInfo_test.cpp index 0cda0632e..78aa9e646 100644 --- a/src/test/rpc/AccountInfo_test.cpp +++ b/src/test/rpc/AccountInfo_test.cpp @@ -546,14 +546,15 @@ class AccountInfo_test : public beast::unit_test::suite } static constexpr std:: - array, 4> + array, 5> disallowIncomingFlags{ {{"disallowIncomingCheck", asfDisallowIncomingCheck}, {"disallowIncomingNFTokenOffer", asfDisallowIncomingNFTokenOffer}, {"disallowIncomingPayChan", asfDisallowIncomingPayChan}, {"disallowIncomingTrustline", - asfDisallowIncomingTrustline}}}; + asfDisallowIncomingTrustline}, + {"disallowIncomingRemit", asfDisallowIncomingRemit}}}; if (features[featureDisallowIncoming]) { diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index e8de02602..afcf227ec 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -88,7 +88,7 @@ class AccountSet_test : public beast::unit_test::suite flag == asfDisallowIncomingPayChan || flag == asfDisallowIncomingNFTokenOffer || flag == asfDisallowIncomingTrustline || - flag == asfTshCollect) + flag == asfTshCollect || flag == asfDisallowIncomingRemit) { // These flags are part of the DisallowIncoming amendment // and are tested elsewhere diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index b4e34e6c9..85dd2978d 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -571,6 +571,15 @@ class AccountTx_test : public beast::unit_test::suite env.close(); } + // Remit + { + // Empty + // XAH + // USD + // URIToken Mint + // URIToken Transfer + } + // Setup is done. Look at the transactions returned by account_tx. Json::Value params; params[jss::account] = alice.human();