diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 7eec46e89eb..7d9d8a70660 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,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 = 78; +static constexpr std::size_t numFeatures = 79; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -371,6 +371,7 @@ extern uint256 const fixReducedOffersV2; extern uint256 const fixEnforceNFTokenTrustline; extern uint256 const fixInnerObjTemplate2; extern uint256 const featureInvariantsV1_1; +extern uint256 const featurePaychanAndEscrowForTokens; } // namespace ripple diff --git a/include/xrpl/protocol/PayChan.h b/include/xrpl/protocol/PayChan.h index 8344120ccd6..c0edfc6efbd 100644 --- a/include/xrpl/protocol/PayChan.h +++ b/include/xrpl/protocol/PayChan.h @@ -38,6 +38,32 @@ serializePayChanAuthorization( msg.add64(amt.drops()); } +inline void +serializePayChanAuthorization( + Serializer& msg, + uint256 const& key, + IOUAmount const& amt, + Currency const& cur, + AccountID const& iss) +{ + msg.add32(HashPrefix::paymentChannelClaim); + msg.addBitString(key); + if (amt == beast::zero) + msg.add64(STAmount::cNotNative); + else if (amt.signum() == -1) // 512 = not native + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 97) + << (64 - 10))); + else // 256 = positive + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 256 + 97) + << (64 - 10))); + msg.addBitString(cur); + msg.addBitString(iss); +} + } // namespace ripple #endif diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 3eed0860f54..a29f0842c96 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -552,6 +552,57 @@ isXRP(STAmount const& amount) return isXRP(amount.issue().currency); } +inline bool +isFakeXRP(STAmount const& amount) +{ + if (amount.native()) + return false; + + return isFakeXRP(amount.issue().currency); +} + +/** returns true iff adding or subtracting results in less than or equal to + * 0.01% precision loss **/ +inline bool +isAddable(STAmount const& amt1, STAmount const& amt2) +{ + // special case: adding anything to zero is always fine + if (amt1 == beast::zero || amt2 == beast::zero) + return true; + + // special case: adding two xrp amounts together. + // this is just an overflow check + if (isXRP(amt1) && isXRP(amt2)) + { + XRPAmount A = (amt1.signum() == -1 ? -(amt1.xrp()) : amt1.xrp()); + XRPAmount B = (amt2.signum() == -1 ? -(amt2.xrp()) : amt2.xrp()); + + XRPAmount finalAmt = A + B; + return (finalAmt >= A && finalAmt >= B); + } + + static const STAmount one{IOUAmount{1, 0}, noIssue()}; + static const STAmount maxLoss{IOUAmount{1, -4}, noIssue()}; + + STAmount A = amt1; + STAmount B = amt2; + + if (isXRP(A)) + A = STAmount{IOUAmount{A.xrp().drops(), -6}, noIssue()}; + + if (isXRP(B)) + B = STAmount{IOUAmount{B.xrp().drops(), -6}, noIssue()}; + + A.setIssue(noIssue()); + B.setIssue(noIssue()); + + STAmount lhs = divide((A - B) + B, A, noIssue()) - one; + STAmount rhs = divide((B - A) + A, B, noIssue()) - one; + + return ((rhs.negative() ? -rhs : rhs) + (lhs.negative() ? -lhs : lhs)) <= + maxLoss; +} + // Since `canonicalize` does not have access to a ledger, this is needed to put // the low-level routine stAmountCanonicalize on an amendment switch. Only // transactions need to use this switchover. Outside of a transaction it's safe diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 335ef8de39a..72b57cc3083 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -338,7 +338,9 @@ enum TECcodes : TERUnderlyingType { tecINVALID_UPDATE_TIME = 188, tecTOKEN_PAIR_NOT_FOUND = 189, tecARRAY_EMPTY = 190, - tecARRAY_TOO_LARGE = 191 + tecARRAY_TOO_LARGE = 191, + tecREQUIRES_FLAG = 192, + tecPRECISION_LOSS = 193 }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index a0a8069f669..080568401a5 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -77,6 +77,12 @@ isXRP(Currency const& c) return c == beast::zero; } +inline bool +isFakeXRP(Currency const& c) +{ + return c == badCurrency(); +} + /** Returns "", "XRP", or three letter ISO code. */ std::string to_string(Currency const& c); diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index a46e15f39ef..bf940ea79af 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -456,6 +456,8 @@ JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList JSS(low); // out: BookChanges +JSS(locked_balance); // out: AccountLines +JSS(lock_count); // out: AccountLines JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo JSS(lp_token); // out: amm_info diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 87395b7e189..e1250d77279 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -500,6 +500,7 @@ REGISTER_FIX (fixInnerObjTemplate2, Supported::yes, VoteBehavior::De // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. REGISTER_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo); +REGISTER_FEATURE(PaychanAndEscrowForTokens, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 30d97416cfa..63cbd38195f 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -60,7 +60,7 @@ enum class LedgerNameSpace : std::uint16_t { FEE_SETTINGS = 'e', TICKET = 'T', SIGNER_LIST = 'S', - XRP_PAYMENT_CHANNEL = 'x', + PAYMENT_CHANNEL = 'x', CHECK = 'C', DEPOSIT_PREAUTH = 'p', NEGATIVE_UNL = 'N', @@ -334,8 +334,7 @@ Keylet payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept { return { - ltPAYCHAN, - indexHash(LedgerNameSpace::XRP_PAYMENT_CHANNEL, src, dst, seq)}; + ltPAYCHAN, indexHash(LedgerNameSpace::PAYMENT_CHANNEL, src, dst, seq)}; } Keylet diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 9401c00278b..b39446e04f4 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -118,15 +118,16 @@ LedgerFormats::LedgerFormats() {sfAccount, soeREQUIRED}, {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, + {sfTransferRate, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, {sfSourceTag, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, + {sfDestinationNode, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, - {sfDestinationNode, soeOPTIONAL}, }, commonFields); @@ -200,14 +201,15 @@ LedgerFormats::LedgerFormats() {sfBalance, soeREQUIRED}, {sfPublicKey, soeREQUIRED}, {sfSettleDelay, soeREQUIRED}, + {sfTransferRate, soeOPTIONAL}, {sfExpiration, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfSourceTag, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, + {sfDestinationNode, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, - {sfDestinationNode, soeOPTIONAL}, }, commonFields); diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index d56f3983352..60ee0c6c91d 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -163,7 +163,6 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32, CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44); CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45); CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46); -// 47 is reserved for LockCount(Hooks) CONSTRUCT_TYPED_SFIELD(sfVoteWeight, "VoteWeight", UINT32, 48); CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50); CONSTRUCT_TYPED_SFIELD(sfOracleDocumentID, "OracleDocumentID", UINT32, 51); @@ -258,6 +257,7 @@ CONSTRUCT_TYPED_SFIELD(sfMinimumOffer, "MinimumOffer", AMOUNT, CONSTRUCT_TYPED_SFIELD(sfRippleEscrow, "RippleEscrow", AMOUNT, 17); CONSTRUCT_TYPED_SFIELD(sfDeliveredAmount, "DeliveredAmount", AMOUNT, 18); CONSTRUCT_TYPED_SFIELD(sfNFTokenBrokerFee, "NFTokenBrokerFee", AMOUNT, 19); +CONSTRUCT_TYPED_SFIELD(sfHookCallbackFee, "HookCallbackFee", AMOUNT, 20); // Reserve 20 & 21 for Hooks diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index f452b05464e..47ba9395c35 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -63,7 +63,7 @@ transResults() MAKE_ERROR(tecUNFUNDED_ADD, "DEPRECATED."), MAKE_ERROR(tecUNFUNDED_AMM, "Insufficient balance to fund AMM."), MAKE_ERROR(tecUNFUNDED_OFFER, "Insufficient balance to fund created offer."), - MAKE_ERROR(tecUNFUNDED_PAYMENT, "Insufficient XRP balance to send."), + MAKE_ERROR(tecUNFUNDED_PAYMENT, "Insufficient balance to send."), MAKE_ERROR(tecOWNERS, "Non-zero owner count."), MAKE_ERROR(tecNO_ISSUER, "Issuer account does not exist."), MAKE_ERROR(tecNO_AUTH, "Not authorized to hold asset."), @@ -115,6 +115,7 @@ transResults() MAKE_ERROR(tecTOKEN_PAIR_NOT_FOUND, "Token pair is not found in Oracle object."), MAKE_ERROR(tecARRAY_EMPTY, "Array is empty."), MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), + MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 813f26da736..7dc692750fd 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -62,15 +62,181 @@ struct Escrow_test : public beast::unit_test::suite 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; + /** Set the "FinishAfter" time tag on a JTx */ + struct finish_time + { + private: + NetClock::time_point value_; + + public: + explicit finish_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); + } + }; + + /** Set the "CancelAfter" time tag on a JTx */ + struct cancel_time + { + private: + NetClock::time_point value_; + + public: + explicit cancel_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); + } + }; + + struct condition + { + private: + std::string value_; + + public: + explicit condition(Slice cond) : value_(strHex(cond)) + { + } + + template + explicit condition(std::array c) + : condition(makeSlice(c)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfCondition.jsonName] = value_; + } + }; + + struct fulfillment + { + private: + std::string value_; + + public: + explicit fulfillment(Slice condition) : value_(strHex(condition)) + { + } + + template + explicit fulfillment(std::array f) + : fulfillment(makeSlice(f)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfFulfillment.jsonName] = value_; + } + }; + + static Json::Value + escrow( + jtx::Account const& account, + jtx::Account const& to, + STAmount const& amount) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[jss::Destination] = to.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + return jv; + } + + static Json::Value + finish( + jtx::Account const& account, + jtx::Account const& from, + std::uint32_t seq) + { + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowFinish; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; + } + + static Json::Value + cancel( + jtx::Account const& account, + jtx::Account const& from, + std::uint32_t seq) + { + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCancel; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; + } + + static Rate + escrowRate( + jtx::Env const& env, + jtx::Account const& account, + uint32_t const& seq) + { + auto const sle = env.le(keylet::escrow(account.id(), seq)); + if (sle->isFieldPresent(sfTransferRate)) + return ripple::Rate((*sle)[sfTransferRate]); + return Rate{0}; + } + + static STAmount + limitAmount( + jtx::Env const& env, + jtx::Account const& account, + jtx::Account const& gw, + jtx::IOU const& iou) + { + auto const aHigh = account.id() > gw.id(); + auto const sle = env.le(keylet::line(account, gw, iou.currency)); + if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) + return (*sle)[aHigh ? sfLowLimit : sfHighLimit]; + return STAmount(iou, 0); + } + + 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); + } void - testEnablement() + testEnablement(FeatureBitset features) { testcase("Enablement"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 1s)); env.close(); @@ -99,14 +265,14 @@ struct Escrow_test : public beast::unit_test::suite } void - testTiming() + testTiming(FeatureBitset features) { using namespace jtx; using namespace std::chrono; { testcase("Timing: Finish Only"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -128,7 +294,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Timing: Cancel Only"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -160,7 +326,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Timing: Finish and Cancel -> Finish"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -194,7 +360,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Timing: Finish and Cancel -> Cancel"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -236,14 +402,14 @@ struct Escrow_test : public beast::unit_test::suite } void - testTags() + testTags(FeatureBitset features) { testcase("Tags"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -272,7 +438,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testDisallowXRP() + testDisallowXRP(FeatureBitset features) { testcase("Disallow XRP"); @@ -292,7 +458,7 @@ struct Escrow_test : public beast::unit_test::suite { // Ignore the "asfDisallowXRP" account flag, which we should // have been doing before. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); @@ -301,7 +467,7 @@ struct Escrow_test : public beast::unit_test::suite } void - test1571() + test1571(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -343,7 +509,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (with fix1571)"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); @@ -370,14 +536,14 @@ struct Escrow_test : public beast::unit_test::suite } void - testFails() + testFails(FeatureBitset features) { testcase("Failure Cases"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -399,10 +565,10 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "carol"); - // Using non-XRP: - env(escrow("alice", "carol", Account("alice")["USD"](500)), - finish_time(env.now() + 1s), - ter(temBAD_AMOUNT)); + // // Using non-XRP: + // env(escrow("alice", "carol", Account("alice")["USD"](500)), + // finish_time(env.now() + 1s), + // ter(temBAD_AMOUNT)); // Sending zero or no XRP: env(escrow("alice", "carol", XRP(0)), @@ -502,7 +668,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testLockup() + testLockup(FeatureBitset features) { testcase("Lockup"); @@ -511,7 +677,7 @@ struct Escrow_test : public beast::unit_test::suite { // Unconditional - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); env(escrow("alice", "alice", XRP(1000)), @@ -535,7 +701,7 @@ struct Escrow_test : public beast::unit_test::suite // Unconditionally pay from Alice to Bob. Zelda (neither source nor // destination) signs all cancels and finishes. This shows that // Escrow will make a payment to Bob with no intervention from Bob. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); @@ -560,7 +726,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Bob sets DepositAuth so only Bob can finish the escrow. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "zelda"); env(fset("bob", asfDepositAuth)); @@ -598,7 +764,7 @@ struct Escrow_test : public beast::unit_test::suite { // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can // finish the escrow. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "zelda"); env(fset("bob", asfDepositAuth)); @@ -625,7 +791,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Conditional - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); env(escrow("alice", "alice", XRP(1000)), @@ -666,7 +832,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Self-escrowed conditional with DepositAuth. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); @@ -702,7 +868,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Self-escrowed conditional with DepositAuth and DepositPreauth. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); @@ -745,7 +911,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowConditions() + testEscrowConditions(FeatureBitset features) { testcase("Escrow with CryptoConditions"); @@ -753,7 +919,7 @@ struct Escrow_test : public beast::unit_test::suite using namespace std::chrono; { // Test cryptoconditions - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); @@ -826,7 +992,7 @@ struct Escrow_test : public beast::unit_test::suite env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); } { // Test cancel when condition is present - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); @@ -842,7 +1008,7 @@ struct Escrow_test : public beast::unit_test::suite BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); } { - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); auto const seq = env.seq("alice"); @@ -864,7 +1030,7 @@ struct Escrow_test : public beast::unit_test::suite env.require(balance("carol", XRP(5000))); } { // Test long & short conditions during creation - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); std::vector v; @@ -921,7 +1087,7 @@ struct Escrow_test : public beast::unit_test::suite env.require(balance("carol", XRP(6000))); } { // Test long and short conditions & fulfillments during finish - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); std::vector cv; @@ -1067,7 +1233,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Test empty condition during creation and // empty condition & fulfillment during finish - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob", "carol"); env(escrow("alice", "carol", XRP(1000)), @@ -1113,7 +1279,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Test a condition other than PreimageSha256, which // would require a separate amendment - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), "alice", "bob"); std::array cb = { @@ -1133,7 +1299,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testMetaAndOwnership() + testMetaAndOwnership(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -1145,7 +1311,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to self"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); @@ -1220,7 +1386,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to other"); - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); @@ -1310,13 +1476,13 @@ struct Escrow_test : public beast::unit_test::suite } void - testConsequences() + testConsequences(FeatureBitset features) { testcase("Consequences"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env{*this, features}; env.memoize("alice"); env.memoize("bob"); @@ -1370,7 +1536,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowWithTickets() + testEscrowWithTickets(FeatureBitset features) { testcase("Escrow with tickets"); @@ -1381,7 +1547,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and finish using tickets. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), alice, bob); env.close(); @@ -1442,7 +1608,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and cancel using tickets. - Env env(*this); + Env env{*this, features}; env.fund(XRP(5000), alice, bob); env.close(); @@ -1508,20 +1674,2594 @@ struct Escrow_test : public beast::unit_test::suite } } + void + testIOUEnablement(FeatureBitset features) + { + testcase("IOU Enablement"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 1s)); + env.close(); + + auto const seq1 = env.seq(alice); + + env(escrow(alice, bob, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + + auto const seq2 = env.seq(alice); + + env(escrow(alice, bob, USD(1000)), + condition(cb2), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s), + fee(1500)); + env.close(); + env(cancel(bob, alice, seq2), fee(1500)); + } + + void + testIOUTiming(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + { + testcase("Timing: IOU Finish Only"); + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be finished in the future + auto const ts = env.now() + 97s; + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(ts)); + + // Advance the ledger, verifying that the finish won't complete + // prematurely. + for (; env.now() < ts; env.close()) + env(finish(bob, alice, seq), fee(1500), ter(tecNO_PERMISSION)); + + env(finish(bob, alice, seq), fee(1500)); + } + + { + testcase("Timing: IOU Cancel Only"); + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const ts = env.now() + 117s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", USD(1000)), + condition(cb1), + cancel_time(ts)); + + // Advance the ledger, verifying that the cancel won't complete + // prematurely. + for (; env.now() < ts; env.close()) + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + + // Verify that a finish won't work anymore. + env(finish("bob", "alice", seq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ter(tecNO_PERMISSION)); + + // Verify that the cancel will succeed + env(cancel("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: IOU Finish and Cancel -> Finish"); + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const fts = env.now() + 117s; + auto const cts = env.now() + 192s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", USD(1000)), + finish_time(fts), + cancel_time(cts)); + + // Advance the ledger, verifying that the finish and cancel won't + // complete prematurely. + for (; env.now() < fts; env.close()) + { + env(finish("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + } + + // Verify that a cancel still won't work + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // And verify that a finish will + env(finish("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: IOU Finish and Cancel -> Cancel"); + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const fts = env.now() + 109s; + auto const cts = env.now() + 184s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", USD(1000)), + finish_time(fts), + cancel_time(cts)); + + // Advance the ledger, verifying that the finish and cancel won't + // complete prematurely. + for (; env.now() < fts; env.close()) + { + env(finish("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + } + + // Continue advancing, verifying that the cancel won't complete + // prematurely. At this point a finish would succeed. + for (; env.now() < cts; env.close()) + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + + // Verify that finish will no longer work, since we are past the + // cancel activation time. + env(finish("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // And verify that a cancel will succeed. + env(cancel("bob", "alice", seq), fee(1500)); + } + } + + void + testIOUTags(FeatureBitset features) + { + testcase("IOU Tags"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Check to make sure that we correctly detect if tags are really + // required: + env(fset(bob, asfRequireDest)); + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() + 1s), + ter(tecDST_TAG_NEEDED)); + + // set source and dest tags + auto const seq = env.seq(alice); + + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() + 1s), + stag(1), + dtag(2)); + + auto const sle = env.le(keylet::escrow(alice.id(), seq)); + BEAST_EXPECT(sle); + BEAST_EXPECT((*sle)[sfSourceTag] == 1); + BEAST_EXPECT((*sle)[sfDestinationTag] == 2); + } + + void + testIOUDisallowXRP(FeatureBitset features) + { + testcase("IOU Disallow XRP"); + + using namespace jtx; + using namespace std::chrono; + + auto const bob = Account("bob"); + auto const george = Account("george"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + { + // Respect the "asfDisallowXRP" account flag: + Env env(*this, supported_amendments() - featureDepositAuth); + env.fund(XRP(5000), bob, george, gw); + env.close(); + env.trust(USD(10000), bob, george); + env.close(); + env(pay(gw, bob, USD(5000))); + env(pay(gw, george, USD(5000))); + env.close(); + env(fset(george, asfDisallowXRP)); + env(escrow(bob, george, USD(10)), finish_time(env.now() + 1s)); + } + { + // Ignore the "asfDisallowXRP" account flag, which we should + // have been doing before. + Env env{*this, features}; + env.fund(XRP(5000), bob, george, gw); + env.close(); + env.trust(USD(10000), bob, george); + env.close(); + env(pay(gw, bob, USD(5000))); + env(pay(gw, george, USD(5000))); + env.close(); + env(fset(george, asfDisallowXRP)); + env(escrow(bob, george, USD(10)), finish_time(env.now() + 1s)); + } + } + + void + testIOU1571(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + { + testcase("IOU Implied Finish Time (without fix1571)"); + + Env env(*this, supported_amendments() - fix1571); + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + // Creating an escrow without a finish time and finishing it + // is allowed without fix1571: + auto const seq1 = env.seq(alice); + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 1s), + fee(1500)); + env.close(); + env(finish(carol, alice, seq1), fee(1500)); + BEAST_EXPECT(env.balance(bob, USD.issue()).value() == USD(5100)); + env.close(); + + // Creating an escrow without a finish time and a condition is + // also allowed without fix1571: + auto const seq2 = env.seq(alice); + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 1s), + condition(cb1), + fee(1500)); + env.close(); + env(finish(carol, alice, seq2), + condition(cb1), + fulfillment(fb1), + fee(1500)); + BEAST_EXPECT(env.balance(bob, USD.issue()).value() == USD(5200)); + } + + { + testcase("IOU Implied Finish Time (with fix1571)"); + + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + // Creating an escrow with only a cancel time is not allowed: + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 90s), + fee(1500), + ter(temMALFORMED)); + + // Creating an escrow with only a cancel time and a condition is + // allowed: + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 90s), + condition(cb1), + fee(1500)); + env.close(); + env(finish(carol, alice, seq), + condition(cb1), + fulfillment(fb1), + fee(1500)); + BEAST_EXPECT(env.balance(bob, USD.issue()).value() == USD(5100)); + } + } + + void + testIOUFails(FeatureBitset features) + { + testcase("IOU Failure Cases"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Finish time is in the past + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() - 5s), + ter(tecNO_PERMISSION)); + + // Cancel time is in the past + env(escrow(alice, bob, USD(1000)), + condition(cb1), + cancel_time(env.now() - 5s), + ter(tecNO_PERMISSION)); + + // no destination account + env(escrow(alice, "carol", USD(1000)), + finish_time(env.now() + 1s), + ter(tecNO_DST)); + + auto const carol = Account("carol"); + env.fund(XRP(5000), carol); + env.close(); + env.trust(USD(10000), carol); + env.close(); + env(pay(gw, carol, USD(5000))); + env.close(); + + // Sending zero or no XRP: + env(escrow(alice, carol, USD(0)), + finish_time(env.now() + 1s), + ter(temBAD_AMOUNT)); + env(escrow(alice, carol, USD(-1000)), + finish_time(env.now() + 1s), + ter(temBAD_AMOUNT)); + + // Fail if neither CancelAfter nor FinishAfter are specified: + env(escrow(alice, carol, USD(1)), ter(temBAD_EXPIRATION)); + + // Fail if neither a FinishTime nor a condition are attached: + env(escrow(alice, carol, USD(1)), + cancel_time(env.now() + 1s), + ter(temMALFORMED)); + + // Fail if FinishAfter has already passed: + env(escrow(alice, carol, USD(1)), + finish_time(env.now() - 1s), + ter(tecNO_PERMISSION)); + + // If both CancelAfter and FinishAfter are set, then CancelAfter must + // be strictly later than FinishAfter. + env(escrow(alice, carol, USD(1)), + condition(cb1), + finish_time(env.now() + 10s), + cancel_time(env.now() + 10s), + ter(temBAD_EXPIRATION)); + + env(escrow(alice, carol, USD(1)), + condition(cb1), + finish_time(env.now() + 10s), + cancel_time(env.now() + 5s), + ter(temBAD_EXPIRATION)); + + // Carol now requires the use of a destination tag + env(fset(carol, asfRequireDest)); + + // missing destination tag + env(escrow(alice, carol, USD(1)), + condition(cb1), + cancel_time(env.now() + 1s), + ter(tecDST_TAG_NEEDED)); + + // Success! + env(escrow(alice, carol, USD(1)), + condition(cb1), + cancel_time(env.now() + 1s), + dtag(1)); + + { // Fail if the sender wants to send more than he has: + auto const daniel = Account("daniel"); + env.fund(XRP(5000), daniel); + env.close(); + env.trust(USD(100), daniel); + env.close(); + env(pay(gw, daniel, USD(50))); + env.close(); + + env(escrow(daniel, bob, USD(51)), + finish_time(env.now() + 1s), + ter(tecINSUFFICIENT_FUNDS)); + + // Removed 3 Account Reserve/Increment XRP tests + // See line 602 + + env(escrow(daniel, bob, USD(10)), finish_time(env.now() + 1s)); + env.close(); + env(escrow(daniel, bob, USD(51)), + finish_time(env.now() + 1s), + ter(tecINSUFFICIENT_FUNDS)); + } + + { // Specify incorrect sequence number + auto const hannah = Account("hannah"); + env.fund(XRP(5000), hannah); + env.close(); + env.trust(USD(10000), hannah); + env.close(); + env(pay(gw, hannah, USD(5000))); + env.close(); + + auto const seq = env.seq(hannah); + env(escrow(hannah, hannah, USD(5000)), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + env(finish(hannah, hannah, seq + 7), fee(1500), ter(tecNO_TARGET)); + } + + { // Try to specify a condition for a non-conditional payment + auto const ivan = Account("ivan"); + env.fund(XRP(5000), ivan); + env.close(); + env.trust(USD(10000), ivan); + env.close(); + env(pay(gw, ivan, USD(5000))); + env.close(); + + auto const seq = env.seq(ivan); + + env(escrow(ivan, ivan, USD(10)), finish_time(env.now() + 1s)); + env.close(); + env(finish(ivan, ivan, seq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + } + } + + void + testIOULockup(FeatureBitset features) + { + testcase("IOU Lockup"); + + using namespace jtx; + using namespace std::chrono; + + { + // Unconditional + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice); + env.trust(USD(10000), bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + + // Finish should succeed. Verify funds. + env(finish(bob, alice, seq)); + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(5000))); + } + { + // Unconditionally pay from Alice to Bob. Zelda (neither source nor + // destination) signs all cancels and finishes. This shows that + // Escrow will make a payment to Bob with no intervention from Bob. + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 5s)); + + // Verify amounts + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + + // Finish should succeed. Verify funds. + env(finish(carol, alice, seq)); + env.close(); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + env.require(balance(bob, USD(6000))); + env.require(balance(carol, XRP(5000) - drops(40))); + } + { + // Bob sets DepositAuth so only Bob can finish the escrow. + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + env(fset(bob, asfDepositAuth)); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 5s)); + + // Verify amounts + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible. Finish will only succeed + // for + // Bob, because of DepositAuth. + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq)); + env.close(); + + // Verify amounts + env.require(balance(alice, USD(4000))); + env.require(balance(bob, USD(6000))); + auto const baseFee = env.current()->fees().base; + env.require(balance(alice, XRP(5000) - (baseFee * 5))); + env.require(balance(bob, XRP(5000) - (baseFee * 5))); + env.require(balance(carol, XRP(5000) - (baseFee * 4))); + } + { + // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can + // finish the escrow. + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + env(fset(bob, asfDepositAuth)); + env.close(); + env(deposit::auth(bob, carol)); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + env.close(); + + // DepositPreauth allows Finish to succeed for either Zelda or + // Bob. But Finish won't succeed for Alice since she is not + // preauthorized. + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq)); + env.close(); + + env.require(balance(alice, USD(4000))); + env.require(balance(bob, USD(6000))); + auto const baseFee = env.current()->fees().base; + env.require(balance(alice, XRP(5000) - (baseFee * 2))); + env.require(balance(bob, XRP(5000) - (baseFee * 2))); + env.require(balance(carol, XRP(5000) - (baseFee * 1))); + } + { + // Conditional + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), + condition(cb2), + finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible. Finish is possible but + // requires the fulfillment associated with the escrow. + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(alice, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env.close(); + + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500)); + env.close(); + + env.require(balance(alice, USD(5000))); + } + { + // Self-escrowed conditional with DepositAuth. + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), + condition(cb3), + finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + env.close(); + + // Finish is now possible but requires the cryptocondition. + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(alice, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + + // Enable deposit authorization. After this only Alice can finish + // the escrow. + env(fset(alice, asfDepositAuth)); + env.close(); + + env(finish(alice, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500), + ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500)); + env.close(); + + env.require(balance(alice, USD(5000))); + } + { + // Self-escrowed conditional with DepositAuth and DepositPreauth. + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), + condition(cb3), + finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + env.close(); + + // Alice preauthorizes Zelda for deposit, even though Alice has + // not + // set the lsfDepositAuth flag (yet). + env(deposit::auth(alice, carol)); + env.close(); + + // Finish is now possible but requires the cryptocondition. + env(finish(alice, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(carol, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + + // Alice enables deposit authorization. After this only Alice or + // Zelda (because Zelda is preauthorized) can finish the escrow. + env(fset(alice, asfDepositAuth)); + env.close(); + + env(finish(alice, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500), + ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500)); + env.close(); + + env.require(balance(alice, USD(5000))); + } + } + + void + testIOUEscrowConditions(FeatureBitset features) + { + testcase("IOU Escrow with CryptoConditions"); + + using namespace jtx; + using namespace std::chrono; + + { // Test cryptoconditions + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env(escrow(alice, carol, USD(1000)), + condition(cb1), + cancel_time(env.now() + 1s)); + + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + env.require(balance(carol, USD(5000))); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish without a fulfillment + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish with a condition instead of a fulfillment + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(cb1), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(cb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(cb3), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish with an incorrect condition and various + // combinations of correct and incorrect fulfillments. + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb1), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb3), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish with the correct condition & fulfillment + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(fb1), + fee(1500)); + + // SLE removed on finish + BEAST_EXPECT(!env.le(keylet::escrow(Account(alice).id(), seq))); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env.require(balance(carol, USD(6000))); + env(cancel(bob, alice, seq), ter(tecNO_TARGET)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env(cancel(bob, carol, 1), ter(tecNO_TARGET)); + } + { // Test cancel when condition is present + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env(escrow(alice, carol, USD(1000)), + condition(cb2), + cancel_time(env.now() + 1s)); + env.close(); + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + // balance restored on cancel + env(cancel(bob, alice, seq)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(5000))); + // SLE removed on cancel + BEAST_EXPECT(!env.le(keylet::escrow(Account(alice).id(), seq))); + } + { + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(cb3), + cancel_time(env.now() + 1s)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + // cancel fails before expiration + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env.close(); + // finish fails after expiration + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500), + ter(tecNO_PERMISSION)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env.require(balance(carol, USD(5000))); + } + { // Test long & short conditions during creation + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + std::vector v; + v.resize(cb1.size() + 2, 0x78); + std::memcpy(v.data() + 1, cb1.data(), cb1.size()); + + auto const p = v.data(); + auto const s = v.size(); + + auto const ts = env.now() + 1s; + + // All these are expected to fail, because the + // condition we pass in is malformed in some way + env(escrow(alice, carol, USD(1000)), + condition(Slice{p, s}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p, s - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p, s - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 1, s - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 1, s - 3}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 2, s - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 2, s - 3}), + cancel_time(ts), + ter(temMALFORMED)); + + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 1, s - 2}), + cancel_time(ts), + fee(100)); + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(fb1), + fee(1500)); + + env.require(balance(alice, XRP(5000) - drops(100))); + env.require(balance(alice, USD(4000))); + env.require(balance(bob, XRP(5000) - drops(1500))); + env.require(balance(bob, USD(5000))); + env.require(balance(carol, XRP(5000))); + env.require(balance(carol, USD(6000))); + } + { // Test long and short conditions & fulfillments during finish + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + std::vector cv; + cv.resize(cb2.size() + 2, 0x78); + std::memcpy(cv.data() + 1, cb2.data(), cb2.size()); + + auto const cp = cv.data(); + auto const cs = cv.size(); + + std::vector fv; + fv.resize(fb2.size() + 2, 0x13); + std::memcpy(fv.data() + 1, fb2.data(), fb2.size()); + + auto const fp = fv.data(); + auto const fs = fv.size(); + + auto const ts = env.now() + 1s; + + // All these are expected to fail, because the + // condition we pass in is malformed in some way + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp, cs}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp, cs - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp, cs - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 1, cs - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 1, cs - 3}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 2, cs - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 2, cs - 3}), + cancel_time(ts), + ter(temMALFORMED)); + + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 1, cs - 2}), + cancel_time(ts), + fee(100)); + + // Now, try to fulfill using the same sequence of + // malformed conditions. + env(finish(bob, alice, seq), + condition(Slice{cp, cs}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp, cs - 1}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp, cs - 2}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 1}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 3}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 2, cs - 2}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 2, cs - 3}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + + // Now, using the correct condition, try malformed fulfillments: + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs - 1}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs - 2}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 1}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 3}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 3}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 2, fs - 2}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 2, fs - 3}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + + // Now try for the right one + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500)); + + env.require(balance(alice, XRP(5000) - drops(100))); + env.require(balance(alice, USD(4000))); + env.require(balance(carol, XRP(5000))); + env.require(balance(carol, USD(6000))); + } + { // Test empty condition during creation and + // empty condition & fulfillment during finish + Env env{*this, features}; + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + env(escrow(alice, carol, USD(1000)), + condition(Slice{}), + cancel_time(env.now() + 1s), + ter(temMALFORMED)); + + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(cb3), + cancel_time(env.now() + 1s)); + + env(finish(bob, alice, seq), + condition(Slice{}), + fulfillment(Slice{}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(Slice{}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{}), + fulfillment(fb3), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + + // Assemble finish that is missing the Condition or the Fulfillment + // since either both must be present, or neither can: + env(finish(bob, alice, seq), condition(cb3), ter(temMALFORMED)); + env(finish(bob, alice, seq), fulfillment(fb3), ter(temMALFORMED)); + + // Now finish it. + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + env.require(balance(carol, XRP(5000))); + env.require(balance(carol, USD(6000))); + } + { // Test a condition other than PreimageSha256, which + // would require a separate amendment + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + std::array cb = { + {0xA2, 0x2B, 0x80, 0x20, 0x42, 0x4A, 0x70, 0x49, 0x49, + 0x52, 0x92, 0x67, 0xB6, 0x21, 0xB3, 0xD7, 0x91, 0x19, + 0xD7, 0x29, 0xB2, 0x38, 0x2C, 0xED, 0x8B, 0x29, 0x6C, + 0x3C, 0x02, 0x8F, 0xA9, 0x7D, 0x35, 0x0F, 0x6D, 0x07, + 0x81, 0x03, 0x06, 0x34, 0xD2, 0x82, 0x02, 0x03, 0xC8}}; + + // FIXME: this transaction should, eventually, return temDISABLED + // instead of temMALFORMED. + env(escrow(alice, bob, USD(1000)), + condition(cb), + cancel_time(env.now() + 1s), + ter(temMALFORMED)); + } + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + 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"]; + { + testcase("IOU Metadata to self"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow(alice, alice, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 500s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(aa); + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) != aod.end()); + } + + env(escrow(bob, bob, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bb); + + { + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + env.close(5s); + env(finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + env.close(5s); + env(cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) == bod.end()); + } + } + { + testcase("IOU Metadata to other"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow(bob, carol, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) != aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), bc) != cod.end()); + } + + env.close(5s); + env(finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + } + + env.close(5s); + env(cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) == bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + } + } + } + + void + testIOUConsequences(FeatureBitset features) + { + testcase("IOU Consequences"); + + using namespace jtx; + using namespace std::chrono; + Env env{*this, features}; + + 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.fund(XRP(5000), alice, bob, carol, gw); + env.close(); + env.trust(USD(10000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + + env.memoize(alice); + env.memoize(bob); + env.memoize(carol); + + { + auto const jtx = env.jt( + escrow(alice, carol, USD(1000)), + finish_time(env.now() + 1s), + seq(1), + fee(10)); + auto const pf = preflight( + env.app(), + env.current()->rules(), + *jtx.stx, + tapNONE, + env.journal); + BEAST_EXPECT(pf.ter == tesSUCCESS); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend().value() == 0); + } + + { + auto const jtx = env.jt(cancel(bob, alice, 3), seq(1), fee(10)); + auto const pf = preflight( + env.app(), + env.current()->rules(), + *jtx.stx, + tapNONE, + env.journal); + BEAST_EXPECT(pf.ter == tesSUCCESS); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend().value() == 0); + } + + { + auto const jtx = env.jt(finish(bob, alice, 3), seq(1), fee(10)); + auto const pf = preflight( + env.app(), + env.current()->rules(), + *jtx.stx, + tapNONE, + env.journal); + BEAST_EXPECT(pf.ter == tesSUCCESS); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend().value() == 0); + } + } + + void + testIOUEscrowWithTickets(FeatureBitset features) + { + testcase("IOU Escrow with tickets"); + + using namespace jtx; + using namespace std::chrono; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + { + // Create escrow and finish using tickets. + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + // alice creates a ticket. + std::uint32_t const aliceTicket{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // bob creates a bunch of tickets because he will be burning + // through them with tec transactions. Just because we can + // we'll use them up starting from largest and going smaller. + constexpr static std::uint32_t bobTicketCount{20}; + env(ticket::create(bob, bobTicketCount)); + env.close(); + std::uint32_t bobTicket{env.seq(bob)}; + env.require(tickets(alice, 1)); + env.require(tickets(bob, bobTicketCount)); + + // Note that from here on all transactions use tickets. No account + // root sequences should change. + std::uint32_t const aliceRootSeq{env.seq(alice)}; + std::uint32_t const bobRootSeq{env.seq(bob)}; + + // alice creates an escrow that can be finished in the future + auto const ts = env.now() + 97s; + + std::uint32_t const escrowSeq = aliceTicket; + env(escrow(alice, bob, USD(1000)), + finish_time(ts), + ticket::use(aliceTicket)); + BEAST_EXPECT(env.seq(alice) == aliceRootSeq); + env.require(tickets(alice, 0)); + env.require(tickets(bob, bobTicketCount)); + + // Advance the ledger, verifying that the finish won't complete + // prematurely. Note that each tec consumes one of bob's tickets. + for (; env.now() < ts; env.close()) + { + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(--bobTicket), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + // bob tries to re-use a ticket, which is rejected. + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket), + ter(tefNO_TICKET)); + + // bob uses one of his remaining tickets. Success! + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(--bobTicket)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + { + // Create escrow and cancel using tickets. + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // alice creates a ticket. + std::uint32_t const aliceTicket{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // bob creates a bunch of tickets because he will be burning + // through them with tec transactions. + constexpr std::uint32_t bobTicketCount{20}; + std::uint32_t bobTicket{env.seq(bob) + 1}; + env(ticket::create(bob, bobTicketCount)); + env.close(); + env.require(tickets(alice, 1)); + env.require(tickets(bob, bobTicketCount)); + + // Note that from here on all transactions use tickets. No account + // root sequences should change. + std::uint32_t const aliceRootSeq{env.seq(alice)}; + std::uint32_t const bobRootSeq{env.seq(bob)}; + + // alice creates an escrow that can be finished in the future. + auto const ts = env.now() + 117s; + + std::uint32_t const escrowSeq = aliceTicket; + env(escrow(alice, bob, USD(1000)), + condition(cb1), + cancel_time(ts), + ticket::use(aliceTicket)); + BEAST_EXPECT(env.seq(alice) == aliceRootSeq); + env.require(tickets(alice, 0)); + env.require(tickets(bob, bobTicketCount)); + + // Advance the ledger, verifying that the cancel won't complete + // prematurely. + for (; env.now() < ts; env.close()) + { + env(cancel(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket++), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + // Verify that a finish won't work anymore. + env(finish(bob, alice, escrowSeq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ticket::use(bobTicket++), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + + // Verify that the cancel succeeds. + env(cancel(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket++)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + + // Verify that bob actually consumed his tickets. + env.require(tickets(bob, env.seq(bob) - bobTicket)); + } + } + + void + testIOURippleState(FeatureBitset features) + { + testcase("IOU RippleState"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + Account gw; + bool hasTrustline; + bool negative; + }; + + std::array tests = {{ + // src > dst && src > issuer && dst no trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true}, + // src < dst && src < issuer && dst no trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false}, + // // dst > src && dst > issuer && dst no trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true}, + // // dst < src && dst < issuer && dst no trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false}, + // // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true}, + // // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false}, + // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true}, + // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100000), t.src, t.dst); + else + env.trust(USD(100000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10000))); + env.close(); + + // src can create escrow + auto const seq1 = env.seq(t.src); + auto const delta = USD(1000); + env(escrow(t.src, t.dst, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // dst can finish escrow + auto const preSrc = lineBalance(env, t.src, t.gw, USD); + auto const preDst = lineBalance(env, t.dst, t.gw, USD); + + env(finish(t.dst, t.src, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(lineBalance(env, t.src, t.gw, USD) == preSrc); + BEAST_EXPECT( + lineBalance(env, t.dst, t.gw, USD) == + (t.negative ? (preDst - delta) : (preDst + delta))); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU 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(); + + // issuer can create escrow + auto const seq1 = env.seq(t.src); + auto const preDst = lineBalance(env, t.dst, t.src, USD); + env(escrow(t.src, t.dst, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // src can finish escrow, no dest trustline + env(finish(t.dst, t.src, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + auto const preAmount = t.hasTrustline ? 10000 : 0; + BEAST_EXPECT( + preDst == (t.negative ? -USD(preAmount) : USD(preAmount))); + auto const postAmount = t.hasTrustline ? 11000 : 1000; + 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("carol0"), Account{"gw1"}, true, false}, + // // // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true, true}, + // // // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env.close(); + + env.trust(USD(100000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10000))); + env.close(); + + // issuer can create escrow + auto const seq1 = env.seq(t.src); + auto const preSrc = lineBalance(env, t.src, t.dst, USD); + env(escrow(t.src, t.dst, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // src can finish escrow, no dest trustline + env(finish(t.dst, t.src, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + auto const preAmount = 10000; + BEAST_EXPECT( + preSrc == (t.negative ? -USD(preAmount) : USD(preAmount))); + auto const postAmount = 9000; + 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)); + } + } + + void + testIOULockedRate(FeatureBitset features) + { + testcase("IOU Locked Rate"); + 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"]; + + // test locked rate + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.25)); + 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(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD.issue()); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + auto const transferRate = escrowRate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // bob can finish escrow + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD.issue()) == USD(10100)); + } + // test rate change - higher + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.25)); + 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(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD.issue()); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + auto transferRate = escrowRate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can finish escrow - rate unchanged + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD.issue()) == USD(10100)); + } + // test rate change - lower + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.25)); + 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(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD.issue()); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + auto transferRate = escrowRate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.00)); + env.close(); + + // bob can finish escrow - rate changed + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD.issue()) == USD(10125)); + } + // test issuer doesnt pay own rate + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.25)); + 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(); + + // issuer with rate can create escrow + auto const preAlice = env.balance(alice, USD.issue()); + auto const seq1 = env.seq(gw); + auto const delta = USD(125); + env(escrow(gw, alice, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + auto transferRate = escrowRate(env, gw, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // alice can finish escrow - no rate charged + env(finish(alice, gw, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + delta); + BEAST_EXPECT(env.balance(alice, USD.issue()) == USD(10125)); + } + } + + void + testIOUTLLimitAmount(FeatureBitset features) + { + testcase("IOU Trustline Require Auth"); + 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"]; + + // test LimitAmount + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env.close(); + + // alice can create escrow + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // bob can finish + auto const preBobLimit = limitAmount(env, bob, gw, USD); + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + auto const postBobLimit = limitAmount(env, bob, gw, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOUTLRequireAuth(FeatureBitset features) + { + testcase("IOU Trustline 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 create escrow - fails without auth + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10000)), txflags(tfSetfAuth)); + env(trust(bob, USD(10000))); + env.close(); + env(pay(gw, bob, USD(1000))); + env.close(); + + // alice cannot create escrow - bob has auth + seq1 = env.seq(alice); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // bob can finish + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + } + } + + void + testIOUTLFreeze(FeatureBitset features) + { + testcase("IOU 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"]; + + // 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(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob finish escrow fails - frozen trustline + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500), + ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // bob finish escrow success + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + 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(10000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10000), alice, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10000), bob, tfSetFreeze)); + env.close(); + + // bob finish escrow fails - frozen trustline + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500), + ter(tecFROZEN)); + env.close(); + + // clear freeze on bob trustline + env(trust(gw, USD(10000), bob, tfClearFreeze)); + env.close(); + + // bob finish escrow success + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + } + } + void + testIOUTLINSF(FeatureBitset features) + { + testcase("IOU Trustline Insuficient Funds"); + 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"]; + { + // test tecPATH_PARTIAL + // ie. has 10000, escrow 1000 then try to pay 10000 + 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(); + + // create escrow success + auto const delta = USD(1000); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + env(pay(alice, gw, USD(10000)), ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10000 escrow 1000 then try to escrow 10000 + 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(); + + auto const delta = USD(1000); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + env(escrow(alice, bob, USD(10000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + 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"]; + + // test min create precision loss + { + Env env(*this, features); + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow(alice, bob, USD(1)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create escrow for 1000 iou + env(escrow(alice, bob, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // bob finish escrow success + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + } + } + + void + testWithFeats(FeatureBitset features) + { + testEnablement(features); + testTiming(features); + testTags(features); + testDisallowXRP(features); + test1571(features); + testFails(features); + testLockup(features); + testEscrowConditions(features); + testMetaAndOwnership(features); + testConsequences(features); + testEscrowWithTickets(features); + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + testIOUTiming(features); + testIOUTags(features); + testIOUDisallowXRP(features); + testIOU1571(features); + testIOUFails(features); + testIOULockup(features); + testIOUEscrowConditions(features); + testIOUMetaAndOwnership(features); + testIOUConsequences(features); + testIOUEscrowWithTickets(features); + testIOURippleState(features); + testIOUGateway(features); + testIOULockedRate(features); + testIOUTLLimitAmount(features); + testIOUTLRequireAuth(features); + testIOUTLFreeze(features); + testIOUTLINSF(features); + testIOUPrecisionLoss(features); + } + +public: void run() override { - testEnablement(); - testTiming(); - testTags(); - testDisallowXRP(); - test1571(); - testFails(); - testLockup(); - testEscrowConditions(); - testMetaAndOwnership(); - testConsequences(); - testEscrowWithTickets(); + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testWithFeats(all - featurePaychanAndEscrowForTokens); + testWithFeats(all); + testIOUWithFeats(all); } }; diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index a8eae4a25ec..efabe843197 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -59,6 +59,23 @@ struct PayChan_test : public beast::unit_test::suite return sign(pk, sk, msg.slice()); } + static Buffer + signClaimIOUAuth( + PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt) + { + Serializer msg; + serializePayChanAuthorization( + msg, + channel, + authAmt.iou(), + authAmt.getCurrency(), + authAmt.getIssuer()); + return sign(pk, sk, msg.slice()); + } + static STAmount channelAmount(ReadView const& view, uint256 const& chan) { @@ -68,6 +85,42 @@ struct PayChan_test : public beast::unit_test::suite return (*slep)[sfAmount]; } + static Rate + channelRate(ReadView const& view, uint256 const& chan) + { + auto const sle = view.read({ltPAYCHAN, chan}); + if (sle->isFieldPresent(sfTransferRate)) + return ripple::Rate((*sle)[sfTransferRate]); + return Rate{0}; + } + + static STAmount + limitAmount( + jtx::Env const& env, + jtx::Account const& account, + jtx::Account const& gw, + jtx::IOU const& iou) + { + auto const aHigh = account.id() > gw.id(); + auto const sle = env.le(keylet::line(account, gw, iou.currency)); + if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) + return (*sle)[aHigh ? sfLowLimit : sfHighLimit]; + return STAmount(iou, 0); + } + + 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 std::optional channelExpiration(ReadView const& view, uint256 const& chan) { @@ -88,7 +141,6 @@ struct PayChan_test : public beast::unit_test::suite Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); - auto USDA = alice["USD"]; env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; @@ -110,10 +162,7 @@ struct PayChan_test : public beast::unit_test::suite BEAST_EXPECT(chanAmt == XRP(2000)); { - // bad amounts (non-xrp, negative amounts) - env(create(alice, bob, USDA(1000), settleDelay, pk), - ter(temBAD_AMOUNT)); - env(fund(alice, chan, USDA(1000)), ter(temBAD_AMOUNT)); + // bad amounts (negative amounts) env(create(alice, bob, XRP(-1000), settleDelay, pk), ter(temBAD_AMOUNT)); env(fund(alice, chan, XRP(-1000)), ter(temBAD_AMOUNT)); @@ -136,13 +185,9 @@ struct PayChan_test : public beast::unit_test::suite env(create(alice, bob, XRP(10000), settleDelay, pk), ter(tecUNFUNDED)); { - // No signature claim with bad amounts (negative and non-xrp) - auto const iou = USDA(100).value(); + // No signature claim with bad amounts (negative) auto const negXRP = XRP(-100).value(); auto const posXRP = XRP(100).value(); - env(claim(alice, chan, iou, iou), ter(temBAD_AMOUNT)); - env(claim(alice, chan, posXRP, iou), ter(temBAD_AMOUNT)); - env(claim(alice, chan, iou, posXRP), ter(temBAD_AMOUNT)); env(claim(alice, chan, negXRP, negXRP), ter(temBAD_AMOUNT)); env(claim(alice, chan, posXRP, negXRP), ter(temBAD_AMOUNT)); env(claim(alice, chan, negXRP, posXRP), ter(temBAD_AMOUNT)); @@ -495,6 +540,7 @@ struct PayChan_test : public beast::unit_test::suite // receiver can still claim auto const chanBal = channelBalance(*env.current(), chan); auto const chanAmt = channelAmount(*env.current(), chan); + auto preAlice = env.balance(bob); auto preBob = env.balance(bob); auto const delta = XRP(500); auto const reqBal = chanBal + delta; @@ -718,7 +764,6 @@ struct PayChan_test : public beast::unit_test::suite auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); - auto USDA = alice["USD"]; { Env env{*this, features}; env.fund(XRP(10000), alice, bob, carol); @@ -1438,6 +1483,18 @@ struct PayChan_test : public beast::unit_test::suite args.toStyledString())[jss::result]; BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); } + { + // Amount is string. tecSUCCESS + Json::Value args{Json::objectValue}; + args[jss::amount] = "1000"; + args[jss::channel_id] = chan1Str; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + } } } @@ -1501,7 +1558,6 @@ struct PayChan_test : public beast::unit_test::suite Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); - auto USDA = alice["USD"]; env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; @@ -1910,7 +1966,6 @@ struct PayChan_test : public beast::unit_test::suite Env env{*this, features}; auto const alice = Account("alice"); auto const bob = Account("bob"); - auto USDA = alice["USD"]; env.fund(XRP(10000), alice, bob); // alice and bob grab enough tickets for all of the following @@ -2064,29 +2119,3259 @@ struct PayChan_test : public beast::unit_test::suite } void - testWithFeats(FeatureBitset features) + testIOUSimple(FeatureBitset features) { - testSimple(features); - testDisallowIncoming(features); - testCancelAfter(features); - testSettleDelay(features); - testExpiration(features); - testCloseDry(features); - testDefaultAmount(features); - testDisallowXRP(features); - testDstTag(features); - testDepositAuth(features); - testMultiple(features); - testAccountChannelsRPC(features); - testAccountChannelsRPCMarkers(features); - testAccountChannelsRPCSenderOnly(features); - testAccountChannelAuthorize(features); - testAuthVerifyRPC(features); - testOptionalFields(features); - testMalformedPK(features); - testMetaAndOwnership(features); - testAccountDelete(features); - testUsingTickets(features); + testcase("iou simple"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + + auto const feeDrops = env.current()->fees().base; + 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.fund(XRP(10000), alice, bob, carol, 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 preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + BEAST_EXPECT(preAlice - USD(1000)); + { + env(fund(alice, chan, USD(1000))); + } + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + + { + // bad amounts (negative amounts) + env(create(alice, bob, USD(-1000), settleDelay, pk), + ter(temBAD_AMOUNT)); + env(fund(alice, chan, USD(-1000)), ter(temBAD_AMOUNT)); + } + + // invalid account + env(create(alice, "noAccount", USD(1000), settleDelay, pk), + ter(tecNO_DST)); + // can't create channel to the same account + env(create(alice, alice, USD(1000), settleDelay, pk), + ter(temDST_IS_SRC)); + // invalid channel + + env(fund( + alice, + channel(alice, "noAccount", env.seq(alice) - 1), + USD(1000)), + ter(tecNO_ENTRY)); + // not enough funds + env(create(alice, bob, USD(10000), settleDelay, pk), + ter(tecINSUFFICIENT_FUNDS)); + + { + // No signature claim with bad amounts (negative) + auto const negIOU = USD(-100).value(); + auto const posIOU = USD(100).value(); + env(claim(alice, chan, negIOU, negIOU), ter(temBAD_AMOUNT)); + env(claim(alice, chan, posIOU, negIOU), ter(temBAD_AMOUNT)); + env(claim(alice, chan, negIOU, posIOU), ter(temBAD_AMOUNT)); + } + { + // No signature claim more than authorized + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(-100); + assert(reqBal <= chanAmt); + env(claim(alice, chan, reqBal, authAmt), ter(temBAD_AMOUNT)); + } + { + // No signature needed since the owner is claiming + auto const preBob = env.balance(bob, USD.issue()); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + env(claim(alice, chan, reqBal, authAmt)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + chanBal = reqBal; + } + { + // Claim with signature + auto preBob = env.balance(bob, USD.issue()); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + + chanBal = reqBal; + + // claim again + preBob = env.balance(bob, USD.issue()); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ter(tecUNFUNDED_PAYMENT)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob.value()); + } + { + // Try to claim more than authorized + auto const preBob = env.balance(bob, USD.issue()); + STAmount const authAmt = chanBal + USD(500); + STAmount const reqAmt = authAmt + USD(1); + assert(reqAmt <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), + ter(temBAD_AMOUNT)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob.value()); + } + + // Dst tries to fund the channel + env(fund(bob, chan, USD(1000)), ter(tecNO_PERMISSION)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + + { + // Wrong signing key + auto const sig = + signClaimIOUAuth(bob.pk(), bob.sk(), chan, USD(1500)); + env(claim( + bob, + chan, + USD(1500).value(), + USD(1500).value(), + Slice(sig), + bob.pk()), + ter(temBAD_SIGNER)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + } + { + // Bad signature + auto const sig = + signClaimIOUAuth(bob.pk(), bob.sk(), chan, USD(1500)); + env(claim( + bob, + chan, + USD(1500).value(), + USD(1500).value(), + Slice(sig), + alice.pk()), + ter(temBAD_SIGNATURE)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + } + { + // Dst closes channel + auto const preAlice = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob, USD.issue()); + auto const preBobXrp = env.balance(bob); + env(claim(bob, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + auto const delta = chanAmt - chanBal; + assert(delta > beast::zero); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == (preAlice.value()) + delta); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob.value()); + } + } + + void + testIOUCancelAfter(FeatureBitset features) + { + testcase("iou cancel after"); + 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"]; + + { + // If dst claims after cancel after, channel closes + 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 preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + NetClock::time_point const cancelAfter = + env.current()->info().parentCloseTime + 3600s; + auto const channelFunds = USD(1000); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + + env.close(cancelAfter); + { + // dst cannot claim after cancelAfter + auto const chanBal = channelBalance(*env.current(), chan); + auto const chanAmt = channelAmount(*env.current(), chan); + auto preBob = env.balance(bob, USD.issue()); + auto preBobXrp = env.balance(bob); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + // BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + + // delta); + } + } + { + // Third party can close after cancel after + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + env.trust(USD(100000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, carol, USD(10000))); + env.close(); + + auto preAlice = env.balance(alice, USD.issue()); + auto preBob = env.balance(bob, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + NetClock::time_point const cancelAfter = + env.current()->info().parentCloseTime + 3600s; + auto const channelFunds = USD(1000); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + + // third party close before cancelAfter + env(claim(carol, chan), txflags(tfClose), ter(tecNO_PERMISSION)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + env.close(cancelAfter); + // third party close after cancelAfter + env(claim(carol, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice); + } + } + + void + testIOUSettleDelay(FeatureBitset features) + { + testcase("iou settle delay"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + 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 preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + NetClock::time_point const settleTimepoint = + env.current()->info().parentCloseTime + settleDelay; + auto const channelFunds = USD(1000); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + BEAST_EXPECT(channelExists(*env.current(), chan)); + // Owner closes, will close after settleDelay + env(claim(alice, chan), txflags(tfClose)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + env.close(settleTimepoint - settleDelay / 2); + { + // receiver can still claim + auto const chanBal = channelBalance(*env.current(), chan); + auto const chanAmt = channelAmount(*env.current(), chan); + auto preAlice = env.balance(alice, USD.issue()); + auto preBob = env.balance(bob, USD.issue()); + auto preBobXrp = env.balance(bob); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + } + env.close(settleTimepoint); + { + // past settleTime, channel will close + auto const chanBal = channelBalance(*env.current(), chan); + auto const chanAmt = channelAmount(*env.current(), chan); + auto const preAlice = env.balance(alice, USD.issue()); + auto preBob = env.balance(bob, USD.issue()); + auto preBobXRP = env.balance(bob); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(env.balance(bob) == preBobXRP - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + } + } + + void + testIOUExpiration(FeatureBitset features) + { + testcase("iou expiration"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + 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.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + env.trust(USD(100000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, carol, USD(10000))); + env.close(); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const closeTime = env.current()->info().parentCloseTime; + auto const minExpiration = closeTime + settleDelay; + NetClock::time_point const cancelAfter = closeTime + 7200s; + auto const channelFunds = USD(1000); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + BEAST_EXPECT(!channelExpiration(*env.current(), chan)); + // Owner closes, will close after settleDelay + env(claim(alice, chan), txflags(tfClose)); + auto counts = [](auto const& t) { + return t.time_since_epoch().count(); + }; + BEAST_EXPECT( + *channelExpiration(*env.current(), chan) == counts(minExpiration)); + // increase the expiration time + env(fund( + alice, chan, USD(1), NetClock::time_point{minExpiration + 100s})); + BEAST_EXPECT( + *channelExpiration(*env.current(), chan) == + counts(minExpiration) + 100); + // decrease the expiration, but still above minExpiration + env(fund( + alice, chan, USD(1), NetClock::time_point{minExpiration + 50s})); + BEAST_EXPECT( + *channelExpiration(*env.current(), chan) == + counts(minExpiration) + 50); + // decrease the expiration below minExpiration + env(fund( + alice, chan, USD(1), NetClock::time_point{minExpiration - 50s}), + ter(temBAD_EXPIRATION)); + BEAST_EXPECT( + *channelExpiration(*env.current(), chan) == + counts(minExpiration) + 50); + env(claim(bob, chan), txflags(tfRenew), ter(tecNO_PERMISSION)); + BEAST_EXPECT( + *channelExpiration(*env.current(), chan) == + counts(minExpiration) + 50); + env(claim(alice, chan), txflags(tfRenew)); + BEAST_EXPECT(!channelExpiration(*env.current(), chan)); + // decrease the expiration below minExpiration + env(fund( + alice, chan, USD(1), NetClock::time_point{minExpiration - 50s}), + ter(temBAD_EXPIRATION)); + BEAST_EXPECT(!channelExpiration(*env.current(), chan)); + env(fund(alice, chan, USD(1), NetClock::time_point{minExpiration})); + env.close(minExpiration); + // Try to extend the expiration after the expiration has already passed + env(fund( + alice, chan, USD(1), NetClock::time_point{minExpiration + 1000s})); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + } + + void + testIOUCloseDry(FeatureBitset features) + { + testcase("iou close dry"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + 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 preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + BEAST_EXPECT(channelExists(*env.current(), chan)); + // Owner tries to close channel, but it will remain open (settle delay) + env(claim(alice, chan), txflags(tfClose)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + { + // claim the entire amount + auto const preBob = env.balance(bob, USD.issue()); + env(claim(alice, chan, channelFunds.value(), channelFunds.value())); + BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds); + BEAST_EXPECT( + env.balance(bob, USD.issue()) == preBob + channelFunds); + } + // Channel is now dry, can close before expiration date + env(claim(alice, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(alice) == preAliceXrp - (feeDrops * 4)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + } + + void + testIOUDefaultAmount(FeatureBitset features) + { + // auth amount defaults to balance if not present + testcase("iou default amount"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + 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 preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + BEAST_EXPECT(channelExists(*env.current(), chan)); + // Owner tries to close channel, but it will remain open (settle delay) + env(claim(alice, chan), txflags(tfClose)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + { + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const preBob = env.balance(bob, USD.issue()); + auto const preBobXrp = env.balance(bob); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, reqBal); + env(claim(bob, chan, reqBal, std::nullopt, Slice(sig), alice.pk())); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT( + env.balance(bob, USD.issue()).value() == + preBob.value() + delta); + chanBal = reqBal; + } + { + // Claim again + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const preBob = env.balance(bob, USD.issue()); + auto const preBobXrp = env.balance(bob); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, reqBal); + env(claim(bob, chan, reqBal, std::nullopt, Slice(sig), alice.pk())); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(env.balance(alice) == preBobXrp - feeDrops); + BEAST_EXPECT( + env.balance(bob, USD.issue()).value() == + preBob.value() + delta); + chanBal = reqBal; + } + } + + void + testIOUDisallowXRP(FeatureBitset features) + { + // auth amount defaults to balance if not present + testcase("IOU Disallow XRP"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + { + // Ignore the flag since it this is Issued Currency + Env env(*this, supported_amendments() - featureDepositAuth); + 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(); + env(fset(bob, asfDisallowXRP)); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), 3600s, alice.pk())); + BEAST_EXPECT(channelExists(*env.current(), chan)); + } + { + // Ignore the flag since it this is Issued Currency + Env env{*this, features}; + 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(); + env(fset(bob, asfDisallowXRP)); + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), 3600s, alice.pk())); + BEAST_EXPECT(channelExists(*env.current(), chan)); + } + + { + // Ignore the flag since it this is Issued Currency + Env env(*this, supported_amendments() - featureDepositAuth); + 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 chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), 3600s, alice.pk())); + BEAST_EXPECT(channelExists(*env.current(), chan)); + + env(fset(bob, asfDisallowXRP)); + auto const reqBal = USD(500).value(); + env(claim(alice, chan, reqBal, reqBal)); + } + { + // Ignore the flag since it this is Issued Currency + Env env{*this, features}; + 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 chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), 3600s, alice.pk())); + BEAST_EXPECT(channelExists(*env.current(), chan)); + + env(fset(bob, asfDisallowXRP)); + auto const reqBal = USD(500).value(); + env(claim(alice, chan, reqBal, reqBal)); + } + } + + void + testIOUDstTag(FeatureBitset features) + { + // auth amount defaults to balance if not present + testcase("IOU Dst Tag"); + using namespace jtx; + using namespace std::literals::chrono_literals; + // Create a channel where dst disallows XRP + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + 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(); + + env(fset(bob, asfRequireDest)); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + { + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk), + ter(tecDST_TAG_NEEDED)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + } + { + auto const chan = channel(alice, bob, env.seq(alice)); + env(create( + alice, bob, channelFunds, settleDelay, pk, std::nullopt, 1)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + } + } + + void + testIOUDepositAuth(FeatureBitset features) + { + testcase("IOU 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}; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + env.trust(USD(100000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, carol, USD(10000))); + env.close(); + env(fset(bob, asfDepositAuth)); + env.close(); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + + // alice can add more funds to the channel even though bob has + // asfDepositAuth set. + env(fund(alice, chan, USD(1000))); + env.close(); + + // alice claims. Fails because bob's lsfDepositAuth flag is set. + env(claim(alice, chan, USD(500).value(), USD(500).value()), + ter(tecNO_PERMISSION)); + env.close(); + + // Claim with signature + auto const baseFee = env.current()->fees().base; + auto const preBob = env.balance(bob, USD.issue()); + auto const preBobXrp = env.balance(bob); + { + auto const delta = USD(500).value(); + auto const sig = signClaimIOUAuth(pk, alice.sk(), chan, delta); + + // alice claims with signature. Fails since bob has + // lsfDepositAuth flag set. + env(claim(alice, chan, delta, delta, Slice(sig), pk), + ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(env.balance(bob) == preBobXrp); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob.value()); + + // bob claims but omits the signature. Fails because only + // alice can claim without a signature. + env(claim(bob, chan, delta, delta), ter(temBAD_SIGNATURE)); + env.close(); + + // bob claims with signature. Succeeds even though bob's + // lsfDepositAuth flag is set since bob submitted the + // transaction. + env(claim(bob, chan, delta, delta, Slice(sig), pk)); + env.close(); + BEAST_EXPECT(env.balance(bob) == preBobXrp - baseFee); + BEAST_EXPECT( + env.balance(bob, USD.issue()) == preBob.value() + delta); + } + { + // Explore the limits of deposit preauthorization. + auto const delta = USD(600).value(); + auto const sig = signClaimIOUAuth(pk, alice.sk(), chan, delta); + + // carol claims and fails. Only channel participants (bob or + // alice) may claim. + env(claim(carol, chan, delta, delta, Slice(sig), pk), + ter(tecNO_PERMISSION)); + env.close(); + + // bob preauthorizes carol for deposit. But after that carol + // still can't claim since only channel participants may claim. + env(deposit::auth(bob, carol)); + env.close(); + + env(claim(carol, chan, delta, delta, Slice(sig), pk), + ter(tecNO_PERMISSION)); + + // Since alice is not preauthorized she also may not claim + // for bob. + env(claim(alice, chan, delta, delta, Slice(sig), pk), + ter(tecNO_PERMISSION)); + env.close(); + + // However if bob preauthorizes alice for deposit then she can + // successfully submit a claim. + env(deposit::auth(bob, alice)); + env.close(); + + env(claim(alice, chan, delta, delta, Slice(sig), pk)); + env.close(); + + BEAST_EXPECT(env.balance(bob) == preBobXrp - (3 * baseFee)); + BEAST_EXPECT( + env.balance(bob, USD.issue()) == preBob.value() + delta); + } + { + // bob removes preauthorization of alice. Once again she + // cannot submit a claim. + auto const delta = USD(800).value(); + + env(deposit::unauth(bob, alice)); + env.close(); + + // alice claims and fails since she is no longer preauthorized. + env(claim(alice, chan, delta, delta), ter(tecNO_PERMISSION)); + env.close(); + + // bob clears lsfDepositAuth. Now alice can claim. + env(fclear(bob, asfDepositAuth)); + env.close(); + + // alice claims successfully. + env(claim(alice, chan, delta, delta)); + env.close(); + BEAST_EXPECT(env.balance(bob) == preBobXrp - (5 * baseFee)); + BEAST_EXPECT( + env.balance(bob, USD.issue()) == preBob.value() + USD(800)); + } + } + } + + void + testIOUMultiple(FeatureBitset features) + { + // auth amount defaults to balance if not present + testcase("IOU Multiple channels to the same account"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + 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 preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + auto const chan1 = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - channelFunds); + BEAST_EXPECT(channelExists(*env.current(), chan1)); + auto const chan2 = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk)); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice - USD(2000)); + BEAST_EXPECT(channelExists(*env.current(), chan2)); + BEAST_EXPECT(chan1 != chan2); + } + + void + testIOUAccountChannelsRPC(FeatureBitset features) + { + testcase("IOU AccountChannels RPC"); + + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const charlie = Account("charlie", KeyType::ed25519); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, charlie, gw); + env.close(); + env.trust(USD(100000), alice, bob, charlie); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, charlie, USD(10000))); + env.close(); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); + env(create(alice, bob, channelFunds, settleDelay, pk)); + env.close(); + { + auto const r = + env.rpc("account_channels", alice.human(), bob.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); + BEAST_EXPECT(r[jss::result][jss::validated]); + } + { + auto const r = env.rpc("account_channels", alice.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); + BEAST_EXPECT(r[jss::result][jss::validated]); + } + { + auto const r = + env.rpc("account_channels", bob.human(), alice.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); + BEAST_EXPECT(r[jss::result][jss::validated]); + } + auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); + env(create(alice, bob, channelFunds, settleDelay, pk)); + env.close(); + { + auto const r = + env.rpc("account_channels", alice.human(), bob.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 2); + BEAST_EXPECT(r[jss::result][jss::validated]); + BEAST_EXPECT(chan1Str != chan2Str); + for (auto const& c : {chan1Str, chan2Str}) + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == c || + r[jss::result][jss::channels][1u][jss::channel_id] == c); + } + } + + void + testIOUAccountChannelsRPCMarkers(FeatureBitset features) + { + testcase("IOU Account channels RPC markers"); + + using namespace test::jtx; + using namespace std::literals; + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + auto const alice = Account("alice"); + auto const bobs = []() -> std::vector { + int const n = 10; + std::vector r; + r.reserve(n); + for (int i = 0; i < n; ++i) + { + r.emplace_back("bob"s + std::to_string(i)); + } + return r; + }(); + + Env env{*this, features}; + env.fund(XRP(10000), alice, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + for (auto const& a : bobs) + { + env.fund(XRP(10000), a); + env.close(); + env.trust(USD(100000), a); + env.close(); + env(pay(gw, a, USD(10000))); + env.close(); + } + + { + // create a channel from alice to every bob account + auto const settleDelay = 3600s; + auto const channelFunds = USD(1); + for (auto const& b : bobs) + { + env(create(alice, b, channelFunds, settleDelay, alice.pk())); + } + } + + auto testLimit = [](test::jtx::Env& env, + test::jtx::Account const& src, + std::optional limit = std::nullopt, + Json::Value const& marker = Json::nullValue, + std::optional const& dst = + std::nullopt) { + Json::Value jvc; + jvc[jss::account] = src.human(); + if (dst) + jvc[jss::destination_account] = dst->human(); + if (limit) + jvc[jss::limit] = *limit; + if (marker) + jvc[jss::marker] = marker; + + return env.rpc( + "json", "account_channels", to_string(jvc))[jss::result]; + }; + + { + // No marker + auto const r = testLimit(env, alice); + BEAST_EXPECT(r.isMember(jss::channels)); + BEAST_EXPECT(r[jss::channels].size() == bobs.size()); + } + + auto const bobsB58 = [&bobs]() -> std::set { + std::set r; + for (auto const& a : bobs) + r.insert(a.human()); + return r; + }(); + + for (int limit = 1; limit < bobs.size() + 1; ++limit) + { + auto leftToFind = bobsB58; + auto const numFull = bobs.size() / limit; + auto const numNonFull = bobs.size() % limit ? 1 : 0; + + Json::Value marker = Json::nullValue; + + auto const testIt = [&](bool expectMarker, int expectedBatchSize) { + auto const r = testLimit(env, alice, limit, marker); + BEAST_EXPECT(!expectMarker || r.isMember(jss::marker)); + if (r.isMember(jss::marker)) + marker = r[jss::marker]; + BEAST_EXPECT(r[jss::channels].size() == expectedBatchSize); + auto const c = r[jss::channels]; + auto const s = r[jss::channels].size(); + for (int j = 0; j < s; ++j) + { + auto const dstAcc = + c[j][jss::destination_account].asString(); + BEAST_EXPECT(leftToFind.count(dstAcc)); + leftToFind.erase(dstAcc); + } + }; + + for (int i = 0; i < numFull; ++i) + { + bool const expectMarker = (numNonFull != 0 || i < numFull - 1); + testIt(expectMarker, limit); + } + + if (numNonFull) + { + testIt(false, bobs.size() % limit); + } + BEAST_EXPECT(leftToFind.empty()); + } + + { + // degenerate case + auto const r = testLimit(env, alice, 0); + BEAST_EXPECT(r.isMember(jss::error_message)); + } + } + + void + testIOUAccountChannelsRPCSenderOnly(FeatureBitset features) + { + // Check that the account_channels command only returns channels owned + // by the account + testcase("IOU Account channels RPC owner only"); + + 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"]; + + Env env{*this, features}; + 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(); + // Create a channel from alice to bob and from bob to alice + // When retrieving alice's channels, it should only retrieve the + // channels where alice is the source, not the destination + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + env(create(alice, bob, channelFunds, settleDelay, alice.pk())); + env(create(bob, alice, channelFunds, settleDelay, bob.pk())); + + auto const r = [&] { + Json::Value jvc; + jvc[jss::account] = alice.human(); + + return env.rpc( + "json", "account_channels", to_string(jvc))[jss::result]; + }(); + BEAST_EXPECT(r.isMember(jss::channels)); + BEAST_EXPECT(r[jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::channels][0u][jss::destination_account].asString() == + bob.human()); + } + + void + testIOUAuthVerifyRPC(FeatureBitset features) + { + testcase("IOU PayChan Auth/Verify RPC"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const charlie = Account("charlie", KeyType::ed25519); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, charlie, gw); + env.close(); + env.trust(USD(100000), alice, bob, charlie); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, charlie, USD(10000))); + env.close(); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); + env(create(alice, bob, channelFunds, settleDelay, pk)); + env.close(); + std::string chan1PkStr; + { + auto const r = + env.rpc("account_channels", alice.human(), bob.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); + BEAST_EXPECT(r[jss::result][jss::validated]); + chan1PkStr = + r[jss::result][jss::channels][0u][jss::public_key].asString(); + } + { + auto const r = env.rpc("account_channels", alice.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); + BEAST_EXPECT(r[jss::result][jss::validated]); + chan1PkStr = + r[jss::result][jss::channels][0u][jss::public_key].asString(); + } + { + auto const r = + env.rpc("account_channels", bob.human(), alice.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); + BEAST_EXPECT(r[jss::result][jss::validated]); + } + auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); + env(create(alice, bob, channelFunds, settleDelay, pk)); + env.close(); + { + auto const r = + env.rpc("account_channels", alice.human(), bob.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 2); + BEAST_EXPECT(r[jss::result][jss::validated]); + BEAST_EXPECT(chan1Str != chan2Str); + for (auto const& c : {chan1Str, chan2Str}) + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == c || + r[jss::result][jss::channels][1u][jss::channel_id] == c); + } + + auto sliceToHex = [](Slice const& slice) { + std::string s; + s.reserve(2 * slice.size()); + for (int i = 0; i < slice.size(); ++i) + { + s += "0123456789ABCDEF"[((slice[i] & 0xf0) >> 4)]; + s += "0123456789ABCDEF"[((slice[i] & 0x0f) >> 0)]; + } + return s; + }; + + { + // Verify chan1 auth + auto const rs = env.rpc( + "channel_authorize", + "alice", + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + auto const sig = rs[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig.empty()); + { + auto const rv = env.rpc( + "channel_verify", + chan1PkStr, + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool()); + } + + { + // use pk hex to verify + auto const pkAsHex = sliceToHex(pk.slice()); + auto const rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool()); + } + { + // malformed currency + auto const pkAsHex = sliceToHex(pk.slice()); + auto rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1000/x/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + + // malformed issuer + rv = env.rpc( + "channel_verify", pkAsHex, chan1Str, "1000/USD/", sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + + // malformed value + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1000x/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1000 /USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "x1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "x/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + " /USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1000 1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1,000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + " 1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelAmtMalformed"); + } + { + // malformed channel + auto const pkAsHex = sliceToHex(pk.slice()); + auto chan1StrBad = chan1Str; + chan1StrBad.pop_back(); + auto rv = env.rpc( + "channel_verify", + pkAsHex, + chan1StrBad, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelMalformed"); + rv = env.rpc( + "channel_authorize", + "alice", + chan1StrBad, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + BEAST_EXPECT(rv[jss::error] == "channelMalformed"); + + chan1StrBad = chan1Str; + chan1StrBad.push_back('0'); + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1StrBad, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelMalformed"); + rv = env.rpc( + "channel_authorize", + "alice", + chan1StrBad, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + BEAST_EXPECT(rv[jss::error] == "channelMalformed"); + + chan1StrBad = chan1Str; + chan1StrBad.back() = 'x'; + rv = env.rpc( + "channel_verify", + pkAsHex, + chan1StrBad, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT(rv[jss::error] == "channelMalformed"); + rv = env.rpc( + "channel_authorize", + "alice", + chan1StrBad, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + BEAST_EXPECT(rv[jss::error] == "channelMalformed"); + } + { + // give an ill formed base 58 public key + auto illFormedPk = chan1PkStr.substr(0, chan1PkStr.size() - 1); + auto const rv = env.rpc( + "channel_verify", + illFormedPk, + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT( + !rv[jss::result][jss::signature_verified].asBool()); + } + { + // give an ill formed hex public key + auto const pkAsHex = sliceToHex(pk.slice()); + auto illFormedPk = pkAsHex.substr(0, chan1PkStr.size() - 1); + auto const rv = env.rpc( + "channel_verify", + illFormedPk, + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT( + !rv[jss::result][jss::signature_verified].asBool()); + } + } + { + // Try to verify chan2 auth with chan1 key + auto const rs = env.rpc( + "channel_authorize", + "alice", + chan2Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + auto const sig = rs[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig.empty()); + { + auto const rv = env.rpc( + "channel_verify", + chan1PkStr, + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT( + !rv[jss::result][jss::signature_verified].asBool()); + } + { + // use pk hex to verify + auto const pkAsHex = sliceToHex(pk.slice()); + auto const rv = env.rpc( + "channel_verify", + pkAsHex, + chan1Str, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT( + !rv[jss::result][jss::signature_verified].asBool()); + } + } + { + // Try to explicitly specify secp256k1 and Ed25519 keys: + auto const chan = + to_string(channel(charlie, alice, env.seq(charlie))); + env(create( + charlie, alice, channelFunds, settleDelay, charlie.pk())); + env.close(); + + std::string cpk; + { + auto const r = + env.rpc("account_channels", charlie.human(), alice.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan); + BEAST_EXPECT(r[jss::result][jss::validated]); + cpk = r[jss::result][jss::channels][0u][jss::public_key] + .asString(); + } + + // Try to authorize without specifying a key type, expect an + // error: + auto const rs = env.rpc( + "channel_authorize", + "charlie", + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + auto const sig = rs[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig.empty()); + { + auto const rv = env.rpc( + "channel_verify", + cpk, + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig); + BEAST_EXPECT( + !rv[jss::result][jss::signature_verified].asBool()); + } + + // Try to authorize using an unknown key type, except an error: + auto const rs1 = env.rpc( + "channel_authorize", + "charlie", + "nyx", + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + BEAST_EXPECT(rs1[jss::error] == "badKeyType"); + + // Try to authorize using secp256k1; the authorization _should_ + // succeed but the verification should fail: + auto const rs2 = env.rpc( + "channel_authorize", + "charlie", + "secp256k1", + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + auto const sig2 = rs2[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig2.empty()); + { + auto const rv = env.rpc( + "channel_verify", + cpk, + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig2); + BEAST_EXPECT( + !rv[jss::result][jss::signature_verified].asBool()); + } + + // Try to authorize using Ed25519; expect success: + auto const rs3 = env.rpc( + "channel_authorize", + "charlie", + "ed25519", + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"); + auto const sig3 = rs3[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig3.empty()); + { + auto const rv = env.rpc( + "channel_verify", + cpk, + chan, + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + sig3); + BEAST_EXPECT(rv[jss::result][jss::signature_verified].asBool()); + } + } + + { + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = 1000; + args[jss::channel_id] = chan1Str; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + + // send malformed amounts rpc requests + args[jss::amount][jss::value] = "1000x"; + auto rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); + args[jss::amount][jss::value] = "x1000"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); + args[jss::amount][jss::value] = "x"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); + { + // Missing channel_id + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = 1000; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "invalidParams"); + } + { + // Missing amount + Json::Value args{Json::objectValue}; + args[jss::channel_id] = chan1Str; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "invalidParams"); + } + { + // Missing key_type and no secret. + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = 1000; + args[jss::channel_id] = chan1Str; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "invalidParams"); + } + { + // Both passphrase and seed specified. + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = 1000; + args[jss::channel_id] = chan1Str; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + args[jss::seed] = "seed can be anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "invalidParams"); + } + { + // channel_id is not exact hex. + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = 1000; + args[jss::channel_id] = chan1Str + "1"; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "channelMalformed"); + } + { + // Amount is not a decimal string. + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = "TwoThousand"; + args[jss::channel_id] = chan1Str; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + BEAST_EXPECT(rs[jss::error] == "channelAmtMalformed"); + } + { + // Amount is object. tecSUCCESS + Json::Value args{Json::objectValue}; + args[jss::amount][jss::currency] = "USD"; + args[jss::amount][jss::issuer] = gw.human(); + args[jss::amount][jss::value] = 1000; + args[jss::channel_id] = chan1Str; + args[jss::key_type] = "secp256k1"; + args[jss::passphrase] = "passphrase_can_be_anything"; + rs = env.rpc( + "json", + "channel_authorize", + args.toStyledString())[jss::result]; + } + } + } + + void + testIOUOptionalFields(FeatureBitset features) + { + testcase("IOU Optional Fields"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dan = Account("dan"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dan, gw); + env.close(); + env.trust(USD(100000), alice, bob, carol, dan); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, carol, USD(10000))); + env(pay(gw, dan, USD(10000))); + env.close(); + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = USD(1000); + + std::optional cancelAfter; + + { + auto const chan = to_string(channel(alice, bob, env.seq(alice))); + env(create(alice, bob, channelFunds, settleDelay, pk)); + auto const r = + env.rpc("account_channels", alice.human(), bob.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan); + BEAST_EXPECT(!r[jss::result][jss::channels][0u].isMember( + jss::destination_tag)); + } + { + std::uint32_t dstTag = 42; + auto const chan = to_string(channel(alice, carol, env.seq(alice))); + env(create( + alice, + carol, + channelFunds, + settleDelay, + pk, + cancelAfter, + dstTag)); + auto const r = + env.rpc("account_channels", alice.human(), carol.human()); + BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::channel_id] == chan); + BEAST_EXPECT( + r[jss::result][jss::channels][0u][jss::destination_tag] == + dstTag); + } + } + + void + testIOUMalformedPK(FeatureBitset features) + { + testcase("iou malformed pk"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + 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 pk = alice.pk(); + auto const settleDelay = 100s; + + auto const chan = channel(alice, bob, env.seq(alice)); + auto jv = create(alice, bob, USD(1000), settleDelay, pk); + auto const pkHex = strHex(pk.slice()); + jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); + env(jv, ter(temMALFORMED)); + jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2); + env(jv, ter(temMALFORMED)); + auto badPrefix = pkHex; + badPrefix[0] = 'f'; + badPrefix[1] = 'f'; + jv["PublicKey"] = badPrefix; + env(jv, ter(temMALFORMED)); + + jv["PublicKey"] = pkHex; + env(jv); + + auto const authAmt = USD(100); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + jv = claim( + bob, + chan, + authAmt.value(), + authAmt.value(), + Slice(sig), + alice.pk()); + jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); + env(jv, ter(temMALFORMED)); + jv["PublicKey"] = pkHex.substr(0, pkHex.size() - 2); + env(jv, ter(temMALFORMED)); + badPrefix = pkHex; + badPrefix[0] = 'f'; + badPrefix[1] = 'f'; + jv["PublicKey"] = badPrefix; + env(jv, ter(temMALFORMED)); + + // missing public key + jv.removeMember("PublicKey"); + env(jv, ter(temMALFORMED)); + + { + auto const txn = R"*( + { + + "channel_id":"5DB01B7FFED6B67E6B0414DED11E051D2EE2B7619CE0EAA6286D67A3A4D5BDB3", + "signature": + "304402204EF0AFB78AC23ED1C472E74F4299C0C21F1B21D07EFC0A3838A420F76D783A400220154FB11B6F54320666E4C36CA7F686C16A3A0456800BBC43746F34AF50290064", + "public_key": + "aKijDDiC2q2gXjMpM7i4BUS6cmixgsEe18e7CjsUxwihKfuoFgS5", + "amount": "1000000" + } + )*"; + auto const r = env.rpc("json", "channel_verify", txn); + BEAST_EXPECT(r["result"]["error"] == "publicMalformed"); + } + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + testcase("IOU Metadata & Ownership"); + + using namespace jtx; + using namespace std::literals::chrono_literals; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const settleDelay = 100s; + auto const pk = alice.pk(); + + auto inOwnerDir = [](ReadView const& view, + Account const& acc, + std::shared_ptr const& chan) -> bool { + ripple::Dir const ownerDir(view, keylet::ownerDir(acc.id())); + return std::find(ownerDir.begin(), ownerDir.end(), chan) != + ownerDir.end(); + }; + + auto ownerDirCount = [](ReadView const& view, + Account const& acc) -> std::size_t { + ripple::Dir const ownerDir(view, keylet::ownerDir(acc.id())); + return std::distance(ownerDir.begin(), ownerDir.end()); + }; + + { + // Test without adding the paychan to the recipient's owner + // directory + Env env( + *this, supported_amendments() - fixPayChanRecipientOwnerDir); + 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(); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + auto const [chan, chanSle] = + channelKeyAndSle(*env.current(), alice, bob); + BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); + BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + + // close the channel + env(claim(bob, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + } + + { + // Test with adding the paychan to the recipient's owner directory + Env env{*this, features}; + 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(); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + auto const [chan, chanSle] = + channelKeyAndSle(*env.current(), alice, bob); + BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); + BEAST_EXPECT(inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2); + // close the channel + env(claim(bob, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + } + + { + // Test removing paychans created before adding to the recipient's + // owner directory + Env env( + *this, supported_amendments() - fixPayChanRecipientOwnerDir); + 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(); + // create the channel before the amendment activates + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + auto const [chan, chanSle] = + channelKeyAndSle(*env.current(), alice, bob); + BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); + BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + env.enableFeature(fixPayChanRecipientOwnerDir); + env.close(); + BEAST_EXPECT( + env.current()->rules().enabled(fixPayChanRecipientOwnerDir)); + // These checks look redundant, but if you don't `close` after the + // `create` these checks will fail. I believe this is due to the + // create running with one set of amendments initially, then with a + // different set with the ledger closes (tho I haven't dug into it) + BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + + // close the channel after the amendment activates + env(claim(bob, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + BEAST_EXPECT(!inOwnerDir(*env.current(), alice, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1); + BEAST_EXPECT(!inOwnerDir(*env.current(), bob, chanSle)); + BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1); + } + } + + void + testIOUAccountDelete(FeatureBitset features) + { + testcase("IOU Account Delete"); + using namespace test::jtx; + using namespace std::literals::chrono_literals; + auto rmAccount = [this]( + Env& env, + Account const& toRm, + Account const& dst, + TER expectedTer = tesSUCCESS) { + // only allow an account to be deleted if the account's sequence + // number is at least 256 less than the current ledger sequence + for (auto minRmSeq = env.seq(toRm) + 257; + env.current()->seq() < minRmSeq; + env.close()) + { + } + + env(acctdelete(toRm, dst), + fee(drops(env.current()->fees().increment)), + ter(expectedTer)); + env.close(); + this->BEAST_EXPECT( + isTesSuccess(expectedTer) == + !env.closed()->exists(keylet::account(toRm.id()))); + }; + + 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"]; + + for (bool const withOwnerDirFix : {false, true}) + { + auto const amd = withOwnerDirFix + ? supported_amendments() + : supported_amendments() - fixPayChanRecipientOwnerDir; + Env env{*this, amd}; + + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + env.trust(USD(100000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, carol, USD(10000))); + env.close(); + + auto const feeDrops = env.current()->fees().base; + + // Create a channel from alice to bob + auto const preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + + rmAccount(env, alice, carol, tecHAS_OBLIGATIONS); + // can only remove bob if the channel isn't in their owner direcotry + + if (!withOwnerDirFix) + env.trust(USD(0), bob); + env.close(); + env(pay(bob, gw, USD(10000))); + env.close(); + + rmAccount( + env, + bob, + carol, + withOwnerDirFix ? TER(tecHAS_OBLIGATIONS) : TER(tesSUCCESS)); + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + + auto preBob = env.balance(bob, USD.issue()); + auto preBobXrp = env.balance(bob); + auto const delta = USD(50); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + + // claim should fail if the dst was removed + if (withOwnerDirFix) + { + env(claim(alice, chan, reqBal, authAmt)); + env.close(); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + chanBal = reqBal; + } + else + { + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(claim(alice, chan, reqBal, authAmt), ter(tecNO_DST)); + env.close(); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob.value()); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice); + } + + // fund should fail if the dst was removed + if (withOwnerDirFix) + { + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(fund(alice, chan, USD(1000))); + env.close(); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + BEAST_EXPECT( + channelAmount(*env.current(), chan) == chanAmt + USD(1000)); + chanAmt = chanAmt + USD(1000); + } + else + { + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(fund(alice, chan, USD(1000)), ter(tecNO_DST)); + env.close(); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + } + + { + // Owner closes, will close after settleDelay + env(claim(alice, chan), txflags(tfClose)); + env.close(); + // settle delay hasn't ellapsed. Channels should exist. + BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const closeTime = env.current()->info().parentCloseTime; + auto const minExpiration = closeTime + settleDelay; + env.close(minExpiration); + env(claim(alice, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + } + } + + { + // test resurrected account + Env env{ + *this, supported_amendments() - fixPayChanRecipientOwnerDir}; + + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + env.trust(USD(100000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, carol, USD(10000))); + env.close(); + + auto const feeDrops = env.current()->fees().base; + + // Create a channel from alice to bob + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + + // Since `fixPayChanRecipientOwnerDir` is not active, can remove bob + env.trust(USD(0), bob); + env.close(); + env(pay(bob, gw, USD(10000))); + env.close(); + rmAccount(env, bob, carol); + BEAST_EXPECT(!env.closed()->exists(keylet::account(bob.id()))); + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + auto preBob = env.balance(bob, USD.issue()); + auto preBobXrp = env.balance(bob); + auto const delta = USD(50); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + + { + // claim should fail, since bob doesn't exist + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(claim(alice, chan, reqBal, authAmt), ter(tecNO_DST)); + env.close(); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob.value()); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice.value()); + } + + { + // fund should fail, sincebob doesn't exist + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(fund(alice, chan, USD(1000)), ter(tecNO_DST)); + env.close(); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice.value()); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + } + + // resurrect bob + env(pay(alice, bob, XRP(20))); + env.close(); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, bob, USD(10000))); + env.close(); + BEAST_EXPECT(env.closed()->exists(keylet::account(bob.id()))); + { + // alice should be able to claim + preBob = env.balance(bob, USD.issue()); + preBobXrp = env.balance(bob); + reqBal = chanBal + delta; + authAmt = reqBal + USD(100); + env(claim(alice, chan, reqBal, authAmt)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + chanBal = reqBal; + } + + { + // bob should be able to claim + preBob = env.balance(bob, USD.issue()); + preBobXrp = env.balance(bob); + reqBal = chanBal + delta; + authAmt = reqBal + USD(100); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + chanBal = reqBal; + } + + { + // alice should be able to fund + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(fund(alice, chan, USD(1000))); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + BEAST_EXPECT( + channelAmount(*env.current(), chan) == chanAmt + USD(1000)); + chanAmt = chanAmt + USD(1000); + } + + { + // Owner closes, will close after settleDelay + env(claim(alice, chan), txflags(tfClose)); + env.close(); + // settle delay hasn't ellapsed. Channels should exist. + BEAST_EXPECT(channelExists(*env.current(), chan)); + auto const closeTime = env.current()->info().parentCloseTime; + auto const minExpiration = closeTime + settleDelay; + env.close(minExpiration); + env(claim(alice, chan), txflags(tfClose)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + } + } + } + + void + testIOUUsingTickets(FeatureBitset features) + { + testcase("iou using tickets"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + 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(); + // alice and bob grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // and bob's account sequence numbers should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + std::uint32_t const aliceSeq{env.seq(alice)}; + + std::uint32_t bobTicketSeq{env.seq(bob) + 1}; + env(ticket::create(bob, 10)); + std::uint32_t const bobSeq{env.seq(bob)}; + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, aliceTicketSeq); + + env(create(alice, bob, USD(1000), settleDelay, pk), + ticket::use(aliceTicketSeq++)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + + { + auto const preAlice = env.balance(alice, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(fund(alice, chan, USD(1000)), ticket::use(aliceTicketSeq++)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + } + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(2000)); + + { + // No signature needed since the owner is claiming + auto const preBob = env.balance(bob, USD.issue()); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + env(claim(alice, chan, reqBal, authAmt), + ticket::use(aliceTicketSeq++)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + chanBal = reqBal; + } + { + // Claim with signature + auto preBob = env.balance(bob, USD.issue()); + auto preBobXrp = env.balance(bob); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ticket::use(bobTicketSeq++)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + chanBal = reqBal; + + // claim again + preBob = env.balance(bob, USD.issue()); + preBobXrp = env.balance(bob); + // A transaction that generates a tec still consumes its ticket. + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ticket::use(bobTicketSeq++), + ter(tecUNFUNDED_PAYMENT)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + } + { + // Try to claim more than authorized + auto const preBob = env.balance(bob, USD.issue()); + STAmount const authAmt = chanBal + USD(500); + STAmount const reqAmt = authAmt + USD(1); + assert(reqAmt <= chanAmt); + // Note that since claim() returns a tem (neither tec nor tes), + // the ticket is not consumed. So we don't increment bobTicket. + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), + ticket::use(bobTicketSeq), + ter(temBAD_AMOUNT)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + } + + // Dst tries to fund the channel + env(fund(bob, chan, USD(1000)), + ticket::use(bobTicketSeq++), + ter(tecNO_PERMISSION)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + { + // Dst closes channel + auto const preBob = env.balance(bob, USD.issue()); + auto const preBobXrp = env.balance(bob); + env(claim(bob, chan), + txflags(tfClose), + ticket::use(bobTicketSeq++)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(!channelExists(*env.current(), chan)); + auto const feeDrops = env.current()->fees().base; + auto const delta = chanAmt - chanBal; + assert(delta > beast::zero); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + } + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + } + + void + testIOUAutoTL(FeatureBitset features) + { + testcase("IOU Auto Trust Line"); + 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"]; + + { + // test trust line missing + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + auto const feeDrops = env.current()->fees().base; + // Create a channel from alice to bob + auto const preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + auto preBob = env.balance(bob); + auto const delta = USD(50); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + { + // claim should fail, since bob doesn't have tl + // and alice cannot create tl for bob + auto const preBob = env.balance(bob, USD.issue()); + auto const preAliceXrp = env.balance(alice); + env(claim(alice, chan, reqBal, authAmt), ter(tecNO_LINE)); + env.close(); + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + BEAST_EXPECT(env.balance(alice) == preAliceXrp - feeDrops); + } + { + // claim should succeed, since the tl will auto add + auto const preAlice = env.balance(alice, USD.issue()); + auto const preBob = env.balance(bob, USD.issue()); + auto const preBobXrp = env.balance(bob); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + env.close(); + BEAST_EXPECT(preBob == USD(0)); + BEAST_EXPECT(env.balance(bob) == preBobXrp - feeDrops); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob + delta); + } + } + } + + void + testIOURippleState(FeatureBitset features) + { + testcase("IOU RippleState"); + using namespace test::jtx; + using namespace std::literals; + + // + // USE balance(env, ...) over env.balance(...) + // I did this to check the exact sign "-/+" + // + + struct TestAccountData + { + Account src; + Account dst; + Account gw; + bool hasTrustline; + bool negative; + }; + + std::array tests = {{ + // src > dst && src > issuer && dst no trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true}, + // src < dst && src < issuer && dst no trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false}, + // // dst > src && dst > issuer && dst no trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true}, + // // dst < src && dst < issuer && dst no trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false}, + // // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true}, + // // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false}, + // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true}, + // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100000), t.src, t.dst); + else + env.trust(USD(100000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10000))); + env.close(); + + // src can create paychan + auto const preSrc = lineBalance(env, t.src, t.gw, USD); + auto const preDst = lineBalance(env, t.dst, t.gw, USD); + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + auto const chan = channel(t.src, t.dst, env.seq(t.src)); + env(create(t.src, t.dst, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT( + lineBalance(env, t.src, t.gw, USD) == + (t.negative ? (preSrc + USD(1000)) : (preSrc - USD(1000)))); + + // dst can claim paychan + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = USD(1000); + auto const sig = + signClaimIOUAuth(t.src.pk(), t.src.sk(), chan, authAmt); + env(claim(t.dst, chan, reqBal, authAmt, Slice(sig), t.src.pk())); + env.close(); + + BEAST_EXPECT( + lineBalance(env, t.dst, t.gw, USD) == + (t.negative ? (preDst - delta) : (preDst + delta))); + // src claim fails because trust limit is 0 + auto const testResult = + t.hasTrustline ? ter(tesSUCCESS) : ter(tecPATH_DRY); + env(claim(t.src, chan, authAmt, authAmt), testResult); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU 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(); + + // issuer can create paychan + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + auto const chan = channel(t.src, t.dst, env.seq(t.src)); + env(create(t.src, t.dst, USD(1000), settleDelay, pk)); + env.close(); + + // src can claim paychan without trustline + auto const preDst = lineBalance(env, t.dst, t.src, USD); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + auto const sig = + signClaimIOUAuth(t.src.pk(), t.src.sk(), chan, authAmt); + env(claim(t.dst, chan, reqBal, authAmt, Slice(sig), t.src.pk())); + env.close(); + auto const preAmount = t.hasTrustline ? 10000 : 0; + BEAST_EXPECT( + preDst == (t.negative ? -USD(preAmount) : USD(preAmount))); + auto const postAmount = t.hasTrustline ? 10500 : 500; + 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("carol0"), Account{"gw1"}, true, false}, + // // // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true, true}, + // // // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true, 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(); + + env.trust(USD(100000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10000))); + env.close(); + + // src can create paychan to dst/issuer + auto const preSrc = lineBalance(env, t.src, t.dst, USD); + auto const pk = t.src.pk(); + auto const settleDelay = 100s; + auto const chan = channel(t.src, t.dst, env.seq(t.src)); + env(create(t.src, t.dst, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT( + lineBalance(env, t.src, t.dst, USD) == + preSrc - (t.negative ? -USD(1000) : USD(1000))); + + // dst/gw can claim paychan + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + auto const sig = + signClaimIOUAuth(t.src.pk(), t.src.sk(), chan, authAmt); + env(claim(t.dst, chan, reqBal, authAmt, Slice(sig), t.src.pk())); + env.close(); + + auto const postAmount = 9000; + BEAST_EXPECT( + lineBalance(env, t.dst, t.src, USD) == + (t.negative ? -USD(postAmount) : USD(postAmount))); + } + } + + void + testIOUTransferRate(FeatureBitset features) + { + testcase("IOU 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"]; + + // test rate + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(rate(gw, 1.25)); + 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(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + auto const transferRate = channelRate(*env.current(), chan); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + + // alice can claim paychan + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(125); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(500); + env(claim(alice, chan, reqBal, authAmt)); + env.close(); + BEAST_EXPECT(env.balance(bob, USD.issue()) == USD(10100)); + } + // 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); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // alice can create paychan w/ xfer rate + auto const preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + auto transferRate = channelRate(*env.current(), chan); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(100); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(500); + + // alice can fund paychan + env(fund(alice, chan, USD(1000))); + env.close(); + transferRate = channelRate(*env.current(), chan); + // no change in rate + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(2000)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // alice can fund after issuer rate change + env(fund(alice, chan, USD(1000))); + env.close(); + transferRate = channelRate(*env.current(), chan); + // rate changed to new lower rate + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.00)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(3000)); + + // issuer changes rate higher + env(rate(gw, 1.01)); + env.close(); + + // alice cant fund after issuer rate change + env(fund(alice, chan, USD(1000)), ter(temBAD_TRANSFER_RATE)); + env.close(); + transferRate = channelRate(*env.current(), chan); + // issuer rate stays at lower rate + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.00)); + } + // 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(); + + // issuer with rate can create paychan + auto const pk = gw.pk(); + auto const settleDelay = 100s; + auto const chan = channel(gw, alice, env.seq(gw)); + env(create(gw, alice, USD(1000), settleDelay, pk)); + env.close(); + + // issuer can claim paychan, alice has trustline + auto const preAlice = env.balance(alice, USD.issue()); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + USD(100); + env(claim(gw, chan, reqBal, authAmt)); + env.close(); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + delta); + } + } + + void + testIOUTLLimitAmount(FeatureBitset features) + { + testcase("IOU Trustline Limit Amount"); + 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"]; + + // test LimitAmount + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice); + env.trust(USD(1000), bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT(channelBalance(*env.current(), chan) == USD(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == USD(1000)); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + auto preBob = env.balance(bob); + auto const delta = USD(50); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + // alice cannot claim because bobs amount would be > than limit + env(claim(alice, chan, reqBal, authAmt), ter(tecPATH_DRY)); + + // bob can claim, increasing the limit amount + auto const preBobLimit = limitAmount(env, bob, gw, USD); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + env.close(); + auto const postBobLimit = limitAmount(env, bob, gw, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOUTLRequireAuth(FeatureBitset features) + { + testcase("IOU Trustline 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(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto chan = channel(alice, bob, env.seq(alice)); + // alice cannot create because bob's trustline is not authorized + // all parties must be authorized + env(create(alice, bob, USD(1000), settleDelay, pk), + 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 create because bob's trustline is authorized + chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(50); + auto const reqBal = delta; + auto const authAmt = reqBal + USD(100); + // alice can claim + env(claim(alice, chan, reqBal, authAmt)); + env.close(); + + // bob can claim + chanBal = channelBalance(*env.current(), chan); + chanAmt = channelAmount(*env.current(), chan); + auto const newReqBal = chanBal + delta; + auto const newAuthAmt = newReqBal + USD(100); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, newAuthAmt); + env(claim( + bob, chan, newReqBal, newAuthAmt, Slice(sig), alice.pk())); + env.close(); + } + } + + void + testIOUTLFreeze(FeatureBitset features) + { + testcase("IOU 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(); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk), ter(tecFROZEN)); + env.close(); + env(fclear(gw, asfGlobalFreeze)); + env.close(); + chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(10); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + env(claim(alice, chan, reqBal, authAmt), ter(tecFROZEN)); + auto sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ter(tecFROZEN)); + env.close(); + env(fclear(gw, asfGlobalFreeze)); + env.close(); + env(claim(alice, chan, reqBal, authAmt)); + env.close(); + chanBal = channelBalance(*env.current(), chan); + chanAmt = channelAmount(*env.current(), chan); + reqBal = chanBal + delta; + authAmt = reqBal + USD(100); + sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + 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(); + + // setup transaction + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto chan = channel(alice, bob, env.seq(alice)); + + // create paychan fails - frozen trustline + env(create(alice, bob, USD(1000), settleDelay, pk), ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(100000), alice, tfClearFreeze)); + env.close(); + + // create paychan success + chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(100000), alice, tfSetFreeze)); + env.close(); + + // paychan fields + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(10); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + + // alice claim paychan fails - frozen trustline + env(claim(alice, chan, reqBal, authAmt), ter(tecFROZEN)); + + // bob claim paychan fails - frozen trustline + auto sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ter(tecFROZEN)); + env.close(); + + // clear freeze on bob trustline + env(trust(gw, USD(100000), bob, tfClearFreeze)); + // clear freeze on alice trustline + env(trust(gw, USD(100000), alice, tfClearFreeze)); + env.close(); + + // alice claim paychan success + env(claim(alice, chan, reqBal, authAmt)); + env.close(); + + // paychan fields + chanBal = channelBalance(*env.current(), chan); + chanAmt = channelAmount(*env.current(), chan); + reqBal = chanBal + delta; + authAmt = reqBal + USD(100); + + // bob claim paychan success + sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + env.close(); + } + } + + void + testIOUTLINSF(FeatureBitset features) + { + testcase("IOU Trustline Insuficient Funds"); + 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 pay more than escrow amount + // ie. has 10000, escrow 1000 then try to pay 10000 + 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(); + auto preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + auto preBob = env.balance(bob, USD.issue()); + auto const delta = USD(50); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + env(pay(alice, gw, USD(10000)), ter(tecPATH_PARTIAL)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + } + { + // test escrow more than balance + escrowed + // ie. has 10000 escrow 1000 then try to escrow 10000 + 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(); + auto preAlice = env.balance(alice, USD.issue()); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAlice - USD(1000)); + env.close(); + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == USD(0)); + BEAST_EXPECT(chanAmt == USD(1000)); + auto preBob = env.balance(bob, USD.issue()); + auto const delta = USD(50); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + assert(reqBal <= chanAmt); + env(create(alice, bob, USD(10000), settleDelay, pk), + ter(tecINSUFFICIENT_FUNDS)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob); + } + } + + void + testIOUMismatchFunding(FeatureBitset features) + { + testcase("IOU Mismatch Funding"); + 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"]; + auto const EUR = gw["EUR"]; + + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.trust(EUR(100000), alice); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env(pay(gw, alice, EUR(10000))); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + env(fund(alice, chan, EUR(1000)), ter(temBAD_CURRENCY)); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + 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"]; + // test min create precision loss + { + Env env(*this, features); + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000000000000000000), alice); + env.trust(USD(1000000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(100000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + auto const pk = alice.pk(); + auto const settleDelay = 100s; + // alice cannot create paychan for 1/10 iou - precision loss + env(create(alice, bob, USD(1), settleDelay, pk), + ter(tecPRECISION_LOSS)); + env.close(); + + // alice can create paychan for 100 iou + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, USD(100), settleDelay, pk)); + env.close(); + + auto const chanBal = channelBalance(*env.current(), chan); + auto const chanAmt = channelAmount(*env.current(), chan); + + auto const reqBal = USD(100); + auto const sig = + signClaimIOUAuth(alice.pk(), alice.sk(), chan, reqBal); + env(claim(bob, chan, reqBal, reqBal, Slice(sig), alice.pk())); + } + } + + void + testWithFeats(FeatureBitset features) + { + testSimple(features); + testCancelAfter(features); + testSettleDelay(features); + testExpiration(features); + testCloseDry(features); + testDefaultAmount(features); + testDisallowXRP(features); + testDstTag(features); + testDepositAuth(features); + testMultiple(features); + testAccountChannelsRPC(features); + testAccountChannelsRPCMarkers(features); + testAccountChannelsRPCSenderOnly(features); + testAccountChannelAuthorize(features); + testAuthVerifyRPC(features); + testOptionalFields(features); + testMalformedPK(features); + testMetaAndOwnership(features); + testAccountDelete(features); + testUsingTickets(features); + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUSimple(features); + testIOUCancelAfter(features); + testIOUSettleDelay(features); + testIOUExpiration(features); + testIOUCloseDry(features); + testIOUDefaultAmount(features); + testIOUDisallowXRP(features); + testIOUDstTag(features); + testIOUDepositAuth(features); + testIOUMultiple(features); + testIOUAccountChannelsRPC(features); + testIOUAccountChannelsRPCMarkers(features); + testIOUAccountChannelsRPCSenderOnly(features); + testIOUAuthVerifyRPC(features); + testIOUOptionalFields(features); + testIOUMalformedPK(features); + testIOUMetaAndOwnership(features); + testIOUAccountDelete(features); + testIOUUsingTickets(features); + testIOUAutoTL(features); + testIOURippleState(features); + testIOUGateway(features); + testIOUTransferRate(features); + testIOUTLLimitAmount(features); + testIOUTLRequireAuth(features); + testIOUTLFreeze(features); + testIOUTLINSF(features); + testIOUMismatchFunding(features); + testIOUPrecisionLoss(features); } public: @@ -2096,7 +5381,11 @@ struct PayChan_test : public beast::unit_test::suite using namespace test::jtx; FeatureBitset const all{supported_amendments()}; testWithFeats(all - disallowIncoming); + testWithFeats( + all - disallowIncoming - featurePaychanAndEscrowForTokens); testWithFeats(all); + testIOUWithFeats(all - disallowIncoming); + testIOUWithFeats(all); } }; diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 66523700a88..641094b4f08 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -550,21 +550,6 @@ class Invariants_test : public beast::unit_test::suite using namespace test::jtx; testcase << "no zero escrow"; - doInvariantCheck( - {{"Cannot return non-native STAmount as XRPAmount"}}, - [](Account const& A1, Account const& A2, ApplyContext& ac) { - // escrow with nonnative amount - auto const sle = ac.view().peek(keylet::account(A1.id())); - if (!sle) - return false; - auto sleNew = std::make_shared( - keylet::escrow(A1, (*sle)[sfSequence] + 2)); - STAmount nonNative(A2["USD"](51)); - sleNew->setFieldAmount(sfAmount, nonNative); - ac.view().insert(sleNew); - return true; - }); - doInvariantCheck( {{"XRP net change of -1000000 doesn't match fee 0"}, {"escrow specifies invalid amount"}}, diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index f3aaf468a9e..c1b87add4f8 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -1954,6 +1954,28 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, + {"channel_authorize: ic.", + __LINE__, + {"channel_authorize", + "secret_can_be_anything", + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA"}, + RPCCallTestData::no_exception, + R"({ + "method" : "channel_authorize", + "params" : [ + { + "api_version" : %API_VER%, + "amount" : { + "currency" : "USD", + "issuer" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "value" : "1000" + }, + "channel_id" : "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", + "secret" : "secret_can_be_anything" + } + ] + })"}, {"channel_authorize: too few arguments.", __LINE__, { @@ -2117,6 +2139,30 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, + {"channel_verify: ic public key.", + __LINE__, + {"channel_verify", + "aB4BXXLuPu8DpVuyq1DBiu3SrPdtK9AYZisKhu8mvkoiUD8J9Gov", + "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", + "1000/USD/rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "DEADBEEF"}, + RPCCallTestData::no_exception, + R"({ + "method" : "channel_verify", + "params" : [ + { + "api_version" : %API_VER%, + "amount" : { + "currency" : "USD", + "issuer" : "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "value" : "1000" + }, + "channel_id" : "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF", + "public_key" : "aB4BXXLuPu8DpVuyq1DBiu3SrPdtK9AYZisKhu8mvkoiUD8J9Gov", + "signature" : "DEADBEEF" + } + ] + })"}, {"channel_verify: public key hex.", __LINE__, {"channel_verify", diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index e34b675998d..69f1301b132 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -93,7 +93,8 @@ after(NetClock::time_point now, std::uint32_t mark) TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; } NotTEC @@ -105,8 +106,18 @@ EscrowCreate::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount])) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featurePaychanAndEscrowForTokens)) + return temBAD_AMOUNT; + + if (!isLegalNet(amount)) + return temBAD_AMOUNT; + + if (isFakeXRP(amount)) + return temBAD_CURRENCY; + } if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; @@ -206,21 +217,74 @@ EscrowCreate::doApply() } auto const account = ctx_.tx[sfAccount]; - auto const sle = ctx_.view().peek(keylet::account(account)); + auto sle = ctx_.view().peek(keylet::account(account)); if (!sle) return tefINTERNAL; - // Check reserve and funds availability - { - auto const balance = STAmount((*sle)[sfBalance]).xrp(); - auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + STAmount const amount{ctx_.tx[sfAmount]}; + + std::shared_ptr sleLine; + + auto const balance = STAmount((*sle)[sfBalance]).xrp(); + auto const reserve = + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + bool const isIssuer = amount.getIssuer() == account; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + // Check reserve and funds availability + if (isXRP(amount)) + { if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp()) return tecUNFUNDED; + // pass + } + else + { + // preflight will prevent this ever firing, included + // defensively for completeness + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + TER const result = trustTransferAllowed( + ctx_.view(), + {account, ctx_.tx[sfDestination]}, + amount.issue(), + ctx_.journal); + + JLOG(ctx_.journal.trace()) + << "EscrowCreate::doApply trustTransferAllowed result=" << result; + + if (!isTesSuccess(result)) + return result; + + // issuer does not need to lock anything + if (!isIssuer) + { + // perform the lock as a dry run before + // we modify anything on-ledger + sleLine = ctx_.view().peek(keylet::line( + account, amount.getIssuer(), amount.getCurrency())); + + // check if the escrow is capable of being + // finished before we allow it to be created + if (!sleLine) + return tecNO_LINE; + + { + TER const result = transferToEntry( + ctx_.view(), sle, sleLine, amount, ctx_.journal, DryRun); + + JLOG(ctx_.journal.trace()) + << "EscrowCreate::doApply transferToEntry (dry) " + "result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + } } // Check destination account @@ -235,7 +299,7 @@ EscrowCreate::doApply() // Obeying the lsfDissalowXRP flag was a bug. Piggyback on // featureDepositAuth to remove the bug. - if (!ctx_.view().rules().enabled(featureDepositAuth) && + if (!ctx_.view().rules().enabled(featureDepositAuth) && isXRP(amount) && ((*sled)[sfFlags] & lsfDisallowXRP)) return tecNO_TARGET; } @@ -254,6 +318,12 @@ EscrowCreate::doApply() (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + { + auto const xferRate = transferRate(view(), amount.getIssuer()); + (*slep)[~sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add escrow to sender's owner directory @@ -276,7 +346,33 @@ EscrowCreate::doApply() } // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + else + { + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + // issuer does not need to lock anything + if (!isIssuer) + { + if (!sleLine) + return tecNO_LINE; + + // do the lock-up for real now + TER const result = transferToEntry( + ctx_.view(), sle, sleLine, amount, ctx_.journal, WetRun); + + JLOG(ctx_.journal.trace()) + << "EscrowCreate::doApply transferToEntry (wet) " + "result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + } + adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); @@ -371,6 +467,10 @@ EscrowFinish::doApply() if (!slep) return tecNO_TARGET; + AccountID const account = (*slep)[sfAccount]; + auto const sle = ctx_.view().peek(keylet::account(account)); + auto const amount = slep->getFieldAmount(sfAmount); + // If a cancel time is present, a finish operation should only succeed prior // to that time. fix1571 corrects a logic error in the check that would make // a finish only succeed strictly after the cancel time. @@ -471,7 +571,41 @@ EscrowFinish::doApply() } } - AccountID const account = (*slep)[sfAccount]; + if (!isXRP(amount)) + { + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + if (!slep->isFieldPresent(sfTransferRate)) + return tecINTERNAL; + + Rate lockedRate = ripple::Rate(slep->getFieldU32(sfTransferRate)); + auto const issuerAccID = amount.getIssuer(); + auto const xferRate = transferRate(view(), issuerAccID); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // perform a dry run of the transfer before we + // change anything on-ledger + TER const result = transferFromEntry( + ctx_.view(), + account_, // txn signing account + sle, // src account + sled, // dst account + amount, // xfer amount + lockedRate, + j_, + DryRun // dry run + ); + + JLOG(j_.trace()) + << "EscrowFinish::doApply transferFromEntry (dry) result=" + << result; + + if (!isTesSuccess(result)) + return result; + } // Remove escrow from owner directory { @@ -495,12 +629,46 @@ EscrowFinish::doApply() } } - // Transfer amount to destination - (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + if (isXRP(amount)) + (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + else + { + // compute transfer fee, if any + if (!slep->isFieldPresent(sfTransferRate)) + return tecINTERNAL; + + Rate lockedRate = ripple::Rate(slep->getFieldU32(sfTransferRate)); + auto const issuerAccID = amount.getIssuer(); + auto const xferRate = transferRate(view(), issuerAccID); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // all the significant complexity of checking the validity of this + // transfer and ensuring the lines exist etc is hidden away in this + // function, all we need to do is call it and return if unsuccessful. + TER const result = transferFromEntry( + ctx_.view(), + account_, // txn signing account + sle, // src account + sled, // dst account + amount, // xfer amount + lockedRate, + j_, + WetRun // wet run; + ); + + JLOG(j_.trace()) + << "EscrowFinish::doApply transferFromEntry (wet) result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + ctx_.view().update(sled); // Adjust source owner count - auto const sle = ctx_.view().peek(keylet::account(account)); adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); @@ -554,6 +722,30 @@ EscrowCancel::doApply() } AccountID const account = (*slep)[sfAccount]; + auto sle = ctx_.view().peek(keylet::account(account)); + auto const amount = slep->getFieldAmount(sfAmount); + bool const isIssuer = amount.getIssuer() == account; + + std::shared_ptr sleLine; + + if (!isXRP(amount)) + { + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + // issuer does not need to lock anything + if (!isIssuer) + { + sleLine = ctx_.view().peek(keylet::line( + account, amount.getIssuer(), amount.getCurrency())); + + // dry run before we make any changes to ledger + if (TER const result = transferToEntry( + ctx_.view(), sle, sleLine, -amount, ctx_.journal, DryRun); + result != tesSUCCESS) + return result; + } + } // Remove escrow from owner directory { @@ -580,9 +772,32 @@ EscrowCancel::doApply() } } - // Transfer amount back to owner, decrement owner count - auto const sle = ctx_.view().peek(keylet::account(account)); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + // Transfer amount back to the owner (or unlock it in TL case) + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + else + { + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + // issuer does not need to lock anything + if (!isIssuer) + { + // unlock previously locked tokens from source line + TER const result = transferToEntry( + ctx_.view(), sle, sleLine, -amount, ctx_.journal, WetRun); + + JLOG(ctx_.journal.trace()) + << "EscrowCancel::doApply transferToEntry (wet) " + "result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + } + + // Decrement owner count adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 70210b90d75..88fd4cb62b3 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -101,11 +101,14 @@ XRPNotCreated::visitEntry( drops_ -= (*before)[sfBalance].xrp().drops(); break; case ltPAYCHAN: - drops_ -= - ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]) + .xrp() + .drops(); break; case ltESCROW: - drops_ -= (*before)[sfAmount].xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= (*before)[sfAmount].xrp().drops(); break; default: break; @@ -120,13 +123,13 @@ XRPNotCreated::visitEntry( drops_ += (*after)[sfBalance].xrp().drops(); break; case ltPAYCHAN: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += ((*after)[sfAmount] - (*after)[sfBalance]) .xrp() .drops(); break; case ltESCROW: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; default: @@ -287,12 +290,25 @@ NoZeroEscrow::visitEntry( bool NoZeroEscrow::finalize( - STTx const&, + STTx const& txn, TER const, XRPAmount const, - ReadView const&, + ReadView const& rv, beast::Journal const& j) { + // bypass this invariant check for IOU escrows + if (bad_ && rv.rules().enabled(featurePaychanAndEscrowForTokens) && + txn.isFieldPresent(sfTransactionType)) + { + uint16_t const tt = txn.getFieldU16(sfTransactionType); + if (tt == ttESCROW_CANCEL || tt == ttESCROW_FINISH) + return true; + + if (txn.isFieldPresent(sfAmount) && + !isXRP(txn.getFieldAmount(sfAmount))) + return true; + } + if (bad_) { JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount"; diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index d17736c4738..e943735e80e 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -120,6 +120,34 @@ closeChannel( beast::Journal j) { AccountID const src = (*slep)[sfAccount]; + auto const amount = (*slep)[sfAmount] - (*slep)[sfBalance]; + + // Transfer amount back to owner, decrement owner count + auto sle = view.peek(keylet::account(src)); + if (!sle) + return tefINTERNAL; + + std::shared_ptr sleLine; + + if (!isXRP(amount)) + { + if (!view.rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + sleLine = view.peek( + keylet::line(src, amount.getIssuer(), amount.getCurrency())); + + // dry run + TER const result = + transferToEntry(view, sle, sleLine, -amount, j, DryRun); + + JLOG(j.trace()) << "closeChannel: transferToEntry(dry) result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + // Remove PayChan from owner directory { auto const page = (*slep)[sfOwnerNode]; @@ -144,14 +172,22 @@ closeChannel( } } - // Transfer amount back to owner, decrement owner count - auto const sle = view.peek(keylet::account(src)); - if (!sle) - return tefINTERNAL; - assert((*slep)[sfAmount] >= (*slep)[sfBalance]); - (*sle)[sfBalance] = - (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; + + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + amount; + else + { + TER const result = + transferToEntry(view, sle, sleLine, -amount, j, WetRun); + + JLOG(j.trace()) << "closeChannel: transferToEntry(wet) result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + adjustOwnerCount(view, sle, -1, j); view.update(sle); @@ -165,7 +201,8 @@ closeChannel( TxConsequences PayChanCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; } NotTEC @@ -177,7 +214,20 @@ PayChanCreate::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + if (!isLegalNet(amount)) + return temBAD_AMOUNT; + + if (isFakeXRP(amount)) + return temBAD_CURRENCY; + } + + if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) @@ -193,25 +243,60 @@ TER PayChanCreate::preclaim(PreclaimContext const& ctx) { auto const account = ctx.tx[sfAccount]; - auto const sle = ctx.view.read(keylet::account(account)); + auto sle = ctx.view.read(keylet::account(account)); if (!sle) return terNO_ACCOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + + auto const balance = (*sle)[sfBalance]; + auto const reserve = + ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + auto const dst = ctx.tx[sfDestination]; + bool const isIssuer = amount.getIssuer() == account; + // Check reserve and funds availability + if (isXRP(amount) && balance < reserve + amount) + { + return tecUNFUNDED; + } + else if (!isXRP(amount)) { - auto const balance = (*sle)[sfBalance]; - auto const reserve = - ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1); + if (!ctx.view.rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + // check for any possible bars to a channel existing + // between these accounts for this asset + { + TER const result = trustTransferAllowed( + ctx.view, {account, dst}, amount.issue(), ctx.j); + JLOG(ctx.j.trace()) + << "PayChanCreate::preclaim trustTransferAllowed result=" + << result; + + if (!isTesSuccess(result)) + return result; + } - if (balance < reserve + ctx.tx[sfAmount]) - return tecUNFUNDED; + // issuer does not need to lock anything + if (!isIssuer) + { + auto sleLine = ctx.view.read(keylet::line( + account, amount.getIssuer(), amount.getCurrency())); + TER const result = + transferToEntry(ctx.view, sle, sleLine, amount, ctx.j, DryRun); + JLOG(ctx.j.trace()) << "PayChanCreate::preclaim " + "transferToEntry(dry) result=" + << result; + if (!isTesSuccess(result)) + return result; + } } - auto const dst = ctx.tx[sfDestination]; - { // Check destination account auto const sled = ctx.view.read(keylet::account(dst)); @@ -230,8 +315,8 @@ PayChanCreate::preclaim(PreclaimContext const& ctx) // Obeying the lsfDisallowXRP flag was a bug. Piggyback on // featureDepositAuth to remove the bug. - if (!ctx.view.rules().enabled(featureDepositAuth) && - (flags & lsfDisallowXRP)) + if (!ctx.view.rules().enabled(featureDepositAuth) && isXRP(amount) && + ((*sled)[sfFlags] & lsfDisallowXRP)) return tecNO_TARGET; if (sled->isFieldPresent(sfAMMID)) @@ -245,12 +330,15 @@ TER PayChanCreate::doApply() { auto const account = ctx_.tx[sfAccount]; - auto const sle = ctx_.view().peek(keylet::account(account)); + auto sle = ctx_.view().peek(keylet::account(account)); if (!sle) return tefINTERNAL; auto const dst = ctx_.tx[sfDestination]; + STAmount const amount{ctx_.tx[sfAmount]}; + bool const isIssuer = amount.getIssuer() == account; + // Create PayChan in ledger. // // Note that we we use the value from the sequence or ticket as the @@ -271,6 +359,12 @@ PayChanCreate::doApply() (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + { + auto xferRate = transferRate(view(), amount.getIssuer()); + (*slep)[~sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add PayChan to owner directory @@ -295,7 +389,34 @@ PayChanCreate::doApply() } // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + auto sleLine = ctx_.view().peek( + keylet::line(account, amount.getIssuer(), amount.getCurrency())); + + if (!isIssuer) + { + if (!sleLine) + return tecNO_LINE; + + TER const result = transferToEntry( + ctx_.view(), sle, sleLine, amount, ctx_.journal, WetRun); + + JLOG(ctx_.journal.trace()) + << "PayChanCreate::doApply transferToEntry(wet) " + "result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + } + adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); @@ -307,7 +428,8 @@ PayChanCreate::doApply() TxConsequences PayChanFund::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; } NotTEC @@ -319,7 +441,20 @@ PayChanFund::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featurePaychanAndEscrowForTokens)) + return temBAD_AMOUNT; + + if (!isLegalNet(amount)) + return temBAD_AMOUNT; + + if (isFakeXRP(amount)) + return temBAD_CURRENCY; + } + + if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; return preflight2(ctx); @@ -333,10 +468,60 @@ PayChanFund::doApply() if (!slep) return tecNO_ENTRY; + STAmount const amount{ctx_.tx[sfAmount]}; + + std::shared_ptr sleLine; // if XRP or featurePaychanAndEscrowForTokens + // not enabled this remains null + AccountID const src = (*slep)[sfAccount]; auto const txAccount = ctx_.tx[sfAccount]; auto const expiration = (*slep)[~sfExpiration]; + bool const isIssuer = amount.getIssuer() == txAccount; + + auto sle = ctx_.view().peek(keylet::account(txAccount)); + if (!sle) + return tefINTERNAL; + + // if this is a Fund operation on an IOU then perform a dry run here + if (!isXRP(amount) && + ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + { + // adjust transfer rate + if (!slep->isFieldPresent(sfTransferRate)) + return tecINTERNAL; + + Rate lockedRate = ripple::Rate(slep->getFieldU32(sfTransferRate)); + auto const issuerAccID = amount.getIssuer(); + auto const xferRate = transferRate(view(), issuerAccID); + + // update if issuer rate less than locked rate + if (xferRate < lockedRate) + (*slep)[~sfTransferRate] = xferRate.value; + + // throw if issuer rate greater than locked rate + if (xferRate > lockedRate) + return temBAD_TRANSFER_RATE; + + // issuer does not need to lock anything + if (!isIssuer) + { + if (slep->getFieldAmount(sfBalance).issue() != amount.issue()) + return temBAD_CURRENCY; + + sleLine = ctx_.view().peek(keylet::line( + (*slep)[sfAccount], amount.getIssuer(), amount.getCurrency())); + + TER const result = transferToEntry( + ctx_.view(), sle, sleLine, amount, ctx_.journal, DryRun); + + JLOG(ctx_.journal.trace()) + << "PayChanFund::doApply transferToEntry(dry) result=" + << result; + if (!isTesSuccess(result)) + return result; + } + } { auto const cancelAfter = (*slep)[~sfCancelAfter]; auto const closeTime = @@ -365,36 +550,52 @@ PayChanFund::doApply() ctx_.view().update(slep); } - auto const sle = ctx_.view().peek(keylet::account(txAccount)); - if (!sle) - return tefINTERNAL; - + // do not allow adding funds if dst does not exist + if (AccountID const dst = (*slep)[sfDestination]; + !ctx_.view().read(keylet::account(dst))) { - // Check reserve and funds availability - auto const balance = (*sle)[sfBalance]; - auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); + return tecNO_DST; + } + + // Check reserve and funds availability + auto const balance = (*sle)[sfBalance]; + auto const reserve = + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]); - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; - if (balance < reserve + ctx_.tx[sfAmount]) + if (isXRP(amount)) + { + if (balance < reserve + amount) return tecUNFUNDED; - } - // do not allow adding funds if dst does not exist - if (AccountID const dst = (*slep)[sfDestination]; - !ctx_.view().read(keylet::account(dst))) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + ctx_.view().update(sle); + } + else { - return tecNO_DST; + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + // issuer does not need to lock anything + if (!isIssuer) + { + TER const result = transferToEntry( + ctx_.view(), sle, sleLine, amount, ctx_.journal, WetRun); + + JLOG(ctx_.journal.trace()) + << "PayChanFund::doApply transferToEntry(wet) result=" + << result; + + if (!isTesSuccess(result)) + return result; + } } (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; ctx_.view().update(slep); - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; - ctx_.view().update(sle); - return tesSUCCESS; } @@ -407,12 +608,27 @@ PayChanClaim::preflight(PreflightContext const& ctx) return ret; auto const bal = ctx.tx[~sfBalance]; - if (bal && (!isXRP(*bal) || *bal <= beast::zero)) - return temBAD_AMOUNT; + if (bal) + { + if (!isXRP(*bal) && + !ctx.rules.enabled(featurePaychanAndEscrowForTokens)) + return temBAD_AMOUNT; + + if (*bal <= beast::zero) + return temBAD_AMOUNT; + } auto const amt = ctx.tx[~sfAmount]; - if (amt && (!isXRP(*amt) || *amt <= beast::zero)) - return temBAD_AMOUNT; + + if (amt) + { + if (!isXRP(*amt) && + !ctx.rules.enabled(featurePaychanAndEscrowForTokens)) + return temBAD_AMOUNT; + + if (*amt <= beast::zero) + return temBAD_AMOUNT; + } if (bal && amt && *bal > *amt) return temBAD_AMOUNT; @@ -436,8 +652,8 @@ PayChanClaim::preflight(PreflightContext const& ctx) // The signature isn't needed if txAccount == src, but if it's // present, check it - auto const reqBalance = bal->xrp(); - auto const authAmt = amt ? amt->xrp() : reqBalance; + auto const reqBalance = *bal; + auto const authAmt = amt ? *amt : reqBalance; if (reqBalance > authAmt) return temBAD_AMOUNT; @@ -448,7 +664,17 @@ PayChanClaim::preflight(PreflightContext const& ctx) PublicKey const pk(ctx.tx[sfPublicKey]); Serializer msg; - serializePayChanAuthorization(msg, k.key, authAmt); + + if (isXRP(authAmt)) + serializePayChanAuthorization(msg, k.key, authAmt.xrp()); + else + serializePayChanAuthorization( + msg, + k.key, + authAmt.iou(), + authAmt.getCurrency(), + authAmt.getIssuer()); + if (!verify(pk, msg.slice(), *sig, /*canonical*/ true)) return temBAD_SIGNATURE; } @@ -484,9 +710,9 @@ PayChanClaim::doApply() if (ctx_.tx[~sfBalance]) { - auto const chanBalance = slep->getFieldAmount(sfBalance).xrp(); - auto const chanFunds = slep->getFieldAmount(sfAmount).xrp(); - auto const reqBalance = ctx_.tx[sfBalance].xrp(); + auto const chanBalance = slep->getFieldAmount(sfBalance); + auto const chanFunds = slep->getFieldAmount(sfAmount); + auto const reqBalance = ctx_.tx[sfBalance]; if (txAccount == dst && !ctx_.tx[~sfSignature]) return temBAD_SIGNATURE; @@ -505,14 +731,14 @@ PayChanClaim::doApply() // nothing requested return tecUNFUNDED_PAYMENT; - auto const sled = ctx_.view().peek(keylet::account(dst)); + auto sled = ctx_.view().peek(keylet::account(dst)); if (!sled) return tecNO_DST; // Obeying the lsfDisallowXRP flag was a bug. Piggyback on // featureDepositAuth to remove the bug. bool const depositAuth{ctx_.view().rules().enabled(featureDepositAuth)}; - if (!depositAuth && + if (!depositAuth && chanBalance.native() && (txAccount == src && (sled->getFlags() & lsfDisallowXRP))) return tecNO_TARGET; @@ -531,9 +757,52 @@ PayChanClaim::doApply() } (*slep)[sfBalance] = ctx_.tx[sfBalance]; - XRPAmount const reqDelta = reqBalance - chanBalance; + STAmount const reqDelta = reqBalance - chanBalance; assert(reqDelta >= beast::zero); - (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + if (isXRP(reqDelta)) + (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + else + { + // xfer locked tokens to satisfy claim + // RH NOTE: there's no ledger modification before this point so + // no reason to do a dry run first + if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + // compute transfer fee, if any + if (!slep->isFieldPresent(sfTransferRate)) + return tecINTERNAL; + + Rate lockedRate = ripple::Rate(slep->getFieldU32(sfTransferRate)); + auto const issuerAccID = chanFunds.getIssuer(); + auto const xferRate = transferRate(view(), issuerAccID); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + { + (*slep)[~sfTransferRate] = xferRate.value; + lockedRate = xferRate; + } + + auto sleSrcAcc = ctx_.view().peek(keylet::account(src)); + TER const result = transferFromEntry( + ctx_.view(), + txAccount, + sleSrcAcc, + sled, + reqDelta, + lockedRate, + ctx_.journal, + WetRun); + + JLOG(ctx_.journal.trace()) + << "PayChanClaim::doApply transferFromEntry(wet) " + "result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + ctx_.view().update(sled); ctx_.view().update(slep); } @@ -567,4 +836,4 @@ PayChanClaim::doApply() return tesSUCCESS; } -} // namespace ripple +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 09f374d2c29..2fbff7cce92 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -38,8 +39,6 @@ #include #include -#include - namespace ripple { enum class WaiveTransferFee : bool { No = false, Yes }; @@ -452,6 +451,492 @@ transferXRP( STAmount const& amount, beast::Journal j); +//------------------------------------------------------------------------------ + +// +// Trustline Locking and Transfer (PaychanAndEscrowForTokens) +// + +// In functions white require a `RunType` +// pass DryRun (don't apply changes) or WetRun (do apply changes) +// to allow compile time evaluation of which types and calls to use + +// For all functions below that take a Dry/Wet run parameter +// View may be ReadView const or ApplyView for DryRuns. +// View *must* be ApplyView for a WetRun. +// Passed SLEs must be non-const for WetRun. +#define DryRun RunType() +#define WetRun RunType() +template +struct RunType +{ + // see: + // http://alumni.media.mit.edu/~rahimi/compile-time-flags/ + constexpr operator T() const + { + static_assert(std::is_same::value); + return V; + } + + constexpr T + operator!() const + { + static_assert(std::is_same::value); + return !(V); + } +}; + +// allow party lists to be logged easily +template +std::ostream& +operator<<(std::ostream& lhs, std::vector const& rhs) +{ + lhs << "{"; + for (int i = 0; i < rhs.size(); ++i) + lhs << rhs[i] << (i < rhs.size() - 1 ? ", " : ""); + lhs << "}"; + return lhs; +} +// Return true iff the acc side of line is in default state +bool +isTrustDefault( + std::shared_ptr const& acc, // side to check + std::shared_ptr const& line); // line to check + +/** Lock or unlock a TrustLine balance. + If positive deltaAmt lock the amount. + If negative deltaAmt unlock the amount. +*/ +template +[[nodiscard]] TER +transferToEntry( + V& view, + S& sleSrcAcc, + S& sleLine, + STAmount const& deltaAmt, + beast::Journal const& j, + R dryRun) +{ + static_assert( + (std::is_same::value && + std::is_same>::value) || + (std::is_same::value && + std::is_same>::value)); + + // dry runs are explicit in code, but really the view type determines + // what occurs here, so this combination is invalid. + static_assert(!(std::is_same::value && !dryRun)); + + if (!view.rules().enabled(featurePaychanAndEscrowForTokens)) + return temDISABLED; + + if (!sleLine) + return tecNO_LINE; + + if (!sleSrcAcc) + return tecNO_TARGET; + + auto const issuer = deltaAmt.getIssuer(); + auto const srcAccID = sleSrcAcc->getAccountID(sfAccount); + bool const srcIssuer = issuer == srcAccID; + + STAmount const lowLimit = sleLine->getFieldAmount(sfLowLimit); + + // the account which is modifying the Balance is always + // the side that isn't the issuer, so if the low side is the + // issuer then the high side is the account. + bool const high = lowLimit.getIssuer() == issuer; + + std::vector parties{ + high ? sleLine->getFieldAmount(sfHighLimit).getIssuer() + : lowLimit.getIssuer()}; + + // check for freezes & auth + { + TER const result = + trustTransferAllowed(view, parties, deltaAmt.issue(), j); + + JLOG(j.trace()) << "transferToEntry: trustTransferAllowed result=" + << result; + + if (!isTesSuccess(result)) + return result; + } + + // pull the TL balance from the account's perspective + STAmount balance = high ? -(*sleLine)[sfBalance] : (*sleLine)[sfBalance]; + + // this would mean somehow the issuer is trying to lock balance + if (balance < beast::zero) + return tecINTERNAL; + + if (deltaAmt == beast::zero) + return tesSUCCESS; + + // can't lock or unlock a zero balance + if (balance == beast::zero) + { + JLOG(j.error()) << "transferToEntry failed, zero balance"; + return tecUNFUNDED_PAYMENT; + } + + STAmount finalBalance = balance - deltaAmt; + if (deltaAmt > balance) + { + JLOG(j.trace()) << "transferToEntry: " + << "balance(" << finalBalance << ") > balance(" + << balance << ") = true\n"; + return tecINSUFFICIENT_FUNDS; + } + + if (finalBalance < beast::zero) + return tecINTERNAL; + + // check if there is significant precision loss + if (!isAddable(balance, deltaAmt) || !isAddable(finalBalance, balance)) + return tecPRECISION_LOSS; + + // we won't update any SLEs if it is a dry run + if (dryRun) + return tesSUCCESS; + + if constexpr ( + std::is_same::value && + std::is_same>::value) + { + sleLine->setFieldAmount(sfBalance, high ? -finalBalance : finalBalance); + // if source account is not issuer + if (!srcIssuer) + { + // check if source line ended up in default state + if (isTrustDefault(sleSrcAcc, sleLine)) + { + // adjust owner count + uint32_t flags = sleLine->getFieldU32(sfFlags); + uint32_t fReserve{static_cast( + high ? lsfHighReserve : lsfLowReserve)}; + if (flags & fReserve) + { + sleLine->setFieldU32(sfFlags, flags & ~fReserve); + adjustOwnerCount(view, sleSrcAcc, -1, j); + view.update(sleSrcAcc); + } + } + // update source line + view.update(sleLine); + } + } + + return tesSUCCESS; +} + +/** Check if a set of accounts can freely exchange the specified token. + Read only, does not change any ledger object. + May be called with ApplyView or ReadView. + (including unlocking) is forbidden by any flag or condition. + If parties contains 1 entry then noRipple is not a bar to xfer. + If parties contains more than 1 entry then any party with noRipple + on issuer side is a bar to xfer. +*/ +template +[[nodiscard]] TER +trustTransferAllowed( + V& view, + std::vector const& parties, + Issue const& issue, + beast::Journal const& j) +{ + static_assert( + std::is_same::value || + std::is_same::value); + + typedef typename std::conditional< + std::is_same::value, + std::shared_ptr, + std::shared_ptr>::type SLEPtr; + + if (isFakeXRP(issue.currency)) + return tecNO_PERMISSION; + + auto const sleIssuerAcc = view.read(keylet::account(issue.account)); + + // missing issuer is always a bar to xfer + if (!sleIssuerAcc) + return tecNO_ISSUER; + + // issuer global freeze is always a bar to xfer + if (isGlobalFrozen(view, issue.account)) + return tecFROZEN; + + uint32_t issuerFlags = sleIssuerAcc->getFieldU32(sfFlags); + + bool requireAuth = issuerFlags & lsfRequireAuth; + + for (AccountID const& p : parties) + { + if (p == issue.account) + continue; + + auto const line = + view.read(keylet::line(p, issue.account, issue.currency)); + if (!line) + { + if (requireAuth) + { + // the line doesn't exist, i.e. it is in default state + // default state means the line has not been authed + // therefore if auth is required by issuer then + // this is now a bar to xfer + return tecNO_AUTH; + } + + // missing line is a line in default state, this is not + // a general bar to xfer, however additional conditions + // do attach to completing an xfer into a default line + // but these are checked in transferFromEntry at + // the point of transfer. + continue; + } + + // check the bars to xfer ... these are: + // any TL in the set has noRipple on the issuer's side + // any TL in the set has a freeze on the issuer's side + // any TL in the set has RequireAuth and the TL lacks lsf*Auth + { + bool pHigh = p > issue.account; + + auto const flagIssuerNoRipple{ + pHigh ? lsfLowNoRipple : lsfHighNoRipple}; + auto const flagIssuerFreeze{pHigh ? lsfLowFreeze : lsfHighFreeze}; + auto const flagIssuerAuth{pHigh ? lsfLowAuth : lsfHighAuth}; + + uint32_t flags = line->getFieldU32(sfFlags); + + if (flags & flagIssuerFreeze) + { + JLOG(j.trace()) << "trustTransferAllowed: " + << "parties=[" << parties << "], " + << "issuer: " << issue.account << " " + << "has freeze on party: " << p; + return tecFROZEN; + } + + // if called with more than one party then any party + // that has a noripple on the issuer side of their tl + // blocks any possible xfer + if (parties.size() > 1 && (flags & flagIssuerNoRipple)) + { + JLOG(j.trace()) << "trustTransferAllowed: " + << "parties=[" << parties << "], " + << "issuer: " << issue.account << " " + << "has noRipple on party: " << p; + return tecPATH_DRY; + } + + // every party involved must be on an authed trustline if + // the issuer has specified lsfRequireAuth + if (requireAuth && !(flags & flagIssuerAuth)) + { + JLOG(j.trace()) << "trustTransferAllowed: " + << "parties=[" << parties << "], " + << "issuer: " << issue.account << " " + << "requires TL auth which " + << "party: " << p << " " + << "does not possess."; + return tecNO_AUTH; + } + } + } + + return tesSUCCESS; +} + +/** Transfer a locked balance from one TL to an unlocked balance on another + or create a line at the destination if the actingAcc has permission to. + Used for resolving payment instruments that use locked TL balances. +*/ +template +[[nodiscard]] TER +transferFromEntry( + V& view, + AccountID const& actingAccID, // the account whose tx is actioning xfer + S& sleSrcAcc, + S& sleDstAcc, + STAmount const& amount, // issuer, currency are in this field + Rate const& lXferRate, // locked transfer rate + beast::Journal const& j, + R dryRun) +{ + typedef typename std::conditional< + std::is_same::value && !dryRun, + std::shared_ptr, + std::shared_ptr>::type SLEPtr; + + auto peek = [&](Keylet& k) { + if constexpr (std::is_same::value && !dryRun) + return const_cast(view).peek(k); + else + return view.read(k); + }; + + static_assert(std::is_same::value || dryRun); + + if (!view.rules().enabled(featurePaychanAndEscrowForTokens)) + return tefINTERNAL; + + if (!sleSrcAcc || !sleDstAcc) + { + JLOG(j.error()) << "transferFromEntry without sleSrc/sleDst"; + return tecINTERNAL; + } + + if (amount <= beast::zero) + { + JLOG(j.error()) << "transferFromEntry with non-positive amount"; + return tecINTERNAL; + } + + auto const issuerAccID = amount.getIssuer(); + auto const currency = amount.getCurrency(); + auto const srcAccID = sleSrcAcc->getAccountID(sfAccount); + auto const dstAccID = sleDstAcc->getAccountID(sfAccount); + + bool const dstHigh = dstAccID > issuerAccID; + bool const srcIssuer = issuerAccID == srcAccID; + bool const dstIssuer = issuerAccID == dstAccID; + + // check for freezing, auth, no ripple and TL sanity + { + TER const result = trustTransferAllowed( + view, {srcAccID, dstAccID}, {currency, issuerAccID}, j); + + JLOG(j.trace()) << "transferFromEntry: trustTransferAllowed result=" + << result; + if (!isTesSuccess(result)) + return result; + } + + // default dstAmount to amount + auto dstAmt = amount; + + // if tx acct not source issuer or dest issuer + // and xfer rate is not parity + if ((!srcIssuer && !dstIssuer) && lXferRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lXferRate, amount.issue(), true); + // compute balance to transfer + dstAmt = amount.value() - xferFee; + } + // ensure source line exists + Keylet klSrcLine{keylet::line(srcAccID, issuerAccID, currency)}; + SLEPtr sleSrcLine = peek(klSrcLine); + + // check for a destination line + Keylet klDstLine = keylet::line(dstAccID, issuerAccID, currency); + SLEPtr sleDstLine = peek(klDstLine); + + // if dest account is not issuer + if (!dstIssuer) + { + // if dest acct has no trustline + if (!sleDstLine) + { + // if tx acct is not dest acct and src acct is not dest acct + if (actingAccID != dstAccID && srcAccID != dstAccID) + return tecNO_LINE; + + STAmount dstBalanceDrops = sleDstAcc->getFieldAmount(sfBalance); + + // no dst line exists, we might be able to create one... + if (std::uint32_t const ownerCount = {sleDstAcc->at(sfOwnerCount)}; + dstBalanceDrops < view.fees().accountReserve(ownerCount + 1)) + return tecNO_LINE_INSUF_RESERVE; + + // create destination trust line + if constexpr (!dryRun) + { + // clang-format off + if (TER const ter = trustCreate( + view, + !dstHigh, // is dest low? + issuerAccID, // source + dstAccID, // destination + klDstLine.key, // ledger index + sleDstAcc, // Account to add to + false, // authorize account + (sleDstAcc->getFlags() & lsfDefaultRipple) == 0, + false, // freeze trust line + dstAmt, // initial balance + Issue(currency, dstAccID), // limit of zero + 0, // quality in + 0, // quality out + j); // journal + !isTesSuccess(ter)) + { + return ter; + } + } + // clang-format on + } + else + { + // dest trust line does exist + // checked NoRipple and Freeze flags in trustTransferAllowed + + // check the limit + STAmount dstLimit = dstHigh ? (*sleDstLine)[sfHighLimit] + : (*sleDstLine)[sfLowLimit]; + + // get prior balance + STAmount priorBalance = dstHigh ? -((*sleDstLine)[sfBalance]) + : (*sleDstLine)[sfBalance]; + + // combine prior with dest amount for final + STAmount finalBalance = priorBalance + dstAmt; + + // if final is less than prior - fail + if (finalBalance < priorBalance) + { + JLOG(j.warn()) << "transferFromEntry resulted in a " + "lower/equal final balance on dest line"; + return tecINTERNAL; + } + + // if final is more than dest limit and tx acct is not dest acct - + // fail + if (finalBalance > dstLimit && actingAccID != dstAccID) + { + JLOG(j.trace()) << "transferFromEntry would increase dest " + "line above limit without permission"; + return tecPATH_DRY; + } + + // if there is significant precision loss - fail + if (!isAddable(priorBalance, dstAmt) || + !isAddable(finalBalance, priorBalance)) + return tecPRECISION_LOSS; + + // compute final balance to send - reverse sign for high dest + finalBalance = dstHigh ? -finalBalance : finalBalance; + + // if not dry run - set dst line field + if constexpr (!dryRun) + sleDstLine->setFieldAmount(sfBalance, finalBalance); + } + } + + if constexpr (!dryRun) + { + static_assert(std::is_same::value); + // if dest line exists + if (sleDstLine) + // update dest line + view.update(sleDstLine); + } + return tesSUCCESS; +} /** Check if the account requires authorization. * Return tecNO_AUTH or tecNO_LINE if it does * and tesSUCCESS otherwise. diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 13ac07e5e74..0c391e09d86 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -257,6 +257,7 @@ accountHolds( // Put balance in account terms. amount.negate(); } + amount.setIssuer(issuer); } JLOG(j.trace()) << "accountHolds:" @@ -906,6 +907,56 @@ trustDelete( return tesSUCCESS; } +bool +isTrustDefault( + std::shared_ptr const& acc, + std::shared_ptr const& line) +{ + assert(acc && line); + + uint32_t const tlFlags = line->getFieldU32(sfFlags); + + AccountID const highAccID = + line->getFieldAmount(sfHighLimit).issue().account; + AccountID const lowAccID = line->getFieldAmount(sfLowLimit).issue().account; + + AccountID const accID = acc->getAccountID(sfAccount); + + assert(accID == highAccID || accID == lowAccID); + + bool const high = accID == highAccID; + + uint32_t const acFlags = line->getFieldU32(sfFlags); + + const auto fNoRipple{high ? lsfHighNoRipple : lsfLowNoRipple}; + const auto fFreeze{high ? lsfHighFreeze : lsfLowFreeze}; + + if (tlFlags & fFreeze) + return false; + + if ((acFlags & lsfDefaultRipple) && (tlFlags & fNoRipple)) + return false; + + if (line->getFieldAmount(sfBalance) != beast::zero) + return false; + + if (line->getFieldAmount(high ? sfHighLimit : sfLowLimit) != beast::zero) + return false; + + uint32_t const qualityIn = + line->getFieldU32(high ? sfHighQualityIn : sfLowQualityIn); + uint32_t const qualityOut = + line->getFieldU32(high ? sfHighQualityOut : sfLowQualityOut); + + if (qualityIn && qualityIn != QUALITY_ONE) + return false; + + if (qualityOut && qualityOut != QUALITY_ONE) + return false; + + return true; +} + TER offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j) { diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index 533878ab1b0..ad1a09c38b9 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -156,6 +156,35 @@ class RPCParser } } + // Build an object + // { "currency" : "XYZ", "issuer" : "rXYX", "value": 1000 } + static Json::Value + jvParseSTAmount(std::string const& strIC) + { + static boost::regex reCurIss( + "\\`(0|[1-9][0-9]*)(?:/([[:alpha:]]{3}))(?:/(.+))?\\'"); + + boost::smatch icMatch; + + Json::Value jvResult(Json::objectValue); + if (boost::regex_match(strIC, icMatch, reCurIss)) + { + std::string strAmount = icMatch[1]; + std::string strCurrency = icMatch[2]; + std::string strIssuer = icMatch[3]; + + jvResult[jss::currency] = strCurrency; + jvResult[jss::value] = strAmount; + + if (strIssuer.length()) + { + // Could confirm issuer is a valid Ripple address. + jvResult[jss::issuer] = strIssuer; + } + } + return jvResult; + } + static bool validPublicKey( std::string const& strPk, @@ -740,7 +769,8 @@ class RPCParser return parseAccountRaw2(jvParams, jss::destination_account); } - // channel_authorize: [] + // channel_authorize: [] Json::Value parseChannelAuthorize(Json::Value const& jvParams) { @@ -773,14 +803,36 @@ class RPCParser index++; } - if (!jvParams[index].isString() || - !to_uint64(jvParams[index].asString())) - return rpcError(rpcCHANNEL_AMT_MALFORMED); - jvRequest[jss::amount] = jvParams[index]; + { + // validate amount string | json + if (!jvParams[index].isString()) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + + // parse string + Json::Value amountJson = + jvParseSTAmount(jvParams[index].asString()); + if (!amountJson) + { + // amount is string + if (!to_uint64(jvParams[index].asString())) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + + jvRequest[jss::amount] = jvParams[index].asString(); + } + else + { + // amount is json + STAmount amount; + bool isAmount = amountFromJsonNoThrow(amount, amountJson); + if (!isAmount) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + + jvRequest[jss::amount] = amountJson; + } + } // If additional parameters are appended, be sure to increment index // here - return jvRequest; } @@ -804,12 +856,33 @@ class RPCParser } jvRequest[jss::channel_id] = jvParams[1u].asString(); - if (!jvParams[2u].isString() || !to_uint64(jvParams[2u].asString())) - return rpcError(rpcCHANNEL_AMT_MALFORMED); - jvRequest[jss::amount] = jvParams[2u]; + { + // validate amount string | json + if (!jvParams[2u].isString()) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + // parse string + Json::Value amountJson = jvParseSTAmount(jvParams[2u].asString()); + if (!amountJson) + { + // amount is string + if (!to_uint64(jvParams[2u].asString())) + return rpcError(rpcCHANNEL_AMT_MALFORMED); - jvRequest[jss::signature] = jvParams[3u].asString(); + jvRequest[jss::amount] = jvParams[2u].asString(); + } + else + { + // amount is json + STAmount amount; + bool isAmount = amountFromJsonNoThrow(amount, amountJson); + if (!isAmount) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + jvRequest[jss::amount] = amountJson; + } + } + + jvRequest[jss::signature] = jvParams[3u].asString(); return jvRequest; } diff --git a/src/xrpld/rpc/handlers/AccountChannels.cpp b/src/xrpld/rpc/handlers/AccountChannels.cpp index 5ae87e32a12..3c99d02c3ff 100644 --- a/src/xrpld/rpc/handlers/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/AccountChannels.cpp @@ -38,7 +38,7 @@ addChannel(Json::Value& jsonLines, SLE const& line) jDst[jss::channel_id] = to_string(line.key()); jDst[jss::account] = to_string(line[sfAccount]); jDst[jss::destination_account] = to_string(line[sfDestination]); - jDst[jss::amount] = line[sfAmount].getText(); + line[sfAmount].setJson(jDst[jss::amount]); jDst[jss::balance] = line[sfBalance].getText(); if (publicKeyType(line[sfPublicKey])) { @@ -161,7 +161,7 @@ doAccountChannels(RPC::JsonContext& context) accountID, startAfter, startHint, - limit + 1, + limit, [&visitData, &accountID, &count, &limit, &marker, &nextHint]( std::shared_ptr const& sleCur) { if (!sleCur) @@ -170,16 +170,21 @@ doAccountChannels(RPC::JsonContext& context) return false; } + if (sleCur->getType() != ltPAYCHAN) + { + return false; + } + if (++count == limit) { marker = sleCur->key(); nextHint = RPC::getStartHint(sleCur, visitData.accountID); } - if (count <= limit && sleCur->getType() == ltPAYCHAN && - (*sleCur)[sfAccount] == accountID && + if (count <= limit && (*sleCur)[sfAccount] == accountID && (!visitData.raDstAccount || *visitData.raDstAccount == (*sleCur)[sfDestination])) + { visitData.items.emplace_back(sleCur); } @@ -193,7 +198,7 @@ doAccountChannels(RPC::JsonContext& context) // Both conditions need to be checked because marker is set on the limit-th // item, but if there is no item on the limit + 1 iteration, then there is // no need to return a marker. - if (count == limit + 1 && marker) + if (count == limit && marker) { result[jss::limit] = limit; result[jss::marker] = diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index cace3487bb8..99ef6289316 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -36,6 +36,7 @@ addLine(Json::Value& jsonLines, RPCTrustLine const& line) STAmount const& saBalance(line.getBalance()); STAmount const& saLimit(line.getLimit()); STAmount const& saLimitPeer(line.getLimitPeer()); + Json::Value& jPeer(jsonLines.append(Json::objectValue)); jPeer[jss::account] = to_string(line.getAccountIDPeer()); @@ -45,6 +46,7 @@ addLine(Json::Value& jsonLines, RPCTrustLine const& line) // Amount reported is negative if other account holds current // account's IOUs. jPeer[jss::balance] = saBalance.getText(); + jPeer[jss::currency] = to_string(saBalance.issue().currency); jPeer[jss::limit] = saLimit.getText(); jPeer[jss::limit_peer] = saLimitPeer.getText(); diff --git a/src/xrpld/rpc/handlers/PayChanClaim.cpp b/src/xrpld/rpc/handlers/PayChanClaim.cpp index 1fecd5f1449..de2ac183568 100644 --- a/src/xrpld/rpc/handlers/PayChanClaim.cpp +++ b/src/xrpld/rpc/handlers/PayChanClaim.cpp @@ -69,17 +69,38 @@ doChannelAuthorize(RPC::JsonContext& context) if (!channelId.parseHex(params[jss::channel_id].asString())) return rpcError(rpcCHANNEL_MALFORMED); - std::optional const optDrops = params[jss::amount].isString() - ? to_uint64(params[jss::amount].asString()) - : std::nullopt; + Serializer msg; - if (!optDrops) + if (params[jss::amount].isNumeric()) return rpcError(rpcCHANNEL_AMT_MALFORMED); - std::uint64_t const drops = *optDrops; + if (params[jss::amount].isString()) + { + std::optional const optDrops = + params[jss::amount].isString() + ? to_uint64(params[jss::amount].asString()) + : std::nullopt; + + if (!optDrops) + return rpcError(rpcCHANNEL_AMT_MALFORMED); - Serializer msg; - serializePayChanAuthorization(msg, channelId, XRPAmount(drops)); + std::uint64_t const drops = *optDrops; + serializePayChanAuthorization(msg, channelId, XRPAmount(drops)); + } + else + { + STAmount amount; + bool isAmount = amountFromJsonNoThrow(amount, params[jss::amount]); + if (!isAmount) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + + serializePayChanAuthorization( + msg, + channelId, + amount.iou(), + amount.getCurrency(), + amount.getIssuer()); + } try { @@ -131,22 +152,44 @@ doChannelVerify(RPC::JsonContext& context) if (!channelId.parseHex(params[jss::channel_id].asString())) return rpcError(rpcCHANNEL_MALFORMED); - std::optional const optDrops = params[jss::amount].isString() - ? to_uint64(params[jss::amount].asString()) - : std::nullopt; + Serializer msg; - if (!optDrops) + if (params[jss::amount].isNumeric()) return rpcError(rpcCHANNEL_AMT_MALFORMED); - std::uint64_t const drops = *optDrops; + if (params[jss::amount].isString()) + { + std::optional const optDrops = + params[jss::amount].isString() + ? to_uint64(params[jss::amount].asString()) + : std::nullopt; + + if (!optDrops) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + + std::uint64_t const drops = *optDrops; + serializePayChanAuthorization(msg, channelId, XRPAmount(drops)); + } + else + { + STAmount amount; + bool isAmount = amountFromJsonNoThrow(amount, params[jss::amount]); + + if (!isAmount) + return rpcError(rpcCHANNEL_AMT_MALFORMED); + + serializePayChanAuthorization( + msg, + channelId, + amount.iou(), + amount.getCurrency(), + amount.getIssuer()); + } auto sig = strUnHex(params[jss::signature].asString()); if (!sig || !sig->size()) return rpcError(rpcINVALID_PARAMS); - Serializer msg; - serializePayChanAuthorization(msg, channelId, XRPAmount(drops)); - Json::Value result; result[jss::signature_verified] = verify(*pk, msg.slice(), makeSlice(*sig), /*canonical*/ true);