mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-21 19:18:22 +00:00
Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d5a406cc3 | ||
|
|
c0f4f855af | ||
|
|
7939339a65 | ||
|
|
4ea2db2eda | ||
|
|
f0a2ecddab | ||
|
|
ad73699c30 | ||
|
|
569a1aa674 | ||
|
|
45f63750bc | ||
|
|
00a547668b | ||
|
|
4b82a19493 | ||
|
|
ae6c4006d4 | ||
|
|
a0f04b4157 | ||
|
|
7c8044fa0c | ||
|
|
cb277e3512 | ||
|
|
bfc8b714a9 | ||
|
|
925f7a0127 | ||
|
|
8e082ffcf0 | ||
|
|
e592a33428 | ||
|
|
fa9efc6e17 | ||
|
|
bd8b57ba5f | ||
|
|
d4ffe32534 | ||
|
|
7354816f65 | ||
|
|
12cb3932f3 | ||
|
|
dc64a8daac | ||
|
|
d260b4be6d | ||
|
|
df4835d4f0 | ||
|
|
522689aea6 | ||
|
|
795eb2fdfb | ||
|
|
e1ce7f40d8 | ||
|
|
cac9d753e1 | ||
|
|
9696e9021a | ||
|
|
8e82bc8de1 | ||
|
|
21c4a3b02d | ||
|
|
6a1cb5fc1a | ||
|
|
346316e9a3 | ||
|
|
41aeb2149a | ||
|
|
489d2d84fa | ||
|
|
32c8893ee8 | ||
|
|
20b1c3efc2 | ||
|
|
59975e1742 | ||
|
|
0dc1ae9783 | ||
|
|
a5610d8443 | ||
|
|
0536d4f0dd | ||
|
|
41bcd23edd | ||
|
|
4dbffc8201 | ||
|
|
d2ac3a0b58 | ||
|
|
830bc0d1f3 | ||
|
|
9214e7d3fd | ||
|
|
0c0a0e8794 | ||
|
|
86b3835d54 | ||
|
|
7990cfb8a0 | ||
|
|
c01a0539fe | ||
|
|
cdfe9cb65d | ||
|
|
4258eca938 | ||
|
|
01919230a5 | ||
|
|
2ba6f4cc68 | ||
|
|
c5ae59c4a0 | ||
|
|
1ac1d40d06 | ||
|
|
fb10515cff | ||
|
|
3c85a0180f | ||
|
|
096fa967da | ||
|
|
1ced7dc8f3 | ||
|
|
df1458a136 | ||
|
|
cdd5ebf4c1 | ||
|
|
1a9a11fc31 | ||
|
|
a9430d4778 | ||
|
|
fed5093488 | ||
|
|
6c74fc1a7d | ||
|
|
939f8b1a49 | ||
|
|
1f8c462c42 | ||
|
|
47df026dcc |
No files matched your search
@@ -8,6 +8,7 @@ on:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
- "crates/**/Cargo.toml"
|
||||
- "crates/Cargo.lock"
|
||||
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
- should-run
|
||||
- build-test
|
||||
# Only run when committing to a PR that targets a release branch.
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
|
||||
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
|
||||
uses: ./.github/workflows/reusable-upload-recipe.yml
|
||||
secrets:
|
||||
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
# This workflow runs all workflows to build and test the code on various Linux
|
||||
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
|
||||
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
|
||||
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
|
||||
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
|
||||
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
|
||||
# recipe to the Conan remote.
|
||||
name: Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
paths:
|
||||
# These paths are unique to `on-trigger.yml`.
|
||||
- ".github/workflows/on-trigger.yml"
|
||||
@@ -62,9 +64,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
# When a PR is merged into the develop branch it will be assigned a unique
|
||||
# When a PR is merged into the develop branch, it will be assigned a unique
|
||||
# group identifier, so execution will continue even if another PR is merged
|
||||
# while it is still running. In all other cases the group identifier is shared
|
||||
# while it is still running. In all other cases, the group identifier is shared
|
||||
# per branch, so that any in-progress runs are cancelled when a new commit is
|
||||
# pushed.
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
|
||||
@@ -98,7 +100,7 @@ jobs:
|
||||
# However, we do not enable ccache for events targeting a release branch,
|
||||
# to protect against the rare case that the output produced by ccache is
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
|
||||
os: ${{ matrix.os }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -8,7 +8,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
- "release*"
|
||||
- "release/*"
|
||||
- "staging/*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -30,6 +30,10 @@ Version 3.5.0 is not yet released.
|
||||
|
||||
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive the same `transaction` message as the `transactions` stream for each validated transaction whose metadata affects a subscribed issuance. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
|
||||
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
|
||||
- `channel_authorize`, `channel_verify`: `amount` now also accepts the transaction `Amount` JSON object for a token payment channel (`currency`, `issuer` and `value` for a trust line token; `mpt_issuance_id` and `value` for an MPT), and the claim authorization is serialized for that token. A string of drops is still used for an XRP channel. An object that names XRP, is negative or is malformed returns `channelAmtMalformed`. On the command line, the amount argument may be that JSON object as a single argument. For a token channel the signed message is the `PaymentChannelClaim` hash prefix, the channel ID and the amount serialized as an `Amount` field value without its field header; the XRP message is unchanged. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
- `PaymentChannelCreate`, `PaymentChannelFund`, `PaymentChannelClaim`: `Amount` (and `Balance` on `PaymentChannelClaim`) now accept a trust line or MPT value in addition to XRP, gated by the `TokenPaychan` amendment. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
- `ledger_entry`, `account_objects`: The `PayChannel` ledger entry now includes optional `TransferRate` and `IssuerNode` fields, present on a channel funded with a trust line or MPT value. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
- `account_channels`: For a token payment channel, `amount` and `balance` are `Amount` JSON objects (`currency`, `issuer` and `value` for a trust line token; `mpt_issuance_id` and `value` for an MPT). An XRP channel still returns strings of drops. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
|
||||
|
||||
### Bugfixes in 3.5.0
|
||||
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Fees.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -53,153 +40,22 @@ escrowLockPreclaimHelper(
|
||||
beast::Journal j);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const& issue = amount.get<Issue>();
|
||||
auto const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
return tecNO_ISSUER;
|
||||
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the account does not have a trustline to the issuer, return tecNO_LINE
|
||||
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
STAmount const balance = (*sleRippleState)[sfBalance];
|
||||
|
||||
// If balance is positive, issuer must have higher address than account
|
||||
if (balance > beast::kZero && issuer < account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If balance is negative, issuer must have lower address than account
|
||||
if (balance < beast::kZero && issuer > account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is authorized
|
||||
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecFROZEN
|
||||
if (isFrozen(view, account, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
// If the issuer has frozen the destination, return tecFROZEN
|
||||
if (isFrozen(view, dest, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
STAmount const spendableAmount =
|
||||
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the amount is not addable to the balance, return tecPRECISION_LOSS
|
||||
if (!canAdd(spendableAmount, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal j);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
|
||||
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the issuer is not the same as the issuer of the mpt, return
|
||||
// tecNO_PERMISSION
|
||||
if (sleIssuance->getAccountID(sfIssuer) != issuer)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is
|
||||
// authorized
|
||||
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the issuer has frozen the destination, return tecLOCKED
|
||||
if (isFrozen(view, dest, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the mpt cannot be transferred, return tecNO_AUTH
|
||||
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<MPTIssue>(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal j);
|
||||
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
@@ -211,43 +67,22 @@ escrowLockApplyHelper(
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockApplyHelper<Issue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter =
|
||||
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowLockApplyHelper<MPTIssue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter = lockEscrowMPT(view, sender, amount, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
@@ -258,62 +93,20 @@ escrowUnlockPreclaimHelper(
|
||||
bool checkFreeze = true);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has deep frozen the destination, return tecFROZEN
|
||||
if (checkFreeze &&
|
||||
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
bool checkFreeze);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (checkFreeze && isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
bool checkFreeze);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -332,7 +125,7 @@ escrowUnlockApplyHelper(
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockApplyHelper<Issue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
@@ -343,138 +136,10 @@ escrowUnlockApplyHelper<Issue>(
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
if (senderIssuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (receiverIssuer)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const& issue = amount.get<Issue>();
|
||||
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
|
||||
bool const recvLow = issuer > receiver;
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && createAsset)
|
||||
{
|
||||
// Can the account cover the trust line's reserve?
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx,
|
||||
sleDest,
|
||||
xrpBalance,
|
||||
*sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
journal,
|
||||
tecNO_LINE_INSUF_RESERVE);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(journal.trace()) << "Trust line does not exist. "
|
||||
"Insufficient reserve to create line.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
Currency const currency = issue.currency;
|
||||
STAmount initialBalance(issue);
|
||||
initialBalance.get<Issue>().account = noAccount();
|
||||
|
||||
if (TER const ter = trustCreate(
|
||||
ctx.view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
!sleDest->isFlag(lsfDefaultRipple), //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
*sponsorSle, // sponsor
|
||||
journal); // journal
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
ctx.view.update(sleDest);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && !receiverIssuer)
|
||||
return tecNO_LINE;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
|
||||
// validate the line limit if the account submitting txn is not the receiver
|
||||
// of the funds
|
||||
if (!createAsset)
|
||||
{
|
||||
auto const sleRippleState = ctx.view.peek(trustLineKey);
|
||||
if (!sleRippleState)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// if the issuer is the high, then we use the low limit
|
||||
// otherwise we use the high limit
|
||||
STAmount const lineLimit =
|
||||
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
|
||||
|
||||
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
// flip the sign of the line balance if the issuer is not high
|
||||
if (!recvLow)
|
||||
lineBalance.negate();
|
||||
|
||||
// add the final amount to the line balance
|
||||
lineBalance += finalAmt;
|
||||
|
||||
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
|
||||
if (lineLimit < lineBalance)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
// if destination is not the issuer then transfer funds
|
||||
if (!receiverIssuer)
|
||||
{
|
||||
auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
template <>
|
||||
inline TER
|
||||
TER
|
||||
escrowUnlockApplyHelper<MPTIssue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
@@ -485,83 +150,7 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
auto const mptID = amount.get<MPTIssue>().getMptID();
|
||||
auto const issuanceKey = keylet::mptokenIssuance(mptID);
|
||||
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
|
||||
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
|
||||
{
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// update owner count.
|
||||
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
lockedRate >= kParityRate,
|
||||
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
|
||||
// MPTs are integral, so round the delivered amount down and
|
||||
// charge any fractional transfer fee to the escrowed amount.
|
||||
auto const delivered =
|
||||
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
|
||||
finalAmt = STAmount(amount.asset(), delivered.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
}
|
||||
return unlockEscrowMPT(
|
||||
ctx.view,
|
||||
sender,
|
||||
receiver,
|
||||
finalAmt,
|
||||
ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
|
||||
journal);
|
||||
}
|
||||
beast::Journal journal);
|
||||
|
||||
/**
|
||||
* Smart escrow kill switches, driven by the voted FeeSettings.
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
@@ -13,17 +20,81 @@
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Close a payment channel and return its remaining funds to the channel owner.
|
||||
* Check that a payment channel transaction may carry a token amount.
|
||||
*
|
||||
* @param slep The SLE for the PayChannel object to close.
|
||||
* @param view The apply view in which ledger state modifications are made.
|
||||
* @param key The ledger key identifying the PayChannel entry.
|
||||
* @param j Journal used for fatal-level diagnostic messages.
|
||||
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
|
||||
* fails; tefINTERNAL if the source account SLE cannot be found.
|
||||
* Token channels rely on fixCleanup3_2_0 and fixTokenEscrowV1, so both must be
|
||||
* enabled alongside featureTokenPaychan.
|
||||
*
|
||||
* @param rules The current ledger rules used to check amendment status.
|
||||
* @return temBAD_AMOUNT if featureTokenPaychan is not enabled; temDISABLED if
|
||||
* fixCleanup3_2_0 or fixTokenEscrowV1 is not enabled; tesSUCCESS otherwise.
|
||||
*/
|
||||
NotTEC
|
||||
checkTokenPayChanRules(Rules const& rules);
|
||||
|
||||
/**
|
||||
* Validate the token amount of a PaymentChannelCreate or PaymentChannelFund
|
||||
* transaction during preflight.
|
||||
*
|
||||
* @param amount The channel or funding amount from the transaction.
|
||||
* @return tesSUCCESS if the amount is valid; temBAD_AMOUNT or temBAD_CURRENCY
|
||||
* otherwise.
|
||||
*/
|
||||
template <ValidIssueType T>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper(STAmount const& amount);
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<Issue>(STAmount const& amount);
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<MPTIssue>(STAmount const& amount);
|
||||
|
||||
/**
|
||||
* Check that locking @p amount debits the source's trust line by exactly
|
||||
* @p amount.
|
||||
*
|
||||
* @param view The ledger view holding the source's trust line.
|
||||
* @param account The channel source whose balance is debited.
|
||||
* @param amount The amount to lock in the channel.
|
||||
* @param j Journal passed to the balance lookup.
|
||||
* @return tecPRECISION_LOSS if @p amount is an IOU and the source's balance
|
||||
* less @p amount is not exactly representable; tesSUCCESS otherwise,
|
||||
* including for XRP and MPT amounts.
|
||||
*/
|
||||
TER
|
||||
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j);
|
||||
payChanLockPrecisionHelper(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Close a payment channel and return its remaining funds to the channel owner.
|
||||
*
|
||||
* @param slep The SLE for the PayChannel object to close.
|
||||
* @param ctx The apply view context (view and transaction) in which ledger
|
||||
* state modifications are made.
|
||||
* @param key The ledger key identifying the PayChannel entry.
|
||||
* @param txAccount The account submitting the transaction that closes the
|
||||
* channel.
|
||||
* @param submitterPreFeeBalance The submitter's XRP balance before the
|
||||
* transaction fee; the source's balance when the source closes its own
|
||||
* channel and its trust line must be re-created for the refund.
|
||||
* @param j Journal used for fatal-level diagnostic messages.
|
||||
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
|
||||
* fails; tefINTERNAL if the source account SLE cannot be found.
|
||||
*/
|
||||
TER
|
||||
closeChannel(
|
||||
SLE::Ref slep,
|
||||
ApplyViewContext ctx,
|
||||
UInt256 const& key,
|
||||
AccountID const& txAccount,
|
||||
XRPAmount submitterPreFeeBalance,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* Add two uint32_t values with saturation at UINT32_MAX.
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Serialize the PaymentChannelClaim authorization message:
|
||||
* HashPrefix::PaymentChannelClaim, the channel key and the drops as a
|
||||
* 64-bit integer.
|
||||
*/
|
||||
inline void
|
||||
serializePayChanAuthorization(Serializer& msg, UInt256 const& key, XRPAmount const& amt)
|
||||
{
|
||||
@@ -15,4 +21,23 @@ serializePayChanAuthorization(Serializer& msg, UInt256 const& key, XRPAmount con
|
||||
msg.add64(amt.drops());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the PaymentChannelClaim authorization message for any asset: a
|
||||
* native amount uses the XRP layout; a token amount follows
|
||||
* HashPrefix::PaymentChannelClaim and the channel key with STAmount::add, the
|
||||
* Amount field value without its field header.
|
||||
*/
|
||||
inline void
|
||||
serializePayChanAuthorization(Serializer& msg, UInt256 const& key, STAmount const& amt)
|
||||
{
|
||||
if (amt.native())
|
||||
{
|
||||
serializePayChanAuthorization(msg, key, amt.xrp());
|
||||
return;
|
||||
}
|
||||
msg.add32(HashPrefix::PaymentChannelClaim);
|
||||
msg.addBitString(key);
|
||||
amt.add(msg);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -805,6 +805,39 @@ canAdd(STAmount const& amt1, STAmount const& amt2);
|
||||
bool
|
||||
canSubtract(STAmount const& amt1, STAmount const& amt2);
|
||||
|
||||
/**
|
||||
* Determine whether `amt1 + amt2` equals the mathematical sum.
|
||||
*
|
||||
* XRP and MPT sums are integer additions, so this is canAdd(). An IOU sum is
|
||||
* rounded to the mantissa width and is exact only if subtracting each
|
||||
* operand from it gives back the other.
|
||||
*
|
||||
* @param amt1 The first addend.
|
||||
* @param amt2 The second addend.
|
||||
* @return true if the sum is exact; false if it rounds, exceeds the largest
|
||||
* amount the asset can represent, or the amounts are not comparable.
|
||||
* Never throws.
|
||||
*/
|
||||
bool
|
||||
isExactSum(STAmount const& amt1, STAmount const& amt2);
|
||||
|
||||
/**
|
||||
* Determine whether `amt1 - amt2` equals the mathematical difference.
|
||||
*
|
||||
* XRP and MPT differences are integer subtractions, so this is canSubtract().
|
||||
* An IOU difference is rounded to the mantissa width and is exact only if
|
||||
* adding the subtrahend back gives the minuend and subtracting the difference
|
||||
* from the minuend gives back the subtrahend.
|
||||
*
|
||||
* @param amt1 The minuend.
|
||||
* @param amt2 The subtrahend.
|
||||
* @return true if the difference is exact; false if it rounds, underflows,
|
||||
* exceeds the largest amount the asset can represent, or the amounts are
|
||||
* not comparable. Never throws.
|
||||
*/
|
||||
bool
|
||||
isExactDifference(STAmount const& amt1, STAmount const& amt2);
|
||||
|
||||
/**
|
||||
* Get the scale of a Number for a given asset.
|
||||
*
|
||||
|
||||
@@ -23,6 +23,7 @@ XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultN
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchV1_2, Supported::Yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(LendingProtocolV1_1, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -376,6 +376,8 @@ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
{sfDestinationNode, SoeOptional},
|
||||
{sfTransferRate, SoeOptional},
|
||||
{sfIssuerNode, SoeOptional},
|
||||
}))
|
||||
|
||||
/** The ledger object which tracks the AMM.
|
||||
|
||||
@@ -182,7 +182,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet,
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfDestination, SoeRequired},
|
||||
{sfAmount, SoeRequired},
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfSettleDelay, SoeRequired},
|
||||
{sfPublicKey, SoeRequired},
|
||||
{sfCancelAfter, SoeOptional},
|
||||
@@ -195,7 +195,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegatio
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfChannel, SoeRequired},
|
||||
{sfAmount, SoeRequired},
|
||||
{sfAmount, SoeRequired, SoeMptSupported},
|
||||
{sfExpiration, SoeOptional},
|
||||
}))
|
||||
|
||||
@@ -205,8 +205,8 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::D
|
||||
#endif
|
||||
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({.delegable = Delegation::Delegable}), ({
|
||||
{sfChannel, SoeRequired},
|
||||
{sfAmount, SoeOptional},
|
||||
{sfBalance, SoeOptional},
|
||||
{sfAmount, SoeOptional, SoeMptSupported},
|
||||
{sfBalance, SoeOptional, SoeMptSupported},
|
||||
{sfSignature, SoeOptional},
|
||||
{sfPublicKey, SoeOptional},
|
||||
{sfCredentialIDs, SoeOptional},
|
||||
|
||||
@@ -287,6 +287,54 @@ public:
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfDestinationNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfTransferRate (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getTransferRate() const
|
||||
{
|
||||
if (hasTransferRate())
|
||||
return this->sle_->at(sfTransferRate);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfTransferRate is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasTransferRate() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfTransferRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfIssuerNode (SoeOptional)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT64::type::value_type>
|
||||
getIssuerNode() const
|
||||
{
|
||||
if (hasIssuerNode())
|
||||
return this->sle_->at(sfIssuerNode);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfIssuerNode is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasIssuerNode() const
|
||||
{
|
||||
return this->sle_->isFieldPresent(sfIssuerNode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -508,6 +556,28 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfTransferRate (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PayChannelBuilder&
|
||||
setTransferRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfTransferRate] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfIssuerNode (SoeOptional)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PayChannelBuilder&
|
||||
setIssuerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
|
||||
{
|
||||
object_[sfIssuerNode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed PayChannel wrapper.
|
||||
* @param index The ledger entry index.
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -86,6 +87,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfBalance (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -246,6 +248,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelClaimBuilder&
|
||||
@@ -257,6 +260,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfBalance (SoeOptional)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelClaimBuilder&
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -207,6 +208,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelCreateBuilder&
|
||||
|
||||
@@ -60,6 +60,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
@@ -155,6 +156,7 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set sfAmount (SoeRequired)
|
||||
* @note This field supports MPT (Multi-Purpose Token) amounts.
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PaymentChannelFundBuilder&
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ReadView;
|
||||
|
||||
/**
|
||||
* @brief Invariants for TransactionProposal ledger entries.
|
||||
*
|
||||
* - Only transaction types implementing proposal lifecycle operations may
|
||||
* create or modify proposals, and a failed one leaves no proposal effect.
|
||||
* - A proposal is deleted only by TransactionProposalCancel or together with
|
||||
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
|
||||
* to it along.
|
||||
* - Collected signer arrays remain sorted, unique, and bounded.
|
||||
*/
|
||||
class ValidTransactionProposal
|
||||
{
|
||||
// The Ticket each deleted proposal was keyed to; a proposal may go with
|
||||
// its Ticket, or by Cancel, and no other way.
|
||||
std::vector<uint256> ticketsOfDeletedProposals_;
|
||||
// The proposal each deleted Ticket was keyed to, whether or not one
|
||||
// existed; none may survive its Ticket.
|
||||
std::vector<Keylet> proposalsOfDeletedTickets_;
|
||||
std::set<uint256> deletedTickets_;
|
||||
std::uint32_t created_ = 0;
|
||||
std::uint32_t modified_ = 0;
|
||||
std::uint32_t deleted_ = 0;
|
||||
bool invalidSignerArrays_ = false;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
|
||||
const;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/FreezeInvariant.h>
|
||||
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
|
||||
@@ -483,6 +484,7 @@ using InvariantChecks = std::tuple<
|
||||
ValidAmounts,
|
||||
ValidMPTTransfer,
|
||||
ObjectHasPseudoAccount,
|
||||
ValidTransactionProposal,
|
||||
SponsorshipOwnerCountsMatch,
|
||||
SponsorshipAccountCountMatchesField>;
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class ValidMPTIssuance
|
||||
|
||||
std::uint32_t mptokensCreated_ = 0;
|
||||
std::uint32_t mptokensDeleted_ = 0;
|
||||
// holder of the last MPToken created
|
||||
std::optional<AccountID> mptokenCreatedHolder_;
|
||||
// non-MPT transactions may attempt to create
|
||||
// MPToken by an issuer
|
||||
bool mptCreatedByIssuer_ = false;
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -23,9 +23,15 @@ public:
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static bool
|
||||
checkExtraFeatures(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTAmount.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const& issue = amount.get<Issue>();
|
||||
auto const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
|
||||
auto const sleIssuer = view.read(keylet::account(issuer));
|
||||
if (!sleIssuer)
|
||||
return tecNO_ISSUER;
|
||||
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the account does not have a trustline to the issuer, return tecNO_LINE
|
||||
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
|
||||
if (!sleRippleState)
|
||||
return tecNO_LINE;
|
||||
|
||||
STAmount const balance = (*sleRippleState)[sfBalance];
|
||||
|
||||
// If balance is positive, issuer must have higher address than account
|
||||
if (balance > beast::kZero && issuer < account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If balance is negative, issuer must have lower address than account
|
||||
if (balance < beast::kZero && issuer > account)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is authorized
|
||||
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecFROZEN
|
||||
if (isFrozen(view, account, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
// If the issuer has frozen the destination, return tecFROZEN
|
||||
if (isFrozen(view, dest, issue))
|
||||
return tecFROZEN;
|
||||
|
||||
STAmount const spendableAmount =
|
||||
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the amount is not addable to the balance, return tecPRECISION_LOSS
|
||||
if (!canAdd(spendableAmount, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
AccountID const& dest,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tecNO_PERMISSION
|
||||
if (issuer == account)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
|
||||
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// If the issuer is not the same as the issuer of the mpt, return
|
||||
// tecNO_PERMISSION
|
||||
if (sleIssuance->getAccountID(sfIssuer) != issuer)
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
|
||||
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
|
||||
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is
|
||||
// authorized
|
||||
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the issuer has frozen the destination, return tecLOCKED
|
||||
if (isFrozen(view, dest, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
// If the mpt cannot be transferred, return tecNO_AUTH
|
||||
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
STAmount const spendableAmount = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<MPTIssue>(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
AuthHandling::IgnoreAuth,
|
||||
j);
|
||||
|
||||
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount <= beast::kZero)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// If the spendable amount is less than the amount, return
|
||||
// tecINSUFFICIENT_FUNDS
|
||||
if (spendableAmount < amount)
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockApplyHelper<Issue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter =
|
||||
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowLockApplyHelper<MPTIssue>(
|
||||
ApplyView& view,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
STAmount const& amount,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// Defensive: Issuer cannot create an escrow
|
||||
if (issuer == sender)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ter = lockEscrowMPT(view, sender, amount, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<Issue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the issuer has requireAuth set, check if the destination is authorized
|
||||
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has deep frozen the destination, return tecFROZEN
|
||||
if (checkFreeze &&
|
||||
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
|
||||
return tecFROZEN;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockPreclaimHelper<MPTIssue>(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
bool checkFreeze)
|
||||
{
|
||||
AccountID const& issuer = amount.getIssuer();
|
||||
// If the issuer is the same as the account, return tesSUCCESS
|
||||
if (issuer == account)
|
||||
return tesSUCCESS;
|
||||
|
||||
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
|
||||
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
|
||||
auto const sleIssuance = view.read(issuanceKey);
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// If the issuer has requireAuth set, check if the account is
|
||||
// authorized
|
||||
auto const& mptIssue = amount.get<MPTIssue>();
|
||||
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// If the issuer has frozen the account, return tecLOCKED
|
||||
if (checkFreeze && isFrozen(view, account, *sleIssuance))
|
||||
return tecLOCKED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockApplyHelper<Issue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::Ref sleDest,
|
||||
XRPAmount xrpBalance,
|
||||
STAmount const& amount,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
if (senderIssuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (receiverIssuer)
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const& issue = amount.get<Issue>();
|
||||
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
|
||||
bool const recvLow = issuer > receiver;
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && createAsset)
|
||||
{
|
||||
// Can the account cover the trust line's reserve?
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx,
|
||||
sleDest,
|
||||
xrpBalance,
|
||||
*sponsorSle,
|
||||
{.ownerCountDelta = 1},
|
||||
journal,
|
||||
tecNO_LINE_INSUF_RESERVE);
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
JLOG(journal.trace()) << "Trust line does not exist. "
|
||||
"Insufficient reserve to create line.";
|
||||
return ret;
|
||||
}
|
||||
|
||||
Currency const currency = issue.currency;
|
||||
STAmount initialBalance(issue);
|
||||
initialBalance.get<Issue>().account = noAccount();
|
||||
|
||||
if (TER const ter = trustCreate(
|
||||
ctx.view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
!sleDest->isFlag(lsfDefaultRipple), //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
*sponsorSle, // sponsor
|
||||
journal); // journal
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
ctx.view.update(sleDest);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(trustLineKey) && !receiverIssuer)
|
||||
return tecNO_LINE;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
|
||||
// validate the line limit if the account submitting txn is not the receiver
|
||||
// of the funds
|
||||
if (!createAsset)
|
||||
{
|
||||
auto const sleRippleState = ctx.view.peek(trustLineKey);
|
||||
if (!sleRippleState)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// if the issuer is the high, then we use the low limit
|
||||
// otherwise we use the high limit
|
||||
STAmount const lineLimit =
|
||||
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
|
||||
|
||||
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
// flip the sign of the line balance if the issuer is not high
|
||||
if (!recvLow)
|
||||
lineBalance.negate();
|
||||
|
||||
// add the final amount to the line balance
|
||||
lineBalance += finalAmt;
|
||||
|
||||
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
|
||||
if (lineLimit < lineBalance)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
// if destination is not the issuer then transfer funds
|
||||
if (!receiverIssuer)
|
||||
{
|
||||
auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal);
|
||||
if (!isTesSuccess(ter))
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
TER
|
||||
escrowUnlockApplyHelper<MPTIssue>(
|
||||
ApplyViewContext ctx,
|
||||
Rate lockedRate,
|
||||
SLE::Ref sleDest,
|
||||
XRPAmount xrpBalance,
|
||||
STAmount const& amount,
|
||||
AccountID const& issuer,
|
||||
AccountID const& sender,
|
||||
AccountID const& receiver,
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
auto const mptID = amount.get<MPTIssue>().getMptID();
|
||||
auto const issuanceKey = keylet::mptokenIssuance(mptID);
|
||||
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
|
||||
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
|
||||
{
|
||||
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
|
||||
if (!sponsorSle)
|
||||
return sponsorSle.error(); // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = checkReserve(
|
||||
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// update owner count.
|
||||
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
|
||||
}
|
||||
|
||||
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const xferRate = transferRate(ctx.view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
|
||||
{
|
||||
if (ctx.view.rules().enabled(fixCleanup3_4_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
lockedRate >= kParityRate,
|
||||
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
|
||||
// MPTs are integral, so round the delivered amount down and
|
||||
// charge any fractional transfer fee to the escrowed amount.
|
||||
auto const delivered =
|
||||
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
|
||||
finalAmt = STAmount(amount.asset(), delivered.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
}
|
||||
return unlockEscrowMPT(
|
||||
ctx.view,
|
||||
sender,
|
||||
receiver,
|
||||
finalAmt,
|
||||
ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
|
||||
journal);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -3,16 +3,26 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -21,9 +31,76 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
TER
|
||||
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j)
|
||||
NotTEC
|
||||
checkTokenPayChanRules(Rules const& rules)
|
||||
{
|
||||
if (!rules.enabled(featureTokenPaychan))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!rules.enabled(fixCleanup3_2_0) || !rules.enabled(fixTokenEscrowV1))
|
||||
return temDISABLED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<Issue>(STAmount const& amount)
|
||||
{
|
||||
if (amount.native() || amount <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (badCurrency() == amount.get<Issue>().currency)
|
||||
return temBAD_CURRENCY;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <>
|
||||
NotTEC
|
||||
payChanAmountPreflightHelper<MPTIssue>(STAmount const& amount)
|
||||
{
|
||||
if (amount.native() || amount <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
payChanLockPrecisionHelper(
|
||||
ReadView const& view,
|
||||
AccountID const& account,
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!amount.holds<Issue>())
|
||||
return tesSUCCESS;
|
||||
|
||||
// The trust line debit rounds to the mantissa, so require an exact
|
||||
// difference rather than canAdd's relative tolerance.
|
||||
STAmount const spendable = accountHolds(
|
||||
view,
|
||||
account,
|
||||
amount.get<Issue>().currency,
|
||||
amount.getIssuer(),
|
||||
FreezeHandling::IgnoreFreeze,
|
||||
j);
|
||||
if (!isExactDifference(spendable, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
closeChannel(
|
||||
SLE::Ref slep,
|
||||
ApplyViewContext ctx,
|
||||
UInt256 const& key,
|
||||
AccountID const& txAccount,
|
||||
XRPAmount submitterPreFeeBalance,
|
||||
beast::Journal j)
|
||||
{
|
||||
ApplyView& view = ctx.view;
|
||||
AccountID const src = (*slep)[sfAccount];
|
||||
// Remove PayChan from owner directory
|
||||
{
|
||||
@@ -38,9 +115,9 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
|
||||
}
|
||||
|
||||
// Remove PayChan from recipient's owner directory, if present.
|
||||
AccountID const dst = (*slep)[sfDestination];
|
||||
if (auto const page = (*slep)[~sfDestinationNode])
|
||||
{
|
||||
auto const dst = (*slep)[sfDestination];
|
||||
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -50,15 +127,69 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer amount back to owner, decrement owner count
|
||||
auto const sle = view.peek(keylet::account(src));
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Release the channel's reserve first: a token refund can re-create a
|
||||
// holding the owner deleted while the channel was open.
|
||||
decreaseOwnerCountForObject(view, sle, slep, 1, j);
|
||||
|
||||
XRPL_ASSERT(
|
||||
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
|
||||
decreaseOwnerCountForObject(view, sle, slep, 1, j);
|
||||
XRPL_ASSERT(
|
||||
isExactDifference((*slep)[sfAmount], (*slep)[sfBalance]),
|
||||
"xrpl::closeChannel : exact refund");
|
||||
|
||||
auto const reqDelta = (*slep)[sfAmount] - (*slep)[sfBalance];
|
||||
auto const& issuer = reqDelta.getIssuer();
|
||||
|
||||
// Only update the balance if there is a positive delta.
|
||||
if (reqDelta > beast::kZero)
|
||||
{
|
||||
if (isXRP(reqDelta))
|
||||
{
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] + reqDelta;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(view, src, reqDelta, false);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
bool const createAsset = src == txAccount;
|
||||
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx,
|
||||
kParityRate,
|
||||
sle,
|
||||
submitterPreFeeBalance,
|
||||
reqDelta,
|
||||
issuer,
|
||||
src,
|
||||
src,
|
||||
createAsset,
|
||||
j);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove PayChan from issuer's owner directory, if present.
|
||||
if (auto const optPage = (*slep)[~sfIssuerNode])
|
||||
{
|
||||
if (!view.dirRemove(keylet::ownerDir(issuer), *optPage, key, true))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j.fatal()) << "Could not remove paychan from issuer owner directory";
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
view.update(sle);
|
||||
|
||||
// Remove PayChan from ledger
|
||||
|
||||
@@ -43,6 +43,9 @@ constexpr auto kProposalTxTypes = std::to_array<TxType>({
|
||||
bool
|
||||
isValidProposalTxnType(STObject const& proposedTx)
|
||||
{
|
||||
if (!proposedTx.isFieldPresent(sfTransactionType))
|
||||
return false;
|
||||
|
||||
auto const type = proposedTx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
|
||||
|
||||
@@ -522,6 +522,64 @@ canAdd(STAmount const& a, STAmount const& b)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Integral types defer to canAdd(). For IOU amounts the rounding error of the
|
||||
* sum is a multiple of the finer operand's unit in the last place, so
|
||||
* subtracting the coarser operand back from the sum returns the finer one
|
||||
* only when that error is zero. Both differences are checked because either
|
||||
* operand may be the finer. IOUAmount throws std::overflow_error when a sum
|
||||
* or difference exceeds the largest IOU value; the exact result is then not
|
||||
* representable, so the sum is not exact.
|
||||
*/
|
||||
bool
|
||||
isExactSum(STAmount const& a, STAmount const& b)
|
||||
{
|
||||
if (!areComparable(a, b))
|
||||
return false;
|
||||
|
||||
if (a.integral())
|
||||
return canAdd(a, b);
|
||||
|
||||
try
|
||||
{
|
||||
STAmount const sum = a + b;
|
||||
return sum - a == b && sum - b == a;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integral types defer to canSubtract(). For IOU amounts each check misses
|
||||
* the case where one operand is dust against the other. With a dust
|
||||
* subtrahend, a - b rounds to a and adding b back rounds to a again, so only
|
||||
* a - diff != b shows the loss. With a dust minuend, a - b rounds to -b and
|
||||
* a - diff rounds to b, so only diff + b != a shows it. Both are checked.
|
||||
* IOUAmount throws std::overflow_error when a step rounds past the largest
|
||||
* IOU value; the difference is then not exact.
|
||||
*/
|
||||
bool
|
||||
isExactDifference(STAmount const& a, STAmount const& b)
|
||||
{
|
||||
if (!areComparable(a, b))
|
||||
return false;
|
||||
|
||||
if (a.integral())
|
||||
return canSubtract(a, b);
|
||||
|
||||
try
|
||||
{
|
||||
STAmount const diff = a - b;
|
||||
return diff + b == a && a - diff == b;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines if it is safe to subtract one STAmount from another.
|
||||
*
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
#undef TRANSACTION
|
||||
#undef TRANSACTION_INCLUDE
|
||||
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
|
||||
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include <xrpl/tx/invariants/CosignerInvariant.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
namespace {
|
||||
|
||||
bool
|
||||
validSignerArray(STArray const& signers, std::size_t limit, SField const& elementName)
|
||||
{
|
||||
if (signers.size() > limit)
|
||||
return false;
|
||||
|
||||
std::optional<AccountID> previous;
|
||||
for (auto const& signer : signers)
|
||||
{
|
||||
if (signer.getFName() != elementName || !signer.isFieldPresent(sfAccount))
|
||||
return false;
|
||||
|
||||
auto const account = signer.getAccountID(sfAccount);
|
||||
if (previous && account <= *previous)
|
||||
return false;
|
||||
previous = account;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
validNestedSigners(STObject const& signature)
|
||||
{
|
||||
return !signature.isFieldPresent(sfSigners) ||
|
||||
validSignerArray(signature.getFieldArray(sfSigners), STTx::kMaxMultiSigners, sfSigner);
|
||||
}
|
||||
|
||||
bool
|
||||
validSignerArrays(STObject const& proposedTx)
|
||||
{
|
||||
if (!validNestedSigners(proposedTx))
|
||||
return false;
|
||||
|
||||
for (auto const* field :
|
||||
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
|
||||
{
|
||||
if (proposedTx.isFieldPresent(*field) &&
|
||||
!validNestedSigners(proposedTx.getFieldObject(*field)))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!proposedTx.isFieldPresent(sfBatchSigners))
|
||||
return true;
|
||||
|
||||
auto const& batchSigners = proposedTx.getFieldArray(sfBatchSigners);
|
||||
if (!validSignerArray(batchSigners, kMaxBatchSigners, sfBatchSigner))
|
||||
return false;
|
||||
|
||||
return std::ranges::all_of(
|
||||
batchSigners, [](auto const& batchSigner) { return validNestedSigners(batchSigner); });
|
||||
}
|
||||
|
||||
// The Ticket a proposal is keyed to: the one its proposed transaction spends.
|
||||
uint256
|
||||
proposalTicketKey(SLE const& proposal)
|
||||
{
|
||||
auto const& proposedTx = proposal.getFieldObject(sfProposedTransaction);
|
||||
return keylet::ticket(
|
||||
proposedTx.getAccountID(sfAccount),
|
||||
SeqProxy::rawTicket(proposedTx.getFieldU32(sfTicketSequence)))
|
||||
.key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
ValidTransactionProposal::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
|
||||
{
|
||||
// `after` is always present (on deletion it holds the final state), so it
|
||||
// alone determines the entry type. LedgerEntryTypesMatch owns malformed
|
||||
// type transitions; skip them rather than read one side with the other's
|
||||
// schema.
|
||||
if (!after || (before && before->getType() != after->getType()))
|
||||
return;
|
||||
|
||||
auto const type = after->getType();
|
||||
|
||||
// Consuming a Ticket deletes the proposal keyed to it (XLS-0103 §4.5).
|
||||
// Remember each Ticket that went, so finalize can tell a proposal deleted
|
||||
// with its Ticket from one that simply vanished, and the proposal each
|
||||
// Ticket was keyed to, so finalize can check that none stayed behind.
|
||||
if (type == ltTICKET)
|
||||
{
|
||||
if (isDelete)
|
||||
{
|
||||
deletedTickets_.insert(after->key());
|
||||
proposalsOfDeletedTickets_.push_back(
|
||||
keylet::txProposal(
|
||||
after->getAccountID(sfAccount), after->getFieldU32(sfTicketSequence)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != ltTRANSACTION_PROPOSAL)
|
||||
return;
|
||||
|
||||
if (!before)
|
||||
{
|
||||
++created_;
|
||||
}
|
||||
else if (isDelete)
|
||||
{
|
||||
++deleted_;
|
||||
ticketsOfDeletedProposals_.push_back(proposalTicketKey(*before));
|
||||
}
|
||||
else
|
||||
{
|
||||
++modified_;
|
||||
}
|
||||
|
||||
// A live proposal's signer lists must stay submittable: within size
|
||||
// limits, sorted by account, and free of duplicates.
|
||||
if (!isDelete && !validSignerArrays(after->getFieldObject(sfProposedTransaction)))
|
||||
invalidSignerArrays_ = true;
|
||||
}
|
||||
|
||||
bool
|
||||
ValidTransactionProposal::finalize(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) const
|
||||
{
|
||||
if (!view.rules().enabled(featureCosign))
|
||||
return true;
|
||||
|
||||
if (invalidSignerArrays_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: TransactionProposal signer arrays are not canonical.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A proposal leaves the ledger in two ways (XLS-0103 §4.5): a
|
||||
// TransactionProposalCancel deletes the proposal it names, and consuming
|
||||
// the Ticket a proposal is keyed to deletes the proposal with it, whether
|
||||
// the proposed transaction executed (or failed with a claimed-fee tec) or
|
||||
// AccountDelete swept the target's Tickets.
|
||||
std::uint32_t deletedWithTicket = 0;
|
||||
for (auto const& ticketKey : ticketsOfDeletedProposals_)
|
||||
{
|
||||
if (deletedTickets_.contains(ticketKey))
|
||||
++deletedWithTicket;
|
||||
}
|
||||
|
||||
bool const succeeded = isTesSuccess(result);
|
||||
bool effectsMatch = false;
|
||||
if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CREATE)
|
||||
{
|
||||
effectsMatch = created_ == (succeeded ? 1u : 0u) && modified_ == 0 && deleted_ == 0;
|
||||
}
|
||||
else if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CANCEL)
|
||||
{
|
||||
// A Cancel deletes one proposal and nothing else. It cannot
|
||||
// spend a reserved Ticket, so no proposal goes with its Ticket here.
|
||||
effectsMatch = created_ == 0 && modified_ == 0 && deletedWithTicket == 0 &&
|
||||
deleted_ == (succeeded ? 1u : 0u);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Any other transaction deletes a proposal only together with its
|
||||
// Ticket, and modifies at most one, only as a successful
|
||||
// SponsorshipTransfer.
|
||||
// TODO: extend this when TransactionProposalSign is implemented.
|
||||
std::uint32_t const modifiable =
|
||||
tx.getTxnType() == ttSPONSORSHIP_TRANSFER && succeeded ? 1 : 0;
|
||||
effectsMatch = created_ == 0 && modified_ <= modifiable && deleted_ == deletedWithTicket;
|
||||
}
|
||||
|
||||
if (!effectsMatch)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal changes do not match transaction result.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The other direction of the deletion rule: ticketDelete deletes the
|
||||
// proposal keyed to a Ticket along with it (XLS-0103 §4.5), so no
|
||||
// proposal may outlive its Ticket, orphaned on a key that can no longer
|
||||
// be spent.
|
||||
bool const orphaned = std::ranges::any_of(
|
||||
proposalsOfDeletedTickets_, [&view](Keylet const& keylet) { return view.exists(keylet); });
|
||||
if (orphaned)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: TransactionProposal outlived the Ticket it is keyed to.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -150,7 +150,8 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
|
||||
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:
|
||||
if (isXRP((*before)[sfAmount]))
|
||||
@@ -183,7 +184,7 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
|
||||
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:
|
||||
@@ -935,8 +936,12 @@ ValidClawback::finalize(
|
||||
return !mptV2Enabled;
|
||||
}
|
||||
|
||||
// IOU balances keep 16 significant digits, so a clawback
|
||||
// much smaller than the balance changes it by a rounded
|
||||
// amount, possibly zero. Compare with the same rounded
|
||||
// subtraction the balance update performs.
|
||||
if (*afterBalance > *beforeBalance ||
|
||||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
|
||||
*afterBalance != *beforeBalance - std::min(*beforeBalance, clawAmount))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: trustline clawback balance change is invalid";
|
||||
|
||||
@@ -125,6 +125,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
|
||||
else if (!before)
|
||||
{
|
||||
mptokensCreated_++;
|
||||
mptokenCreatedHolder_ = after->at(sfAccount);
|
||||
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
|
||||
if (mptIssue.getIssuer() == after->at(sfAccount))
|
||||
mptCreatedByIssuer_ = true;
|
||||
@@ -439,6 +440,13 @@ ValidMPTIssuance::finalize(
|
||||
return true;
|
||||
}
|
||||
|
||||
// A token paychan claim may auto-create the submitter's MPToken.
|
||||
if (rules.enabled(featureTokenPaychan) && txnType == ttPAYCHAN_CLAIM &&
|
||||
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensDeleted_ == 0 &&
|
||||
(mptokensCreated_ == 0 ||
|
||||
(mptokensCreated_ == 1 && mptokenCreatedHolder_ == tx[sfAccount])))
|
||||
return true;
|
||||
|
||||
if (hasPrivilege(tx, Privilege::MayDeleteMpt) &&
|
||||
((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) &&
|
||||
mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
|
||||
|
||||
@@ -254,6 +254,15 @@ private:
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const;
|
||||
|
||||
// If clobQuality is available and has a better quality then return nullopt,
|
||||
// otherwise if amm liquidity is available return AMM offer adjusted based
|
||||
// on clobQuality.
|
||||
@@ -810,10 +819,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
|
||||
try
|
||||
{
|
||||
// All arithmetic in this block runs before the offer is consumed.
|
||||
// A crafted MPTokensV2 offer can overflow while transfer rates or
|
||||
// crossing limits are applied; remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source.
|
||||
// crossing limits are applied, here or in the callback's
|
||||
// limitStepIn()/limitStepOut(); remove that unusable offer instead
|
||||
// of letting it persist as a tecINTERNAL source. Overflows while
|
||||
// consuming the offer are handled by tryConsumeOffer().
|
||||
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
|
||||
|
||||
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
|
||||
@@ -941,16 +951,9 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
removeOffer("Removing offer with overflowing amount calculation");
|
||||
return true;
|
||||
}
|
||||
// An overflow can only be produced by a crafted MPT offer, and MPT
|
||||
// offers require featureMPTokensV2 (enforced at OfferCreate
|
||||
// preflight). So the amendment is always enabled when we get here
|
||||
// and this legacy re-throw is unreachable in practice.
|
||||
// LCOV_EXCL_START
|
||||
XRPL_ASSERT(
|
||||
sb.rules().enabled(featureMPTokensV2),
|
||||
"xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
|
||||
// Legacy behavior: the exception escapes flow() and the
|
||||
// transaction fails with tecINTERNAL.
|
||||
throw;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
};
|
||||
|
||||
@@ -991,6 +994,32 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
return {offers.permToRemove(), counter.count()};
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
BookStep<TIn, TOut, TDerived>::tryConsumeOffer(
|
||||
PaymentSandbox& sb,
|
||||
Offer<TIn, TOut>& offer,
|
||||
TAmounts<TIn, TOut> const& ofrAmt,
|
||||
TAmounts<TIn, TOut> const& stepAmt,
|
||||
TOut const& ownerGives) const
|
||||
{
|
||||
try
|
||||
{
|
||||
// Under MPTokensV2, an overflow while consuming the offer fails the
|
||||
// strand instead of being handled by forEachOffer().
|
||||
consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives);
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
if (sb.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
Throw<FlowException>(tecINTERNAL, "Overflow while consuming offer");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
template <class TIn, class TOut, class TDerived>
|
||||
template <template <typename, typename> typename Offer>
|
||||
void
|
||||
@@ -1192,7 +1221,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(stpAmt.out);
|
||||
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
|
||||
remainingOut = out - result.out;
|
||||
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
this->tryConsumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
|
||||
// return true b/c even if the payment is satisfied,
|
||||
// we need to consume the offer. An offer limited by what the
|
||||
// issuer can issue is not consumed, though: its remainder is still
|
||||
@@ -1224,7 +1253,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
|
||||
savedOuts.insert(remainingOut);
|
||||
result.in = sum(savedIns);
|
||||
result.out = out;
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// Explicitly check whether the offer is funded. Given that we have
|
||||
// (stpAmt.out > remainingOut), it's natural to assume the offer
|
||||
@@ -1408,7 +1437,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
|
||||
savedOuts = std::move(savedOutsAdj);
|
||||
result = resultAdj;
|
||||
remainingIn = in - result.in;
|
||||
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
|
||||
|
||||
// When the mantissas of two iou amounts differ by less than ten, then
|
||||
// subtracting them leaves a result of zero. This can cause the check
|
||||
@@ -1512,6 +1541,18 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
|
||||
return temBAD_PATH;
|
||||
}
|
||||
|
||||
// An issuer paying with its own MPT issues it into the first book. A
|
||||
// holder-sourced strand records the MPT in the first MPTEndpointStep and
|
||||
// so rejects a later book that outputs it again; do the same here. The
|
||||
// step after such a book issues the MPT a second time and the payment
|
||||
// engine cannot keep the two within MaximumAmount once StrandFlow
|
||||
// rebuilds the sandbox on a limiting step.
|
||||
if (ctx.isFirst && book_.in.holds<MPTIssue>() && book_.in.getIssuer() == ctx.strandSrc &&
|
||||
ctx.view.rules().enabled(featureMPTokensV2))
|
||||
{
|
||||
ctx.seenDirectAssets[0].insert(book_.in);
|
||||
}
|
||||
|
||||
// Do not allow two books to output the same issue. This may cause offers on
|
||||
// one step to unfund offers in another step.
|
||||
if (!ctx.seenBookOuts.insert(book_.out).second ||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -874,15 +875,18 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
|
||||
// pure issue/redeem can't be frozen (issuer/holder)
|
||||
// For the first step: check global freeze of the step's own asset.
|
||||
// For the last step: check only the per-holder MPToken lock.
|
||||
// For the last step: check only the per-holder lock.
|
||||
// Global freeze of the deliver asset is not checked here
|
||||
// because MPT semantics allow issuer<->holder transfers even when globally
|
||||
// locked — only holder-to-holder DEX paths are restricted.
|
||||
// For both steps: a vault share inherits the freeze of the vault's
|
||||
// underlying asset.
|
||||
if (!(ctx.isLast && ctx.isFirst))
|
||||
{
|
||||
auto const& account = ctx.isFirst ? src_ : dst_;
|
||||
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_);
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_) ||
|
||||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
|
||||
if (frozen)
|
||||
return terLOCKED;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
@@ -45,11 +43,9 @@ EscrowCancel::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -133,21 +129,19 @@ EscrowCancel::doApply()
|
||||
|
||||
auto const issuer = amount.getIssuer();
|
||||
bool const createAsset = account == accountID_;
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
kParityRate,
|
||||
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account, // sender and receiver are the same
|
||||
account,
|
||||
createAsset,
|
||||
j_);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
kParityRate,
|
||||
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account, // sender and receiver are the same
|
||||
account,
|
||||
createAsset,
|
||||
j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
|
||||
#include <memory>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -153,9 +152,8 @@ EscrowCreate::preflight(PreflightContext const& ctx)
|
||||
if (!ctx.rules.enabled(featureTokenEscrow))
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); },
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit(
|
||||
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); });
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -256,11 +254,9 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (!ctx.view.rules().enabled(featureTokenEscrow))
|
||||
return temDISABLED; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -423,11 +419,9 @@ EscrowCreate::doApply()
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
{
|
||||
return ret; // LCOV_EXCL_LINE
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <system_error>
|
||||
#include <variant>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -242,11 +241,9 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
@@ -431,21 +428,19 @@ EscrowFinish::doApply()
|
||||
: kParityRate;
|
||||
auto const issuer = amount.getIssuer();
|
||||
bool const createAsset = destID == accountID_;
|
||||
if (auto const ret = std::visit(
|
||||
[&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
lockedRate,
|
||||
sled,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account,
|
||||
destID,
|
||||
createAsset,
|
||||
j_);
|
||||
},
|
||||
amount.asset().value());
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
lockedRate,
|
||||
sled,
|
||||
preFeeBalance_,
|
||||
amount,
|
||||
issuer,
|
||||
account,
|
||||
destID,
|
||||
createAsset,
|
||||
j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -46,11 +50,29 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
|
||||
auto const bal = ctx.tx[~sfBalance];
|
||||
if (bal && (!isXRP(*bal) || *bal <= beast::kZero))
|
||||
if (bal && *bal <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
auto const amt = ctx.tx[~sfAmount];
|
||||
if (amt && (!isXRP(*amt) || *amt <= beast::kZero))
|
||||
if (amt && *amt <= beast::kZero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if ((bal && !isXRP(*bal)) || (amt && !isXRP(*amt)))
|
||||
{
|
||||
if (auto const ret = checkTokenPayChanRules(ctx.rules); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
// PaymentChannelCreate and PaymentChannelFund gate an MPT-denominated
|
||||
// Amount on featureMPTokensV1 (checkExtraFeatures); apply the same
|
||||
// gate here for sfBalance and sfAmount.
|
||||
if (((bal && bal->holds<MPTIssue>()) || (amt && amt->holds<MPTIssue>())) &&
|
||||
!ctx.rules.enabled(featureMPTokensV1))
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
// Both bal and amt must reference the same asset before comparing,
|
||||
// otherwise STAmount comparison throws.
|
||||
if (bal && amt && bal->asset() != amt->asset())
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (bal && amt && *bal > *amt)
|
||||
@@ -70,8 +92,8 @@ PaymentChannelClaim::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;
|
||||
@@ -96,12 +118,42 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx)
|
||||
TER
|
||||
PaymentChannelClaim::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureCredentials))
|
||||
return Transactor::preclaim(ctx);
|
||||
if (ctx.view.rules().enabled(featureCredentials))
|
||||
{
|
||||
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
}
|
||||
|
||||
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
if (ctx.view.rules().enabled(featureTokenPaychan))
|
||||
{
|
||||
Keylet const k{ltPAYCHAN, ctx.tx[sfChannel]};
|
||||
auto const slep = ctx.view.read(k);
|
||||
if (!slep)
|
||||
return tecNO_TARGET;
|
||||
|
||||
AccountID const dest = (*slep)[sfDestination];
|
||||
auto const& chanFunds = slep->getFieldAmount(sfAmount);
|
||||
|
||||
// The requested balance and the optional amount must each match
|
||||
// the channel's asset; otherwise the STAmount comparisons and
|
||||
// subtractions in doApply would throw on mismatched issues. amount
|
||||
// is checked independently of balance: a claim can carry Amount
|
||||
// without Balance (the signature-authorized ceiling alone).
|
||||
if (auto const bal = ctx.tx[~sfBalance]; bal && bal->asset() != chanFunds.asset())
|
||||
return tecWRONG_ASSET;
|
||||
if (auto const amt = ctx.tx[~sfAmount]; amt && amt->asset() != chanFunds.asset())
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
if (!isXRP(chanFunds) && ctx.tx.isFieldPresent(sfBalance))
|
||||
{
|
||||
if (auto const ret = chanFunds.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, chanFunds);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -116,25 +168,36 @@ PaymentChannelClaim::doApply()
|
||||
|
||||
AccountID const src = (*slep)[sfAccount];
|
||||
AccountID const dst = (*slep)[sfDestination];
|
||||
AccountID const txAccount = ctx_.tx[sfAccount];
|
||||
|
||||
auto const curExpiration = (*slep)[~sfExpiration];
|
||||
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
|
||||
isChannelExpired(ctx_.view(), curExpiration))
|
||||
{
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
return closeChannel(
|
||||
slep,
|
||||
ctx_.getApplyViewContext(),
|
||||
k.key,
|
||||
accountID_,
|
||||
preFeeBalance_,
|
||||
ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
if (txAccount != src && txAccount != dst)
|
||||
if (accountID_ != src && accountID_ != dst)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
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])
|
||||
// Asset consistency between the requested balance/amount and the
|
||||
// channel was validated in preclaim.
|
||||
XRPL_ASSERT(
|
||||
reqBalance.asset() == chanFunds.asset(),
|
||||
"xrpl::PaymentChannelClaim::doApply : balance matches channel asset");
|
||||
|
||||
if (accountID_ == dst && !ctx_.tx[~sfSignature])
|
||||
{
|
||||
return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION}
|
||||
: TER{temBAD_SIGNATURE};
|
||||
@@ -159,27 +222,64 @@ PaymentChannelClaim::doApply()
|
||||
return tecUNFUNDED_PAYMENT;
|
||||
}
|
||||
|
||||
// The payout reqBalance - sfBalance and the unclaimed remainder
|
||||
// sfAmount - reqBalance must both be exact so that the amounts
|
||||
// unlocked by this claim and by the close equal what sfBalance and
|
||||
// sfAmount record.
|
||||
if (!isExactDifference(chanFunds, reqBalance) ||
|
||||
!isExactDifference(reqBalance, chanBalance))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
auto const sled = ctx_.view().peek(keylet::account(dst));
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
if (auto err =
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal);
|
||||
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, dst, sled, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
(*slep)[sfBalance] = ctx_.tx[sfBalance];
|
||||
XRPAmount const reqDelta = reqBalance - chanBalance;
|
||||
STAmount const reqDelta = reqBalance - chanBalance;
|
||||
XRPL_ASSERT(
|
||||
reqDelta >= beast::kZero, "xrpl::PaymentChannelClaim::doApply : minimum balance delta");
|
||||
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
|
||||
|
||||
// Transfer amount to destination
|
||||
if (isXRP(reqDelta))
|
||||
{
|
||||
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
|
||||
}
|
||||
else
|
||||
{
|
||||
Rate lockedRate = slep->isFieldPresent(sfTransferRate)
|
||||
? xrpl::Rate(slep->getFieldU32(sfTransferRate))
|
||||
: kParityRate;
|
||||
auto const& issuer = reqDelta.getIssuer();
|
||||
bool const createAsset = dst == accountID_;
|
||||
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowUnlockApplyHelper<T>(
|
||||
ctx_.getApplyViewContext(),
|
||||
lockedRate,
|
||||
sled,
|
||||
preFeeBalance_,
|
||||
reqDelta,
|
||||
issuer,
|
||||
src,
|
||||
dst,
|
||||
createAsset,
|
||||
j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
ctx_.view().update(sled);
|
||||
ctx_.view().update(slep);
|
||||
}
|
||||
|
||||
if (ctx_.tx.isFlag(tfRenew))
|
||||
{
|
||||
if (src != txAccount)
|
||||
if (src != accountID_)
|
||||
return tecNO_PERMISSION;
|
||||
(*slep)[~sfExpiration] = std::nullopt;
|
||||
ctx_.view().update(slep);
|
||||
@@ -188,8 +288,16 @@ PaymentChannelClaim::doApply()
|
||||
if (ctx_.tx.isFlag(tfClose))
|
||||
{
|
||||
// Channel will close immediately if dry or the receiver closes
|
||||
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
if (dst == accountID_ || (*slep)[sfBalance] == (*slep)[sfAmount])
|
||||
{
|
||||
return closeChannel(
|
||||
slep,
|
||||
ctx_.getApplyViewContext(),
|
||||
k.key,
|
||||
accountID_,
|
||||
preFeeBalance_,
|
||||
ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
auto const settleExpiration = saturatingAdd(
|
||||
ctx_.view().rules(),
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
#include <xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h>
|
||||
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
|
||||
#include <xrpl/ledger/helpers/SponsorHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -51,14 +58,37 @@ namespace xrpl {
|
||||
TxConsequences
|
||||
PaymentChannelCreate::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::kZero};
|
||||
}
|
||||
|
||||
bool
|
||||
PaymentChannelCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Only require featureMPTokensV1 when the channel amount is an MPT and
|
||||
// fixCleanup3_2_0 is active; XRP/IOU channels are unaffected by this gate.
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
|
||||
return ctx.rules.enabled(featureMPTokensV1);
|
||||
return true;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
PaymentChannelCreate::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero))
|
||||
auto const amount = ctx.tx[sfAmount];
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = checkTokenPayChanRules(ctx.rules); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = amount.asset().visit(
|
||||
[&]<typename T>(T const&) { return payChanAmountPreflightHelper<T>(amount); });
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
else if (amount <= beast::kZero)
|
||||
{
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
|
||||
return temDST_IS_SRC;
|
||||
@@ -77,6 +107,8 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
auto const amount = ctx.tx[sfAmount];
|
||||
|
||||
// Check reserve and funds availability
|
||||
if (!ctx.view.rules().enabled(featureSponsor))
|
||||
{
|
||||
@@ -86,15 +118,15 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
|
||||
if (balance < reserve + ctx.tx[sfAmount])
|
||||
if (isXRP(amount) && balance < reserve + ctx.tx[sfAmount])
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
auto const dst = ctx.tx[sfDestination];
|
||||
auto const dest = ctx.tx[sfDestination];
|
||||
|
||||
{
|
||||
// Check destination account
|
||||
auto const sled = ctx.view.read(keylet::account(dst));
|
||||
auto const sled = ctx.view.read(keylet::account(dest));
|
||||
if (!sled)
|
||||
return tecNO_DST;
|
||||
|
||||
@@ -115,6 +147,24 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
// Vault shares (an MPT issued by a pseudo-account) cannot be locked
|
||||
// in a channel.
|
||||
if (amount.holds<MPTIssue>() && isPseudoAccount(ctx.view, amount.getIssuer()))
|
||||
return tecWRONG_ASSET;
|
||||
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = payChanLockPrecisionHelper(ctx.view, account, amount, ctx.j);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -122,6 +172,7 @@ TER
|
||||
PaymentChannelCreate::doApply()
|
||||
{
|
||||
auto const account = ctx_.tx[sfAccount];
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
auto const sle = ctx_.view().peek(keylet::account(account));
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
@@ -159,25 +210,27 @@ PaymentChannelCreate::doApply()
|
||||
// - unsponsored: 1 — source owes reserve including the new increment.
|
||||
auto const sourceReserve = accountReserve(
|
||||
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
|
||||
if (preFeeBalance_ - ctx_.tx[sfAmount].xrp() < sourceReserve)
|
||||
// Token channels lock no XRP, so only an XRP channel checks the
|
||||
// source's post-lock balance, as EscrowCreate does.
|
||||
if (isXRP(amount) && preFeeBalance_ - amount.xrp() < sourceReserve)
|
||||
return tecUNFUNDED;
|
||||
}
|
||||
|
||||
auto const dst = ctx_.tx[sfDestination];
|
||||
auto const dest = ctx_.tx[sfDestination];
|
||||
|
||||
// Create PayChan in ledger.
|
||||
//
|
||||
// Note that we use the value from the sequence or ticket as the
|
||||
// payChan sequence. For more explanation see comments in SeqProxy.h.
|
||||
Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqProxy());
|
||||
Keylet const payChanKeylet = keylet::payChannel(account, dest, ctx_.tx.getSeqProxy());
|
||||
auto const slep = std::make_shared<SLE>(payChanKeylet);
|
||||
|
||||
// Funds held in this channel
|
||||
(*slep)[sfAmount] = ctx_.tx[sfAmount];
|
||||
(*slep)[sfAmount] = amount;
|
||||
// Amount channel has already paid
|
||||
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
|
||||
(*slep)[sfBalance] = amount.zeroed();
|
||||
(*slep)[sfAccount] = account;
|
||||
(*slep)[sfDestination] = dst;
|
||||
(*slep)[sfDestination] = dest;
|
||||
(*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
|
||||
(*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
|
||||
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
|
||||
@@ -188,6 +241,13 @@ PaymentChannelCreate::doApply()
|
||||
(*slep)[sfSequence] = ctx_.tx.getSeqProxy().value();
|
||||
}
|
||||
|
||||
if (ctx_.view().rules().enabled(featureTokenPaychan) && !isXRP(amount))
|
||||
{
|
||||
auto const xferRate = transferRate(ctx_.view(), amount);
|
||||
if (xferRate != kParityRate)
|
||||
(*slep)[sfTransferRate] = xferRate.value;
|
||||
}
|
||||
|
||||
ctx_.view().insert(slep);
|
||||
|
||||
// Add PayChan to owner directory
|
||||
@@ -202,14 +262,37 @@ PaymentChannelCreate::doApply()
|
||||
// Add PayChan to the recipient's owner directory
|
||||
{
|
||||
auto const page =
|
||||
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
|
||||
ctx_.view().dirInsert(keylet::ownerDir(dest), payChanKeylet, describeOwnerDir(dest));
|
||||
if (!page)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*slep)[sfDestinationNode] = *page;
|
||||
}
|
||||
|
||||
// Add PayChan to the issuer's owner directory, if applicable
|
||||
auto const& issuer = amount.getIssuer();
|
||||
if (!isXRP(amount) && issuer != accountID_ && issuer != dest && !amount.holds<MPTIssue>())
|
||||
{
|
||||
auto page = ctx_.view().dirInsert(
|
||||
keylet::ownerDir(issuer), payChanKeylet, describeOwnerDir(issuer));
|
||||
if (!page)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
(*slep)[sfIssuerNode] = *page;
|
||||
}
|
||||
|
||||
// Deduct owner's balance, increment owner count
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
|
||||
if (isXRP(amount))
|
||||
{
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
|
||||
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
|
||||
ctx_.view().update(sle);
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/EscrowHelpers.h>
|
||||
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Concepts.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -20,12 +24,24 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
TxConsequences
|
||||
PaymentChannelFund::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::kZero};
|
||||
}
|
||||
|
||||
bool
|
||||
PaymentChannelFund::checkExtraFeatures(PreflightContext const& ctx)
|
||||
{
|
||||
// Only require featureMPTokensV1 when the funding amount is an MPT and
|
||||
// fixCleanup3_2_0 is active; XRP/IOU channels are unaffected by this gate.
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
|
||||
return ctx.rules.enabled(featureMPTokensV1);
|
||||
return true;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
@@ -34,8 +50,40 @@ PaymentChannelFund::preflight(PreflightContext const& ctx)
|
||||
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero)
|
||||
return temMALFORMED;
|
||||
|
||||
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero))
|
||||
auto const amount = ctx.tx[sfAmount];
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
if (auto const ret = checkTokenPayChanRules(ctx.rules); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (auto const ret = amount.asset().visit(
|
||||
[&]<typename T>(T const&) { return payChanAmountPreflightHelper<T>(amount); });
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
else if (amount <= beast::kZero)
|
||||
{
|
||||
return temBAD_AMOUNT;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
PaymentChannelFund::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (ctx.view.rules().enabled(featureTokenPaychan))
|
||||
{
|
||||
Keylet const k{ltPAYCHAN, ctx.tx[sfChannel]};
|
||||
auto const slep = ctx.view.read(k);
|
||||
if (!slep)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
// The funded asset must match the channel's asset; otherwise the
|
||||
// STAmount accumulation in doApply would throw on mismatched issues.
|
||||
if (ctx.tx[sfAmount].asset() != (*slep)[sfAmount].asset())
|
||||
return tecWRONG_ASSET;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -49,16 +97,22 @@ PaymentChannelFund::doApply()
|
||||
return tecNO_ENTRY;
|
||||
|
||||
AccountID const src = (*slep)[sfAccount];
|
||||
auto const txAccount = ctx_.tx[sfAccount];
|
||||
AccountID const dst = (*slep)[sfDestination];
|
||||
auto const curExpiration = (*slep)[~sfExpiration];
|
||||
|
||||
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
|
||||
isChannelExpired(ctx_.view(), curExpiration))
|
||||
{
|
||||
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
|
||||
return closeChannel(
|
||||
slep,
|
||||
ctx_.getApplyViewContext(),
|
||||
k.key,
|
||||
accountID_,
|
||||
preFeeBalance_,
|
||||
ctx_.registry.get().getJournal("View"));
|
||||
}
|
||||
|
||||
if (src != txAccount)
|
||||
if (src != accountID_)
|
||||
{
|
||||
// only the owner can add funds or extend
|
||||
return tecNO_PERMISSION;
|
||||
@@ -82,10 +136,22 @@ PaymentChannelFund::doApply()
|
||||
ctx_.view().update(slep);
|
||||
}
|
||||
|
||||
auto const sle = ctx_.view().peek(keylet::account(txAccount));
|
||||
auto const sle = ctx_.view().peek(keylet::account(accountID_));
|
||||
if (!sle)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const amount = ctx_.tx[sfAmount];
|
||||
auto const& chanAmt = slep->getFieldAmount(sfAmount);
|
||||
|
||||
// Asset consistency between the funded amount and the channel was
|
||||
// validated in preclaim.
|
||||
XRPL_ASSERT(
|
||||
amount.asset() == chanAmt.asset(),
|
||||
"xrpl::PaymentChannelFund::doApply : amount matches channel asset");
|
||||
|
||||
// A token fund locks no XRP and adds no owned object, so only an XRP fund
|
||||
// checks the source's XRP reserve.
|
||||
if (isXRP(amount))
|
||||
{
|
||||
// Check reserve and funds availability
|
||||
STAmount const balance = (*sle)[sfBalance];
|
||||
@@ -100,20 +166,59 @@ PaymentChannelFund::doApply()
|
||||
// when a sponsor is present and would ignore the source's post-lock
|
||||
// balance entirely. Funding an existing channel adds no owned object,
|
||||
// so there is no owner-count delta.
|
||||
if (balance < accountReserve(ctx_.view(), sle, j_) + ctx_.tx[sfAmount])
|
||||
if (balance < accountReserve(ctx_.view(), sle, j_) + 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)))
|
||||
if (!ctx_.view().read(keylet::account(dst)))
|
||||
{
|
||||
return tecNO_DST;
|
||||
}
|
||||
|
||||
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
|
||||
ctx_.view().update(slep);
|
||||
if (!isXRP(amount))
|
||||
{
|
||||
// Funding is subject to the same issuer controls (locking opt-in,
|
||||
// authorization, freeze/lock, transferability, spendable balance)
|
||||
// as channel creation.
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockPreclaimHelper<T>(ctx_.view(), accountID_, dst, amount, j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
|
||||
if (auto const ret = payChanLockPrecisionHelper(ctx_.view(), accountID_, amount, j_);
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
// sfAmount must become exactly chanAmt + amount: an IOU sum that
|
||||
// rounds or an MPT sum that overflows would record an increase other
|
||||
// than what the source paid.
|
||||
if (!isExactSum(chanAmt, amount))
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
// The unclaimed remainder sfAmount - sfBalance must stay exact so
|
||||
// that close refunds exactly what was not paid out.
|
||||
if (!isExactDifference(chanAmt + amount, (*slep)[sfBalance]))
|
||||
return tecPRECISION_LOSS;
|
||||
}
|
||||
|
||||
if (isXRP(amount))
|
||||
{
|
||||
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const& issuer = amount.getIssuer();
|
||||
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
|
||||
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
|
||||
});
|
||||
!isTesSuccess(ret))
|
||||
return ret;
|
||||
}
|
||||
|
||||
(*slep)[sfAmount] = chanAmt + amount;
|
||||
ctx_.view().update(slep);
|
||||
ctx_.view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
@@ -37,7 +37,7 @@ TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// Never read the zero keylet. Preflight already rejected a zero
|
||||
// ProposalID, and no real proposal can have one (every ProposalID is a
|
||||
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
|
||||
// hash), so reaching here with zero is an internal error (and a mathematical rarity).
|
||||
if (proposalID == beast::kZero)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
|
||||
@@ -4362,7 +4362,7 @@ private:
|
||||
settleDelay,
|
||||
pk,
|
||||
cancelAfter),
|
||||
Ter(telENV_RPC_FAILED));
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
env(create(carol_, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter),
|
||||
Ter(tecNO_PERMISSION));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/CaptureLogs.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
@@ -14,13 +15,17 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
@@ -803,6 +808,108 @@ class Clawback_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testSubResolutionAmount(FeatureBitset features)
|
||||
{
|
||||
testcase("Sub-resolution amount");
|
||||
using namespace test::jtx;
|
||||
|
||||
// IOU balances keep 16 significant digits. When alice claws back an
|
||||
// amount much smaller than bob's balance, the balance changes by the
|
||||
// rounded amount, which can be zero, less, or more than requested. The
|
||||
// clawback invariant must accept that instead of failing the
|
||||
// transaction under MPTokensV2 or logging a failure without it.
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
struct Case
|
||||
{
|
||||
std::uint64_t balanceMantissa;
|
||||
int balanceExponent;
|
||||
std::uint64_t clawMantissa;
|
||||
int clawExponent;
|
||||
std::uint64_t afterMantissa;
|
||||
int afterExponent;
|
||||
};
|
||||
std::array const cases{
|
||||
// 1e7 - 1e-10 rounds back to 1e7
|
||||
Case{
|
||||
.balanceMantissa = 10'000'000,
|
||||
.balanceExponent = 0,
|
||||
.clawMantissa = 1,
|
||||
.clawExponent = -10,
|
||||
.afterMantissa = 10'000'000,
|
||||
.afterExponent = 0,
|
||||
},
|
||||
// 1234567890.123456 - 0.0000005 rounds back to the balance
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 5,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'456,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
// 0.0000014 removes 0.000001
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 14,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'455,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
// 0.0000016 removes 0.000002
|
||||
Case{
|
||||
.balanceMantissa = 1'234'567'890'123'456,
|
||||
.balanceExponent = -6,
|
||||
.clawMantissa = 16,
|
||||
.clawExponent = -7,
|
||||
.afterMantissa = 1'234'567'890'123'454,
|
||||
.afterExponent = -6,
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const feat : {features, features - featureMPTokensV2})
|
||||
{
|
||||
for (auto const& c : cases)
|
||||
{
|
||||
std::string logs;
|
||||
{
|
||||
Env env(*this, feat, std::make_unique<test::CaptureLogs>(&logs));
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const usd = alice["USD"];
|
||||
|
||||
env(fset(alice, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
env.trust(usd(10'000'000'000), bob);
|
||||
env(pay(
|
||||
alice, bob, STAmount{usd.issue(), c.balanceMantissa, c.balanceExponent}));
|
||||
env.close();
|
||||
|
||||
env(claw(
|
||||
alice,
|
||||
STAmount{
|
||||
Issue{usd.currency, bob.id()}, c.clawMantissa, c.clawExponent}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(bob, usd).value() ==
|
||||
STAmount(usd.issue(), c.afterMantissa, c.afterExponent));
|
||||
}
|
||||
// Without MPTokensV2 a failed invariant only logs and the
|
||||
// transaction still succeeds, so check the logs instead.
|
||||
if (!feat[featureMPTokensV2])
|
||||
BEAST_EXPECT(!logs.contains("Invariant failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTickets(FeatureBitset features)
|
||||
{
|
||||
@@ -872,6 +979,7 @@ class Clawback_test : public beast::unit_test::Suite
|
||||
testDeleteDefaultLine(features);
|
||||
testFrozenLine(features);
|
||||
testAmountExceedsAvailable(features);
|
||||
testSubResolutionAmount(features);
|
||||
testTickets(features);
|
||||
}
|
||||
|
||||
|
||||
@@ -2295,6 +2295,83 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIOUUnlockCreditRoundsAtBalancePrecision(FeatureBitset features)
|
||||
{
|
||||
testcase("IOU Unlock Credit Rounds At Balance Precision");
|
||||
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"];
|
||||
STAmount const limit{usd.issue(), 1, 21};
|
||||
STAmount const large{usd.issue(), 1, 20};
|
||||
|
||||
// The unlock credits 1 to a balance of exactly 1e20; the sum keeps 16
|
||||
// significant digits, so the balance stays 1e20
|
||||
BEAST_EXPECT(large + usd(1) == large);
|
||||
|
||||
// EscrowCancel: the refund of 1 to alice is lost to rounding
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10'000), alice, bob, gw);
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
env.trust(limit, alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(1)));
|
||||
env.close();
|
||||
|
||||
// The nonzero limit keeps alice's line after she escrows all of it
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, usd(1)),
|
||||
escrow::kFinishTime(env.now() + 1s),
|
||||
escrow::kCancelTime(env.now() + 2s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(0));
|
||||
|
||||
env(pay(gw, alice, large));
|
||||
env.close();
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, usd) == large);
|
||||
|
||||
env(escrow::cancel(alice, alice, seq), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == large);
|
||||
}
|
||||
|
||||
// EscrowFinish: the payout of 1 to bob is lost to rounding
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10'000), alice, bob, gw);
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.close();
|
||||
env.trust(usd(1), alice);
|
||||
env.trust(limit, bob);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(1)));
|
||||
env(pay(gw, bob, large));
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow::create(alice, bob, usd(1)), escrow::kFinishTime(env.now() + 1s));
|
||||
env.close();
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, usd) == large);
|
||||
|
||||
env(escrow::finish(bob, alice, seq), Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
|
||||
BEAST_EXPECT(env.balance(alice, usd) == usd(0));
|
||||
BEAST_EXPECT(env.balance(bob, usd) == large);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTEnablement(FeatureBitset features)
|
||||
{
|
||||
@@ -4300,6 +4377,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
|
||||
testIOUFreeze(features);
|
||||
testIOUInsufficientFunds(features);
|
||||
testIOUPrecisionLoss(features);
|
||||
testIOUUnlockCreditRoundsAtBalancePrecision(features);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1215,6 +1215,61 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.balance(bob, y) == y(0));
|
||||
}
|
||||
|
||||
void
|
||||
testConsumeOfferOverflow(FeatureBitset features)
|
||||
{
|
||||
// An overflow while consuming an offer fails the strand under
|
||||
// MPTokensV2, and the transaction with tecINTERNAL otherwise. Either
|
||||
// way no balances change and the offer stays on the book.
|
||||
testcase("BookStep consumeOffer overflow");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gw("gateway");
|
||||
Account const eurGW("eur_gateway");
|
||||
Account const maker("maker");
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
|
||||
for (auto const& testFeatures : {features, features - featureMPTokensV2})
|
||||
{
|
||||
Env env(*this, testFeatures);
|
||||
|
||||
auto const withV2 = testFeatures[featureMPTokensV2];
|
||||
|
||||
env.fund(XRP(10'000), gw, eurGW, maker, alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const usd = gw["USD"];
|
||||
auto const eur = eurGW["EUR"];
|
||||
STAmount const usdMax{usd.issue(), STAmount::kMaxValue, STAmount::kMaxOffset};
|
||||
STAmount const usdIn{usd.issue(), 1, 90};
|
||||
|
||||
env.trust(usdMax, maker, alice);
|
||||
env.trust(eur(1'000), maker, bob);
|
||||
env.close();
|
||||
|
||||
env(pay(gw, maker, usdMax));
|
||||
env(pay(gw, alice, usdIn));
|
||||
env(pay(eurGW, maker, eur(100)));
|
||||
env(offer(maker, usdIn, eur(100)));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, eur(100)),
|
||||
Path(~eur),
|
||||
Sendmax(usdIn),
|
||||
Ter(withV2 ? TER{tecPATH_PARTIAL} : TER{tecINTERNAL}));
|
||||
env.close();
|
||||
|
||||
env.require(
|
||||
Balance(bob, eur(0)),
|
||||
Balance(maker, eur(100)),
|
||||
Balance(maker, usdMax),
|
||||
Balance(alice, usdIn));
|
||||
BEAST_EXPECT(isOffer(env, maker, usdIn, eur(100)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFalseDry(FeatureBitset features)
|
||||
{
|
||||
@@ -3008,6 +3063,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testBookStepInputAboveMaximumAmount(features);
|
||||
testBookStepInputAboveMaximumAmountWithFee(features);
|
||||
testBookStepMissingIssuance(features);
|
||||
testConsumeOfferOverflow(features);
|
||||
testSelfPayment1(features);
|
||||
testSelfPayment2(features);
|
||||
testSelfFundedXRPEndpoint(false, features);
|
||||
@@ -3016,6 +3072,85 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testReExecuteDirectStep(features);
|
||||
testSelfPayLowQualityOffer(features);
|
||||
testLockedMidPathHolder(features);
|
||||
testIssuerSourceMPTLoop(features);
|
||||
}
|
||||
|
||||
void
|
||||
testIssuerSourceMPTLoop(FeatureBitset features)
|
||||
{
|
||||
// An issuer paying with its own MPT may not route it back through a
|
||||
// book that outputs the same MPT. It would issue at the first book
|
||||
// and again after the second, and with the issuer's own offer in
|
||||
// the second book the engine could not keep the sum within
|
||||
// MaximumAmount. Holders already get temBAD_PATH_LOOP for this.
|
||||
testcase("Issuer-sourced payment looping through its own MPT");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const c1{"c1"};
|
||||
Account const e1{"e1"};
|
||||
Account const dan{"dan"};
|
||||
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(1'000), gw, alice, c1, e1, dan);
|
||||
env.close();
|
||||
|
||||
MPT const x =
|
||||
MPTTester({.env = env, .issuer = gw, .holders = {alice, c1, e1, dan}, .maxAmt = 100});
|
||||
auto const eur = gw["EUR"];
|
||||
auto const usd = gw["USD"];
|
||||
env(trust(e1, eur(1'000)));
|
||||
env(trust(e1, usd(1'000)));
|
||||
env(trust(dan, eur(1'000)));
|
||||
env(pay(gw, e1, eur(1'000)));
|
||||
env(pay(gw, e1, usd(1'000)));
|
||||
env(pay(gw, alice, x(10)));
|
||||
env.close();
|
||||
|
||||
// Liquidity for every path below, arranged so nothing crosses at
|
||||
// creation.
|
||||
env(offer(gw, XRP(60), x(30))); // issuer sells x for XRP
|
||||
env(offer(gw, usd(100), x(100))); // issuer sells x for usd
|
||||
env.close();
|
||||
env(offer(c1, x(100), XRP(100))); // buys x for XRP
|
||||
env(offer(e1, XRP(70), usd(70))); // sells usd for XRP
|
||||
env(offer(e1, x(100), eur(100))); // sells eur for x
|
||||
env.close();
|
||||
BEAST_EXPECT(expectOffers(env, gw, 2));
|
||||
BEAST_EXPECT(expectOffers(env, c1, 1));
|
||||
BEAST_EXPECT(expectOffers(env, e1, 2));
|
||||
|
||||
auto const flags = Txflags(tfNoRippleDirect | tfPartialPayment);
|
||||
|
||||
// x -> XRP -> x, the issuer's own offer feeds the endpoint
|
||||
env(pay(gw, dan, x(100)), Sendmax(x(100)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
|
||||
// x -> XRP -> x -> eur, the issuer's own offer feeds a third book
|
||||
env(pay(gw, dan, eur(100)),
|
||||
Sendmax(x(100)),
|
||||
Path(~XRP, ~x, ~eur),
|
||||
flags,
|
||||
Ter(temBAD_PATH_LOOP));
|
||||
// x -> XRP -> usd -> x, a limiting book before the issuer's own offer
|
||||
env(pay(gw, dan, x(100)),
|
||||
Sendmax(x(100)),
|
||||
Path(~XRP, ~usd, ~x),
|
||||
flags,
|
||||
Ter(temBAD_PATH_LOOP));
|
||||
// Same loop from a holder is already rejected
|
||||
env(pay(alice, dan, x(10)), Sendmax(x(10)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(gw, x) == x(-10));
|
||||
BEAST_EXPECT(env.balance(c1, x) == x(0));
|
||||
BEAST_EXPECT(env.balance(dan, x) == x(0));
|
||||
|
||||
// The issuer paying with another asset may still take its own offer
|
||||
env(pay(gw, dan, x(50)), Sendmax(usd(50)), Path(~x), flags);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(dan, x) == x(50));
|
||||
BEAST_EXPECT(env.balance(gw, x) == x(-60));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -994,8 +994,9 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
test::jtx::MPT const mpT1("MPT", makeMptID(env.seq(alice) + 10, alice));
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecOBJECT_NOT_FOUND);
|
||||
env(pay(alice, carol, mpT1(100)), Sendmax(mpt(100)), err);
|
||||
// "paths" is invalid in V1
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tesSUCCESS);
|
||||
// "paths" is invalid in V1. In V2 any path from the issuer back
|
||||
// to its own MPT is a loop, like it is for a holder.
|
||||
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(temBAD_PATH_LOOP);
|
||||
env(pay(alice, carol, mpt(100)), Path(~usd), err);
|
||||
}
|
||||
|
||||
@@ -2009,35 +2010,6 @@ class MPToken_test : public beast::unit_test::Suite
|
||||
};
|
||||
ammBid(sfBidMin);
|
||||
ammBid(sfBidMax);
|
||||
// PaymentChannelCreate
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelCreate;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[jss::Destination] = carol.human();
|
||||
jv[jss::SettleDelay] = 1;
|
||||
jv[sfPublicKey.fieldName] = strHex(alice.pk().slice());
|
||||
jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
|
||||
test(jv, jss::Amount.cStr());
|
||||
}
|
||||
// PaymentChannelFund
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelFund;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfChannel.fieldName] = to_string(UInt256{1});
|
||||
jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
|
||||
test(jv, jss::Amount.cStr());
|
||||
}
|
||||
// PaymentChannelClaim
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelClaim;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfChannel.fieldName] = to_string(UInt256{1});
|
||||
jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
|
||||
test(jv, jss::Amount.cStr());
|
||||
}
|
||||
// NFTokenCreateOffer
|
||||
{
|
||||
json::Value jv;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+125
-116
@@ -99,27 +99,31 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
|
||||
|
||||
{
|
||||
auto const preAlice = env.balance(alice);
|
||||
env(fund(alice, chan, XRP(1000)));
|
||||
env(paychan::fund(alice, chan, XRP(1000)));
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
|
||||
}
|
||||
|
||||
auto chanBal = channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = channelAmount(*env.current(), chan);
|
||||
auto chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
BEAST_EXPECT(chanBal == XRP(0));
|
||||
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));
|
||||
// A token amount needs TokenPaychan (temBAD_AMOUNT) and
|
||||
// fixCleanup3_2_0, which this suite disables (temDISABLED).
|
||||
auto const tokenResult =
|
||||
features[featureTokenPaychan] ? Ter(temDISABLED) : Ter(temBAD_AMOUNT);
|
||||
env(create(alice, bob, usda(1000), settleDelay, pk), tokenResult);
|
||||
env(fund(alice, chan, usda(1000)), tokenResult);
|
||||
env(create(alice, bob, XRP(-1000), settleDelay, pk), Ter(temBAD_AMOUNT));
|
||||
env(fund(alice, chan, XRP(-1000)), Ter(temBAD_AMOUNT));
|
||||
}
|
||||
@@ -140,9 +144,13 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const iou = usda(100).value();
|
||||
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));
|
||||
// A token Balance or Amount needs TokenPaychan (temBAD_AMOUNT) and
|
||||
// fixCleanup3_2_0, which this suite disables (temDISABLED).
|
||||
auto const tokenResult =
|
||||
features[featureTokenPaychan] ? Ter(temDISABLED) : Ter(temBAD_AMOUNT);
|
||||
env(claim(alice, chan, iou, iou), tokenResult);
|
||||
env(claim(alice, chan, posXRP, iou), tokenResult);
|
||||
env(claim(alice, chan, iou, posXRP), tokenResult);
|
||||
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));
|
||||
@@ -162,9 +170,9 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const reqBal = chanBal + delta;
|
||||
auto const authAmt = reqBal + XRP(100);
|
||||
assert(reqBal <= chanAmt);
|
||||
env(claim(alice, chan, reqBal, authAmt));
|
||||
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
|
||||
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
|
||||
env(paychan::claim(alice, chan, reqBal, authAmt));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal);
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + delta);
|
||||
chanBal = reqBal;
|
||||
}
|
||||
@@ -324,14 +332,14 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
NetClock::time_point const cancelAfter =
|
||||
env.current()->header().parentCloseTime + 3600s;
|
||||
auto const channelFunds = XRP(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 const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
|
||||
env.close(cancelAfter);
|
||||
{
|
||||
// dst cannot claim after cancelAfter
|
||||
auto const chanBal = channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = channelAmount(*env.current(), chan);
|
||||
auto const chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
auto const preAlice = env.balance(alice);
|
||||
auto const preBob = env.balance(bob);
|
||||
auto const delta = XRP(500);
|
||||
@@ -341,7 +349,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const sig = signClaimAuth(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(!paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
|
||||
BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds);
|
||||
}
|
||||
@@ -355,9 +363,9 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
NetClock::time_point const cancelAfter =
|
||||
env.current()->header().parentCloseTime + 3600s;
|
||||
auto const channelFunds = XRP(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 const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
|
||||
// third party close before cancelAfter
|
||||
env(claim(carol, chan), Txflags(tfClose), Ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan));
|
||||
@@ -424,9 +432,9 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const minExpiration = closeTime + settleDelay;
|
||||
NetClock::time_point const cancelAfter = closeTime + 7200s;
|
||||
auto const channelFunds = XRP(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 const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
|
||||
// Owner closes, will close after settleDelay
|
||||
env(claim(alice, chan), Txflags(tfClose));
|
||||
@@ -455,7 +463,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}),
|
||||
Ter(temBAD_EXPIRATION));
|
||||
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
|
||||
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration}));
|
||||
env(paychan::fund(alice, chan, XRP(1), NetClock::time_point{minExpiration}));
|
||||
env.close(minExpiration);
|
||||
// Try to extend the expiration after the expiration has already passed
|
||||
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 1000s}));
|
||||
@@ -477,17 +485,17 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
NetClock::time_point const settleTimepoint =
|
||||
env.current()->header().parentCloseTime + settleDelay;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(paychan::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 const chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
auto const preBob = env.balance(bob);
|
||||
auto const delta = XRP(500);
|
||||
auto const reqBal = chanBal + delta;
|
||||
@@ -503,8 +511,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
env.close(settleTimepoint);
|
||||
{
|
||||
// past settleTime, channel will close
|
||||
auto const chanBal = channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = channelAmount(*env.current(), chan);
|
||||
auto const chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
auto const preAlice = env.balance(alice);
|
||||
auto const preBob = env.balance(bob);
|
||||
auto const delta = XRP(500);
|
||||
@@ -533,17 +541,17 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(paychan::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);
|
||||
env(claim(alice, chan, channelFunds.value(), channelFunds.value()));
|
||||
BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds);
|
||||
env(paychan::claim(alice, chan, channelFunds.value(), channelFunds.value()));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == channelFunds);
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + channelFunds);
|
||||
}
|
||||
auto const preAlice = env.balance(alice);
|
||||
@@ -568,15 +576,15 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(paychan::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 const chanAmt = channelAmount(*env.current(), chan);
|
||||
auto chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
auto const preBob = env.balance(bob);
|
||||
|
||||
auto const delta = XRP(500);
|
||||
@@ -591,8 +599,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
}
|
||||
{
|
||||
// Claim again
|
||||
auto chanBal = channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = channelAmount(*env.current(), chan);
|
||||
auto chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
auto const preBob = env.balance(bob);
|
||||
|
||||
auto const delta = XRP(500);
|
||||
@@ -623,9 +631,9 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env(fset(bob, asfDisallowXRP));
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), 3600s, alice.pk()));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk()));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -634,13 +642,13 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// since it is just advisory.
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), 3600s, alice.pk()));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk()));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
|
||||
|
||||
env(fset(bob, asfDisallowXRP));
|
||||
auto const reqBal = XRP(500).value();
|
||||
env(claim(alice, chan, reqBal, reqBal));
|
||||
env(paychan::claim(alice, chan, reqBal, reqBal));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,16 +700,16 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
|
||||
|
||||
// alice can add more funds to the channel even though bob has
|
||||
// asfDepositAuth set.
|
||||
env(fund(alice, chan, XRP(1000)));
|
||||
env(paychan::fund(alice, chan, XRP(1000)));
|
||||
env.close();
|
||||
|
||||
// alice claims. Fails because bob's lsfDepositAuth flag is set.
|
||||
@@ -729,7 +737,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// 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(paychan::claim(bob, chan, delta, delta, Slice(sig), pk));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + delta - baseFee);
|
||||
}
|
||||
@@ -760,7 +768,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
env(deposit::auth(bob, alice));
|
||||
env.close();
|
||||
|
||||
env(claim(alice, chan, delta, delta, Slice(sig), pk));
|
||||
env(paychan::claim(alice, chan, delta, delta, Slice(sig), pk));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + delta - (3 * baseFee));
|
||||
@@ -782,7 +790,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
|
||||
// alice claims successfully.
|
||||
env(claim(alice, chan, delta, delta));
|
||||
env(paychan::claim(alice, chan, delta, delta));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + XRP(800) - (5 * baseFee));
|
||||
}
|
||||
@@ -810,12 +818,12 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
|
||||
// alice add funds to the channel
|
||||
env(fund(alice, chan, XRP(1000)));
|
||||
env(paychan::fund(alice, chan, XRP(1000)));
|
||||
env.close();
|
||||
|
||||
std::string const credBadIdx =
|
||||
@@ -906,12 +914,12 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
|
||||
// alice add funds to the channel
|
||||
env(fund(alice, chan, XRP(1000)));
|
||||
env(paychan::fund(alice, chan, XRP(1000)));
|
||||
env.close();
|
||||
|
||||
auto const delta = XRP(500).value();
|
||||
@@ -939,11 +947,11 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
|
||||
env(fund(alice, chan, XRP(1000)));
|
||||
env(paychan::fund(alice, chan, XRP(1000)));
|
||||
env.close();
|
||||
std::string const credIdx =
|
||||
"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B"
|
||||
@@ -975,12 +983,12 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan1 = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan1));
|
||||
auto const chan2 = channel(alice, bob, env.seq(alice));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(channelExists(*env.current(), chan2));
|
||||
auto const chan1 = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan1));
|
||||
auto const chan2 = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan2));
|
||||
BEAST_EXPECT(chan1 != chan2);
|
||||
}
|
||||
|
||||
@@ -999,8 +1007,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
env.close();
|
||||
{
|
||||
// test account non-string
|
||||
@@ -1106,8 +1114,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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));
|
||||
auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
env.close();
|
||||
{
|
||||
auto const r = env.rpc("account_channels", alice.human(), bob.human());
|
||||
@@ -1157,7 +1165,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const channelFunds = XRP(1);
|
||||
for (auto const& b : bobs)
|
||||
{
|
||||
env(create(alice, b, channelFunds, settleDelay, alice.pk()));
|
||||
env(paychan::create(alice, b, channelFunds, settleDelay, alice.pk()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1281,8 +1289,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// channels where alice is the source, not the destination
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
env(create(alice, bob, channelFunds, settleDelay, alice.pk()));
|
||||
env(create(bob, alice, channelFunds, settleDelay, bob.pk()));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, alice.pk()));
|
||||
env(paychan::create(bob, alice, channelFunds, settleDelay, bob.pk()));
|
||||
|
||||
auto const r = [&] {
|
||||
json::Value jvc;
|
||||
@@ -1309,8 +1317,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
env.close();
|
||||
|
||||
json::Value args{json::ValueType::Object};
|
||||
@@ -1344,8 +1352,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 3600s;
|
||||
auto const channelFunds = XRP(1000);
|
||||
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
|
||||
env(create(alice, bob, channelFunds, settleDelay, pk));
|
||||
auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
env.close();
|
||||
std::string chan1PkStr;
|
||||
{
|
||||
@@ -1367,8 +1375,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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));
|
||||
auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
|
||||
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
|
||||
env.close();
|
||||
{
|
||||
auto const r = env.rpc("account_channels", alice.human(), bob.human());
|
||||
@@ -1836,8 +1844,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
|
||||
auto const chan = channel(alice, bob, env.seq(alice));
|
||||
auto jv = create(alice, bob, XRP(1000), settleDelay, pk);
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
auto jv = paychan::create(alice, bob, XRP(1000), settleDelay, pk);
|
||||
auto const pkHex = strHex(pk.slice());
|
||||
jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2);
|
||||
env(jv, Ter(temMALFORMED));
|
||||
@@ -1915,7 +1923,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// Test with adding the paychan to the recipient's owner directory
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob);
|
||||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle));
|
||||
@@ -1937,7 +1945,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
Env env(*this, features);
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
// create the channel before the amendment activates
|
||||
env(create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob);
|
||||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle));
|
||||
@@ -1990,18 +1998,18 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
// 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, XRP(1000), settleDelay, pk));
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
|
||||
env.close();
|
||||
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
|
||||
|
||||
rmAccount(env, alice, carol, tecHAS_OBLIGATIONS);
|
||||
rmAccount(env, bob, carol, TER(tecHAS_OBLIGATIONS));
|
||||
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
auto chanBal = channelBalance(*env.current(), chan);
|
||||
auto chanAmt = channelAmount(*env.current(), chan);
|
||||
auto chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
BEAST_EXPECT(chanBal == XRP(0));
|
||||
BEAST_EXPECT(chanAmt == XRP(1000));
|
||||
|
||||
@@ -2065,15 +2073,15 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
auto const chan = channel(alice, bob, aliceTicketSeq);
|
||||
auto const chan = paychan::channel(alice, bob, aliceTicketSeq);
|
||||
|
||||
env(create(alice, bob, XRP(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) == XRP(0));
|
||||
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
|
||||
|
||||
{
|
||||
auto const preAlice = env.balance(alice);
|
||||
@@ -2086,8 +2094,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
|
||||
}
|
||||
|
||||
auto chanBal = channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = channelAmount(*env.current(), chan);
|
||||
auto chanBal = paychan::channelBalance(*env.current(), chan);
|
||||
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
|
||||
BEAST_EXPECT(chanBal == XRP(0));
|
||||
BEAST_EXPECT(chanAmt == XRP(2000));
|
||||
|
||||
@@ -2103,8 +2111,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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(paychan::channelBalance(*env.current(), chan) == reqBal);
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + delta);
|
||||
chanBal = reqBal;
|
||||
}
|
||||
@@ -2122,8 +2130,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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);
|
||||
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal);
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
|
||||
chanBal = reqBal;
|
||||
@@ -2138,8 +2146,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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(paychan::channelBalance(*env.current(), chan) == chanBal);
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
|
||||
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
|
||||
}
|
||||
{
|
||||
@@ -2158,8 +2166,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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(paychan::channelBalance(*env.current(), chan) == chanBal);
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
|
||||
BEAST_EXPECT(env.balance(bob) == preBob);
|
||||
}
|
||||
|
||||
@@ -2169,8 +2177,8 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
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(paychan::channelBalance(*env.current(), chan) == chanBal);
|
||||
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
|
||||
|
||||
{
|
||||
// Dst closes channel
|
||||
@@ -2181,7 +2189,7 @@ struct PayChan_test : public beast::unit_test::Suite
|
||||
env.require(Tickets(bob, env.seq(bob) - bobTicketSeq));
|
||||
BEAST_EXPECT(env.seq(bob) == bobSeq);
|
||||
|
||||
BEAST_EXPECT(!channelExists(*env.current(), chan));
|
||||
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
|
||||
auto const feeDrops = env.current()->fees().base;
|
||||
auto const delta = chanAmt - chanBal;
|
||||
assert(delta > beast::kZero);
|
||||
@@ -2232,6 +2240,7 @@ public:
|
||||
// pre-amendment behavior, so run it with the amendment disabled.
|
||||
FeatureBitset const all{testableAmendments() - fixCleanup3_2_0};
|
||||
testWithFeats(all);
|
||||
testWithFeats(all - featureTokenPaychan);
|
||||
testDepositAuthCreds();
|
||||
testMetaAndOwnership(all - fixIncludeKeyletFields);
|
||||
}
|
||||
|
||||
@@ -4353,6 +4353,42 @@ public:
|
||||
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
|
||||
}
|
||||
|
||||
// A sponsored token channel locks no XRP, so a source below its own
|
||||
// reserve before the fee may create one, as it may a token escrow.
|
||||
if (cosigning)
|
||||
{
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const gw("gw");
|
||||
auto const usd = gw["USD"];
|
||||
env.fund(XRP(1000000), alice, bob, sponsor, gw);
|
||||
env.close();
|
||||
env(fset(gw, asfAllowTrustLineLocking));
|
||||
env.trust(usd(10000), alice);
|
||||
env.close();
|
||||
env(pay(gw, alice, usd(1000)));
|
||||
env.close();
|
||||
|
||||
adjustAccountXRPBalance(env, alice, reserve(env, ownerCount(env, alice)));
|
||||
env(noop(alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice) < reserve(env, ownerCount(env, alice)));
|
||||
|
||||
env(escrow::create(alice, bob, usd(100)),
|
||||
escrow::kFinishTime(env.now() + 100s),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const chan = paychan::channel(alice, bob, env.seq(alice));
|
||||
env(paychan::create(alice, bob, usd(100), 10s, alice.pk()),
|
||||
sponsor::As(sponsor, spfSponsorReserve),
|
||||
Sig(sfSponsorSignature, sponsor),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -215,8 +215,9 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
}
|
||||
|
||||
// The target account may refuse any proposal made for it, at any point in
|
||||
// its lifecycle, without owning the object: anyone can create a proposal
|
||||
// against any account and tie up one of its tickets (On-Chain Cosigner spec §7.2).
|
||||
// its lifecycle, without owning the object: any of its signers, an
|
||||
// authorized delegate, or that delegate's signers can create one and tie
|
||||
// up one of its tickets (XLS-0103 §7.2).
|
||||
void
|
||||
testTargetCancel(FeatureBitset features)
|
||||
{
|
||||
@@ -683,6 +684,25 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
|
||||
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
|
||||
}
|
||||
|
||||
// Paying with a ticket changes none of that. The failed cancel still
|
||||
// consumes carol's ticket, and nothing else moves: the proposal and
|
||||
// its reserve stay as they were.
|
||||
{
|
||||
uint256 const proposalID = makeProposal(env, alice, target, bob);
|
||||
std::uint32_t const cancelTicket = proposal::createTicket(env, carol);
|
||||
auto const aliceOwnerCount = ownerCount(env, alice);
|
||||
|
||||
env(proposal::cancel(carol, proposalID),
|
||||
ticket::Use(cancelTicket),
|
||||
Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
|
||||
BEAST_EXPECT(!env.le(keylet::ticket(carol.id(), SeqProxy::rawTicket(cancelTicket))));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
|
||||
BEAST_EXPECT(ownerCount(env, carol) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// A Cancel works as an inner Batch transaction, where each inner runs its
|
||||
|
||||
@@ -1357,6 +1357,51 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
|
||||
env.close();
|
||||
}
|
||||
|
||||
// A proposer may pay for the Create with one of its own Tickets. The
|
||||
// Ticket's reserve goes as the proposal's arrives, so the proposer's owner
|
||||
// count moves by the difference.
|
||||
void
|
||||
testTicketedCreate(FeatureBitset features)
|
||||
{
|
||||
testcase("create paid for with the proposer's own ticket");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
Account const alice{"alice"}; // the proposer
|
||||
Account const target{"target"}; // the account the proposal is for
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, target, bob);
|
||||
env.close();
|
||||
proposal::authorizeProposer(env, target, alice);
|
||||
|
||||
std::uint32_t const targetTicketSeq = proposal::createTicket(env, target);
|
||||
std::uint32_t const aliceTicketSeq = proposal::createTicket(env, alice);
|
||||
std::uint32_t const aliceSeq = env.seq(alice);
|
||||
std::uint32_t const targetOwnerCount = ownerCount(env, target);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// The proposal's reserve lands on alice less the Ticket she paid with,
|
||||
// and her Sequence is untouched.
|
||||
env(proposal::create(
|
||||
alice,
|
||||
proposal::unsignedPayload(env, pay(target, bob, XRP(1)), targetTicketSeq),
|
||||
proposal::expiration(env, 100s)),
|
||||
ticket::Use(aliceTicketSeq),
|
||||
proposal::verify::create());
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(proposal::entry(env, target, targetTicketSeq));
|
||||
BEAST_EXPECT(!env.le(keylet::ticket(alice.id(), SeqProxy::rawTicket(aliceTicketSeq))));
|
||||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
|
||||
|
||||
// The target's Ticket, the one the proposal is keyed to, stays.
|
||||
BEAST_EXPECT(ownerCount(env, target) == targetOwnerCount);
|
||||
}
|
||||
|
||||
// The proposal's reserve can instead be sponsored: the reserve is charged
|
||||
// to the sponsor's account, and the ledger object records the sponsor, the
|
||||
// same as any other reserve-sponsorable object (TransactionProposalCreate
|
||||
@@ -1728,6 +1773,7 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
|
||||
testOtherTransactionTypes(all);
|
||||
testAuxiliaryCoSignatureTypes(all);
|
||||
testReserve(all);
|
||||
testTicketedCreate(all);
|
||||
testSponsoredReserve(all);
|
||||
testSponsorshipTransfer(all);
|
||||
testFeeSponsored(all);
|
||||
|
||||
@@ -970,6 +970,48 @@ class InvariantsMPT_test : public InvariantsBase
|
||||
});
|
||||
}
|
||||
|
||||
// A PaymentChannelClaim may create only the submitter's (A1) MPToken.
|
||||
{
|
||||
STTx const claim{ttPAYCHAN_CLAIM, [](STObject&) {}};
|
||||
MPTID id;
|
||||
Preclose const precloseIssuance = [&](Account const&, Account const&, Env& env) {
|
||||
Account const gw("gw");
|
||||
env.fund(XRP(1'000), gw);
|
||||
MPTTester const mpt({.env = env, .issuer = gw});
|
||||
id = mpt.issuanceID();
|
||||
return true;
|
||||
};
|
||||
auto const insertToken = [&](Account const& holder, ApplyContext& ac) {
|
||||
auto const sleNew = std::make_shared<SLE>(keylet::mptoken(id, holder));
|
||||
(*sleNew)[sfAccount] = holder.id();
|
||||
(*sleNew)[sfMPTokenIssuanceID] = id;
|
||||
ac.view().insert(sleNew);
|
||||
return true;
|
||||
};
|
||||
|
||||
doInvariantCheck(
|
||||
{{"a MPToken was created"}},
|
||||
[&](Account const&, Account const& a2, ApplyContext& ac) {
|
||||
return insertToken(a2, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
claim,
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseIssuance,
|
||||
TxAccount::A1);
|
||||
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const& a1, Account const&, ApplyContext& ac) {
|
||||
return insertToken(a1, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
claim,
|
||||
{tesSUCCESS, tesSUCCESS},
|
||||
precloseIssuance,
|
||||
TxAccount::A1);
|
||||
}
|
||||
|
||||
// LoanSet / VaultWithdraw MayAuthorizeMpt caps (fixCleanup3_4_0) and
|
||||
// the LoanBrokerDelete cap (fixCleanup3_5_0): LoanSet allows at most
|
||||
// two creates and no deletes; VaultWithdraw and LoanBrokerDelete allow
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
#include <test/app/invariants/InvariantsBase.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/proposal.h>
|
||||
#include <test/jtx/utility.h>
|
||||
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/ProposalHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <source_location>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class InvariantsTransactionProposal_test : public InvariantsBase
|
||||
{
|
||||
void
|
||||
testTransactionProposalInvariants()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
// Submits a real proposal, so the entry is in the closed ledger before
|
||||
// the precheck runs and the precheck can modify it.
|
||||
std::optional<Keylet> liveProposalKeylet;
|
||||
std::optional<std::uint32_t> liveTicketSeq;
|
||||
Preclose const precloseLiveProposal =
|
||||
[&](Account const& owner, Account const& destination, Env& env) {
|
||||
auto const ticketSeq = proposal::createTicket(env, owner);
|
||||
auto const proposedTx =
|
||||
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
|
||||
liveProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
|
||||
liveTicketSeq = ticketSeq;
|
||||
env(proposal::create(owner, proposedTx, proposal::expiration(env, 100s)));
|
||||
return BEAST_EXPECT(env.le(*liveProposalKeylet));
|
||||
};
|
||||
|
||||
auto peekLiveProposal = [&](ApplyContext& ac) -> SLE::pointer {
|
||||
if (!liveProposalKeylet)
|
||||
return nullptr;
|
||||
return ac.view().peek(*liveProposalKeylet);
|
||||
};
|
||||
|
||||
// Deletes the live proposal as deleteProposal would: out of the
|
||||
// owner's directory, with the owner's reserve released.
|
||||
auto deleteLiveProposal = [&](Account const& owner, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
if (!account)
|
||||
return false;
|
||||
account->at(sfOwnerCount) -=
|
||||
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
|
||||
ac.view().update(account);
|
||||
if (!ac.view().dirRemove(
|
||||
keylet::ownerDir(owner.id()), sle->getFieldU64(sfOwnerNode), sle->key(), false))
|
||||
return false;
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Consumes one of the owner's Tickets as ticketDelete would.
|
||||
auto deleteTicket = [&](Account const& owner, std::uint32_t ticketSeq, ApplyContext& ac) {
|
||||
auto const ticket =
|
||||
ac.view().peek(keylet::ticket(owner.id(), SeqProxy::rawTicket(ticketSeq)));
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
if (!ticket || !account)
|
||||
return false;
|
||||
if (!ac.view().dirRemove(
|
||||
keylet::ownerDir(owner.id()),
|
||||
ticket->getFieldU64(sfOwnerNode),
|
||||
ticket->key(),
|
||||
false))
|
||||
return false;
|
||||
if (auto const ticketCount = account->getFieldU32(sfTicketCount); ticketCount > 1)
|
||||
{
|
||||
account->setFieldU32(sfTicketCount, ticketCount - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
account->makeFieldAbsent(sfTicketCount);
|
||||
}
|
||||
account->at(sfOwnerCount) -= 1;
|
||||
ac.view().update(account);
|
||||
ac.view().erase(ticket);
|
||||
return true;
|
||||
};
|
||||
|
||||
// A transaction outside the whitelist may not modify a proposal at
|
||||
// all; the invariant rejects it on the count, whatever field changed.
|
||||
testcase("TransactionProposal modified outside the whitelist");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const&, Account const&, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
|
||||
proposedTx.setFieldU32(
|
||||
sfTicketSequence, proposedTx.getFieldU32(sfTicketSequence) + 1);
|
||||
sle->setFieldObject(sfProposedTransaction, proposedTx);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// Only stages the payload and the key it would live under; nothing
|
||||
// reaches the ledger, so a precheck that inserts the entry by hand
|
||||
// presents the invariant with a creation.
|
||||
std::optional<STObject> stagedPayload;
|
||||
std::optional<Keylet> stagedProposalKeylet;
|
||||
Preclose const precloseStagedPayload =
|
||||
[&](Account const& owner, Account const& destination, Env& env) {
|
||||
auto const ticketSeq = proposal::createTicket(env, owner);
|
||||
auto const proposedJson =
|
||||
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
|
||||
stagedPayload.emplace(parse(proposedJson));
|
||||
stagedProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Inserts the staged proposal as doApply would, with the owner's
|
||||
// reserve charged.
|
||||
auto insertStagedProposal = [&](Account const& owner, ApplyContext& ac) {
|
||||
if (!stagedPayload || !stagedProposalKeylet)
|
||||
return false;
|
||||
|
||||
auto const sle = std::make_shared<SLE>(*stagedProposalKeylet);
|
||||
sle->setAccountID(sfOwner, owner.id());
|
||||
sle->setFieldObject(sfProposedTransaction, *stagedPayload);
|
||||
sle->setFieldU32(sfExpiration, 1);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
ac.view().insert(sle);
|
||||
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
if (!account)
|
||||
return false;
|
||||
account->at(sfOwnerCount) += xrpl::proposal::proposalOwnerCount(*stagedPayload);
|
||||
ac.view().update(account);
|
||||
return true;
|
||||
};
|
||||
|
||||
testcase("TransactionProposal effect whitelist");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return insertStagedProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseStagedPayload);
|
||||
|
||||
// The harness runs no transactor, so a failed result has to be fed in
|
||||
// directly. A failed create must leave no trace.
|
||||
testcase("TransactionProposal failed create still inserts");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return insertStagedProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseStagedPayload,
|
||||
TxAccount::None,
|
||||
std::source_location::current(),
|
||||
tecNO_PERMISSION);
|
||||
|
||||
// Reassigns the live proposal's reserve to a sponsor as a
|
||||
// SponsorshipTransfer would. The sponsored and sponsoring counts move
|
||||
// with it so that SponsorshipOwnerCountsMatch stays quiet.
|
||||
auto const reassignLiveProposal =
|
||||
[&](Account const& owner, Account const& sponsor, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto const reserve =
|
||||
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
|
||||
sle->setAccountID(sfSponsor, sponsor.id());
|
||||
ac.view().update(sle);
|
||||
|
||||
auto const account = ac.view().peek(keylet::account(owner.id()));
|
||||
auto const sponsorRoot = ac.view().peek(keylet::account(sponsor.id()));
|
||||
if (!account || !sponsorRoot)
|
||||
return false;
|
||||
account->at(sfSponsoredOwnerCount) += reserve;
|
||||
ac.view().update(account);
|
||||
sponsorRoot->at(sfSponsoringOwnerCount) += reserve;
|
||||
ac.view().update(sponsorRoot);
|
||||
return true;
|
||||
};
|
||||
|
||||
// A failed transfer must not have reassigned anything. The counters are
|
||||
// kept consistent so only the result mismatch is left to catch.
|
||||
testcase("TransactionProposal failed sponsorship transfer still reassigns");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
reassignLiveProposal,
|
||||
XRPAmount{},
|
||||
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal,
|
||||
TxAccount::None,
|
||||
std::source_location::current(),
|
||||
tecNO_PERMISSION);
|
||||
|
||||
// A proposal leaves the ledger only through TransactionProposalCancel
|
||||
// or together with the Ticket it is keyed to. The harness runs no
|
||||
// transactor, so each deletion below is staged by hand, releasing the
|
||||
// reserve as a real one would, to leave the deletion itself as the
|
||||
// only possible violation.
|
||||
testcase("TransactionProposal deletion outside the whitelist");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return deleteLiveProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// The owner holds a second Ticket, one no proposal is keyed to.
|
||||
std::optional<std::uint32_t> spareTicketSeq;
|
||||
Preclose const precloseLiveProposalAndSpareTicket =
|
||||
[&](Account const& owner, Account const& destination, Env& env) {
|
||||
spareTicketSeq = proposal::createTicket(env, owner);
|
||||
return precloseLiveProposal(owner, destination, env);
|
||||
};
|
||||
|
||||
// Consuming some other Ticket of the owner's is no reason for the
|
||||
// proposal to go.
|
||||
testcase("TransactionProposal deleted with an unrelated ticket");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposalAndSpareTicket);
|
||||
|
||||
// And a deleted Ticket takes the proposal keyed to it along: here the
|
||||
// Ticket went and the proposal stayed behind.
|
||||
testcase("TransactionProposal outlives its ticket");
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal outlived the Ticket it is keyed to"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// A Cancel names the live proposal and may pay with the owner's spare
|
||||
// Ticket. Both exist only once the preclose has run, so the Env is
|
||||
// built here rather than by the harness.
|
||||
struct CancelOptions
|
||||
{
|
||||
// The Cancel pays with A1's spare Ticket.
|
||||
bool paySpareTicket = false;
|
||||
TER initialResult = tesSUCCESS;
|
||||
};
|
||||
auto const checkCancel = [&](std::vector<std::string> const& expectLogs,
|
||||
Precheck const& precheck,
|
||||
std::initializer_list<TER> ters,
|
||||
Preclose const& preclose,
|
||||
CancelOptions const& options = CancelOptions(),
|
||||
std::source_location const& loc =
|
||||
std::source_location::current()) {
|
||||
Account const a1{"A1"};
|
||||
Account const a2{"A2"};
|
||||
Env env = makeEnv(testableAmendments());
|
||||
env.fund(XRP(1000), a1, a2);
|
||||
if (!BEAST_EXPECT(preclose(a1, a2, env)) || !BEAST_EXPECT(liveProposalKeylet))
|
||||
return;
|
||||
env.close();
|
||||
|
||||
STTx const tx{ttTRANSACTION_PROPOSAL_CANCEL, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, a1.id());
|
||||
obj.setFieldH256(sfProposalID, liveProposalKeylet->key);
|
||||
if (options.paySpareTicket && spareTicketSeq)
|
||||
{
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfTicketSequence, *spareTicketSeq);
|
||||
}
|
||||
}};
|
||||
doInvariantCheck(
|
||||
std::move(env),
|
||||
a1,
|
||||
a2,
|
||||
expectLogs,
|
||||
precheck,
|
||||
XRPAmount{},
|
||||
tx,
|
||||
ters,
|
||||
loc,
|
||||
options.initialResult);
|
||||
};
|
||||
|
||||
auto const cancelDeletesLiveProposal =
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return deleteLiveProposal(owner, ac);
|
||||
};
|
||||
|
||||
// A Cancel that did not succeed deleted nothing.
|
||||
testcase("TransactionProposal failed cancel still deletes");
|
||||
{
|
||||
CancelOptions options;
|
||||
options.initialResult = tecNO_PERMISSION;
|
||||
checkCancel(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
cancelDeletesLiveProposal,
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal,
|
||||
options);
|
||||
}
|
||||
|
||||
// A Cancel cannot spend a reserved Ticket, so the proposal it deletes
|
||||
// never goes with its Ticket.
|
||||
testcase("TransactionProposal cancel consumes the reserved ticket");
|
||||
checkCancel(
|
||||
{{"TransactionProposal changes do not match transaction result"}},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
|
||||
// Distinct AccountIDs in ascending order, so an array built from them
|
||||
// violates only the bound on its length.
|
||||
auto ascendingAccounts = [](std::size_t count) {
|
||||
std::vector<AccountID> accounts;
|
||||
accounts.reserve(count);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
AccountID account{beast::kZero};
|
||||
account.data()[0] = static_cast<std::uint8_t>(i + 1);
|
||||
accounts.push_back(account);
|
||||
}
|
||||
return accounts;
|
||||
};
|
||||
|
||||
auto signerArray = [](std::vector<AccountID> const& accounts) {
|
||||
STArray signers{sfSigners};
|
||||
for (auto const& account : accounts)
|
||||
{
|
||||
STObject signer{sfSigner};
|
||||
signer.setAccountID(sfAccount, account);
|
||||
signers.pushBack(signer);
|
||||
}
|
||||
return signers;
|
||||
};
|
||||
|
||||
// No transaction type on this amendment branch collects signatures yet,
|
||||
// so every rejection below has to be provoked by editing the stored
|
||||
// payload directly.
|
||||
auto const testBrokenSignerArray = [&](std::string const& name, auto const& breakPayload) {
|
||||
testcase("TransactionProposal canonical signer arrays: " + name);
|
||||
doInvariantCheck(
|
||||
{{"TransactionProposal signer arrays are not canonical"}},
|
||||
[&](Account const&, Account const& signerAccount, ApplyContext& ac) {
|
||||
auto const sle = peekLiveProposal(ac);
|
||||
if (!sle)
|
||||
return false;
|
||||
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
|
||||
breakPayload(proposedTx, signerAccount);
|
||||
sle->setFieldObject(sfProposedTransaction, proposedTx);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseLiveProposal);
|
||||
};
|
||||
|
||||
testBrokenSignerArray("duplicate entries", [&](STObject& tx, Account const& signer) {
|
||||
tx.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
|
||||
});
|
||||
|
||||
testBrokenSignerArray("descending order", [&](STObject& tx, Account const&) {
|
||||
auto accounts = ascendingAccounts(2);
|
||||
std::ranges::reverse(accounts);
|
||||
tx.setFieldArray(sfSigners, signerArray(accounts));
|
||||
});
|
||||
|
||||
testBrokenSignerArray("past the multi-sign limit", [&](STObject& tx, Account const&) {
|
||||
tx.setFieldArray(sfSigners, signerArray(ascendingAccounts(STTx::kMaxMultiSigners + 1)));
|
||||
});
|
||||
|
||||
testBrokenSignerArray("entry without an Account", [](STObject& tx, Account const&) {
|
||||
STArray signers{sfSigners};
|
||||
signers.pushBack(STObject{sfSigner});
|
||||
tx.setFieldArray(sfSigners, signers);
|
||||
});
|
||||
|
||||
for (auto const* field :
|
||||
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
|
||||
{
|
||||
testBrokenSignerArray(
|
||||
"duplicates under " + field->getName(), [&](STObject& tx, Account const& signer) {
|
||||
STObject signature{*field};
|
||||
signature.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
|
||||
tx.setFieldObject(*field, signature);
|
||||
});
|
||||
}
|
||||
|
||||
testBrokenSignerArray("past the batch-signer limit", [&](STObject& tx, Account const&) {
|
||||
STArray batchSigners{sfBatchSigners};
|
||||
for (auto const& account : ascendingAccounts(kMaxBatchSigners + 1))
|
||||
{
|
||||
STObject batchSigner{sfBatchSigner};
|
||||
batchSigner.setAccountID(sfAccount, account);
|
||||
batchSigners.pushBack(batchSigner);
|
||||
}
|
||||
tx.setFieldArray(sfBatchSigners, batchSigners);
|
||||
});
|
||||
|
||||
testBrokenSignerArray(
|
||||
"duplicates under a BatchSigner", [&](STObject& tx, Account const& signer) {
|
||||
STObject batchSigner{sfBatchSigner};
|
||||
batchSigner.setAccountID(sfAccount, signer.id());
|
||||
batchSigner.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
|
||||
|
||||
STArray batchSigners{sfBatchSigners};
|
||||
batchSigners.pushBack(batchSigner);
|
||||
tx.setFieldArray(sfBatchSigners, batchSigners);
|
||||
});
|
||||
|
||||
// Negative controls. These share the setup of the failure cases above,
|
||||
// so they confirm each of those failures came from the defect it names
|
||||
// rather than from the hand-built ledger state.
|
||||
std::initializer_list<TER> const passTers = {tesSUCCESS, tesSUCCESS};
|
||||
|
||||
testcase("TransactionProposal well-formed create");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return insertStagedProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseStagedPayload);
|
||||
|
||||
testcase("TransactionProposal left untouched");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const&, Account const&, ApplyContext& ac) {
|
||||
return liveProposalKeylet && ac.view().read(*liveProposalKeylet) != nullptr;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseLiveProposal);
|
||||
|
||||
// A Cancel deletes the proposal it names.
|
||||
testcase("TransactionProposal cancelled");
|
||||
checkCancel({}, cancelDeletesLiveProposal, passTers, precloseLiveProposal);
|
||||
|
||||
// The Ticket a Cancel pays with is not the one its proposal is keyed
|
||||
// to, so the proposal did not go with its Ticket.
|
||||
testcase("TransactionProposal cancel paid with the canceller's own ticket");
|
||||
{
|
||||
CancelOptions options;
|
||||
options.paySpareTicket = true;
|
||||
checkCancel(
|
||||
{},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
passTers,
|
||||
precloseLiveProposalAndSpareTicket,
|
||||
options);
|
||||
}
|
||||
|
||||
// A SponsorshipTransfer reassigns the Sponsor.
|
||||
testcase("TransactionProposal sponsor reassigned");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
reassignLiveProposal,
|
||||
XRPAmount{},
|
||||
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseLiveProposal);
|
||||
|
||||
// The proposed transaction executing spends the proposal's Ticket and
|
||||
// takes the proposal with it.
|
||||
testcase("TransactionProposal deleted with its ticket");
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&](Account const& owner, Account const&, ApplyContext& ac) {
|
||||
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
|
||||
deleteLiveProposal(owner, ac);
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttACCOUNT_SET, [](STObject&) {}},
|
||||
passTers,
|
||||
precloseLiveProposal);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testTransactionProposalInvariants();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(InvariantsTransactionProposal, app, xrpl);
|
||||
|
||||
} // namespace xrpl::test
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
@@ -1609,26 +1610,263 @@ private:
|
||||
env.close();
|
||||
|
||||
// Direct vault share payment inherits the underlying lock via
|
||||
// sfReferenceHolding.
|
||||
// sfReferenceHolding. Caught in the strand check, like a global
|
||||
// or individual share lock.
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
|
||||
env(pay(alice, bob, shares(1)), Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
|
||||
// The same inherited lock must also block DEX payment paths that
|
||||
// would consume an offer selling vault shares.
|
||||
// would consume an offer selling vault shares. bob, the
|
||||
// destination, inherits the lock and is caught in the strand
|
||||
// check.
|
||||
env(pay(carol, bob, shares(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 1));
|
||||
}
|
||||
|
||||
// A holder individually frozen on the underlying can neither sell
|
||||
// nor buy vault shares through an unfrozen holder's offer. The
|
||||
// inherited freeze is caught when the strand is built, before the
|
||||
// offer is consumed.
|
||||
auto const testShareDEXEndpointFrozen =
|
||||
[&](Env& env,
|
||||
PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& alice,
|
||||
Account const& bob,
|
||||
Account const& carol,
|
||||
std::function<void(bool)> const& setAliceFrozen) {
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
|
||||
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
|
||||
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
|
||||
auto const shareBalance = [&](Account const& account) {
|
||||
auto const sle = env.le(keylet::mptoken(shareMptID, account));
|
||||
return sle ? sle->at(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// bob bids XRP for shares and asks more XRP for shares, so that
|
||||
// his offers don't cross
|
||||
env(offer(bob, shares(1), XRP(1)));
|
||||
env(offer(bob, XRP(2), shares(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
auto const expectUnchanged = [&] {
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
BEAST_EXPECT(expectOffers(env, bob, 2));
|
||||
};
|
||||
|
||||
// alice sells shares: the first step's source is frozen
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Sendmax(shares(1)),
|
||||
Path(~XRP),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
expectUnchanged();
|
||||
|
||||
// carol buys shares for alice: the last step's destination is
|
||||
// frozen
|
||||
env(pay(carol, alice, shares(1)),
|
||||
Sendmax(XRP(2)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
expectUnchanged();
|
||||
|
||||
// Both succeed once alice is unfrozen
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Sendmax(shares(1)),
|
||||
Path(~XRP),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
|
||||
|
||||
env(pay(carol, alice, shares(1)),
|
||||
Sendmax(XRP(2)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
};
|
||||
|
||||
// Vault shares as the intermediate asset between two books. Only the
|
||||
// offer owners hold the shares in transit. An owner individually
|
||||
// frozen on the underlying can neither give nor receive the shares.
|
||||
auto const testShareDEXMiddleFrozen = [&](Env& env,
|
||||
PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& alice,
|
||||
Account const& bob,
|
||||
Account const& carol,
|
||||
std::function<void(bool)> const& setAliceFrozen) {
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
|
||||
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
|
||||
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
|
||||
auto const shareBalance = [&](Account const& account) {
|
||||
auto const sle = env.le(keylet::mptoken(shareMptID, account));
|
||||
return sle ? sle->at(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// alice gives the shares: XRP -> shares (alice) -> asset (bob)
|
||||
{
|
||||
env(offer(alice, XRP(1), shares(1)));
|
||||
env(offer(bob, shares(1), asset(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
env(pay(carol, owner, asset(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(carol, owner, asset(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares - 1);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 0));
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
}
|
||||
|
||||
// alice receives the shares: asset -> shares (bob) -> XRP (alice)
|
||||
{
|
||||
env(offer(bob, asset(1), shares(1)));
|
||||
env(offer(alice, shares(1), XRP(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
env(pay(owner, carol, XRP(1)),
|
||||
Sendmax(asset(1)),
|
||||
Path(BookSpec{shares.raw()}, ~XRP),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(owner, carol, XRP(1)),
|
||||
Sendmax(asset(1)),
|
||||
Path(BookSpec{shares.raw()}, ~XRP),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares + 1);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares - 1);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 0));
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
testcase("MPT individually locked: vault share DEX inherits underlying lock");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
MPTTester asset{{
|
||||
.env = env,
|
||||
.issuer = issuer,
|
||||
.holders = {owner, alice, bob},
|
||||
.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock,
|
||||
}};
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env(pay(issuer, alice, asset(2'000)));
|
||||
env(pay(issuer, bob, asset(2'000)));
|
||||
env.close();
|
||||
|
||||
auto const setAliceFrozen = [&](bool frozen) {
|
||||
asset.set({.holder = alice, .flags = frozen ? tfMPTLock : tfMPTUnlock});
|
||||
env.close();
|
||||
};
|
||||
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("IOU individually frozen: vault share DEX inherits underlying freeze");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Deliver the IOU from a book to a holder
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
PrettyAsset const asset = issuer["IOU"];
|
||||
env.trust(asset(10'000), owner, alice, bob);
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env(pay(issuer, alice, asset(2'000)));
|
||||
env(pay(issuer, bob, asset(2'000)));
|
||||
env.close();
|
||||
|
||||
auto const setAliceFrozen = [&](bool frozen) {
|
||||
env(trust(issuer, asset(0), alice, frozen ? tfSetFreeze : tfClearFreeze));
|
||||
env.close();
|
||||
};
|
||||
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <test/jtx/paychan.h>
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -12,11 +15,13 @@
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -39,6 +44,7 @@ create(
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelCreate;
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[jss::Destination] = to_string(to);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
|
||||
@@ -60,6 +66,7 @@ fund(
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelFund;
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv[sfChannel.fieldName] = to_string(channel);
|
||||
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
|
||||
@@ -79,6 +86,7 @@ claim(
|
||||
{
|
||||
json::Value jv;
|
||||
jv[jss::TransactionType] = jss::PaymentChannelClaim;
|
||||
jv[jss::Flags] = tfFullyCanonicalSig;
|
||||
jv[jss::Account] = to_string(account);
|
||||
jv["Channel"] = to_string(channel);
|
||||
if (amount)
|
||||
@@ -133,8 +141,20 @@ signClaimAuth(
|
||||
STAmount const& authAmt)
|
||||
{
|
||||
Serializer msg;
|
||||
serializePayChanAuthorization(msg, channel, authAmt.xrp());
|
||||
serializePayChanAuthorization(msg, channel, authAmt);
|
||||
return sign(pk, sk, msg.slice());
|
||||
}
|
||||
|
||||
Rate
|
||||
rate(Env const& env, Account const& account, Account const& dest, std::uint32_t const& seq)
|
||||
{
|
||||
auto const sle =
|
||||
env.le(keylet::payChannel(account.id(), dest.id(), SeqProxy::rawSequence(seq)));
|
||||
if (!sle)
|
||||
return Rate{0};
|
||||
if (sle->isFieldPresent(sfTransferRate))
|
||||
return xrpl::Rate((*sle)[sfTransferRate]);
|
||||
return kParityRate;
|
||||
}
|
||||
|
||||
} // namespace xrpl::test::jtx::paychan
|
||||
@@ -238,6 +238,13 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
// Every entry the transaction could touch, read whole, so what follows can
|
||||
// say that nothing moved rather than that the fields we named did not.
|
||||
auto const& view = *env.current();
|
||||
|
||||
// A proposer paying with one of its own Tickets consumes it whenever the
|
||||
// transaction is applied: on success, and on a claimed-fee tec.
|
||||
std::optional<std::uint32_t> ownTicketSeq;
|
||||
if (jt.jv.isMember(sfTicketSequence.jsonName))
|
||||
ownTicketSeq = jt.jv[sfTicketSequence.jsonName].asUInt();
|
||||
|
||||
auto const proposalKeylet = keylet::txProposal(target, ticketSeq);
|
||||
auto const ownerCountBefore = env.ownerCount(proposer);
|
||||
auto const sponsoredOwnerCountBefore = env.sponsoredOwnerCount(proposer);
|
||||
@@ -254,11 +261,15 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
auto const& view = *applied.current();
|
||||
|
||||
auto const created = isTesSuccess(applied.ter());
|
||||
bool const ticketConsumed = ownTicketSeq && (created || isTecClaim(applied.ter()));
|
||||
std::uint32_t const ticketReturned = ticketConsumed ? 1 : 0;
|
||||
|
||||
// The proposer owns the proposal even when another account covers its
|
||||
// reserve. A proposed Batch costs more owner-count increments.
|
||||
// reserve. A proposed Batch costs more owner-count increments. A
|
||||
// Ticket the proposer paid with gives one back.
|
||||
test.expect(
|
||||
applied.ownerCount(proposer) == ownerCountBefore + (created ? cost : 0),
|
||||
applied.ownerCount(proposer) ==
|
||||
ownerCountBefore + (created ? cost : 0) - ticketReturned,
|
||||
"proposal reserve");
|
||||
test.expect(
|
||||
applied.sponsoredOwnerCount(proposer) ==
|
||||
@@ -291,6 +302,14 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
|
||||
auto const sleProposal = view.read(proposalKeylet);
|
||||
|
||||
// The Ticket the proposer paid with has left its directory.
|
||||
auto expectedDir = proposerDirBefore;
|
||||
if (ticketConsumed)
|
||||
{
|
||||
expectedDir.erase(
|
||||
keylet::ticket(proposer.id(), SeqProxy::rawTicket(*ownTicketSeq)).key);
|
||||
}
|
||||
|
||||
if (!created)
|
||||
{
|
||||
// A create that did not succeed leaves the proposal as it found
|
||||
@@ -300,7 +319,7 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
// Nor did the directory gain a listing for an entry that does not
|
||||
// exist.
|
||||
test.expect(
|
||||
ownerDirKeys(view, proposer.id()) == proposerDirBefore, "proposal owner directory");
|
||||
ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -353,7 +372,6 @@ verify::Create::operator()(Env& env, JTx& jt) const
|
||||
|
||||
// The proposal is listed in the proposer's directory on the page its
|
||||
// OwnerNode names, and nothing else listed moved.
|
||||
auto expectedDir = proposerDirBefore;
|
||||
expectedDir.emplace(sleProposal->key(), sleProposal->getFieldU64(sfOwnerNode));
|
||||
test.expect(ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
@@ -85,4 +87,7 @@ signClaimAuth(
|
||||
uint256 const& channel,
|
||||
STAmount const& authAmt);
|
||||
|
||||
Rate
|
||||
rate(Env const& env, Account const& account, Account const& dest, std::uint32_t const& seq);
|
||||
|
||||
} // namespace xrpl::test::jtx::paychan
|
||||
@@ -77,7 +77,9 @@ public:
|
||||
* @brief Check the ledger effects a TransactionProposalCreate must have,
|
||||
* whatever its outcome: on tesSUCCESS a new proposal holding what was
|
||||
* submitted, listed in the proposer's directory and paid for by its reserve;
|
||||
* otherwise nothing moved. Nothing of the target's moves either way.
|
||||
* otherwise nothing moved. Nothing of the target's moves either way. A Ticket
|
||||
* the proposer pays with is expected to be consumed; attach ticket::Use()
|
||||
* before this condition so it sees the TicketSequence.
|
||||
*
|
||||
* @code
|
||||
* env(proposal::create(alice, payload, expiration), proposal::verify::create());
|
||||
|
||||
@@ -1001,6 +1001,222 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIsExactSum()
|
||||
{
|
||||
testcase("is exact sum");
|
||||
|
||||
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
|
||||
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
|
||||
|
||||
// Exact IOU sum
|
||||
{
|
||||
STAmount const amt1(usd, 500);
|
||||
STAmount const amt2(usd, 1500);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// Exact IOU sum with a zero operand
|
||||
{
|
||||
STAmount const amt1(usd, 0);
|
||||
STAmount const amt2(usd, 15, -19);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// Exact IOU sum whose exponent exceeds both operands'
|
||||
{
|
||||
STAmount const amt1(usd, std::uint64_t{9999999999999999});
|
||||
STAmount const amt2(usd, 1);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// IOU sum that drops the finer operand: 1.5e-18 + 1 rounds to 1, and
|
||||
// 1 - 1.5e-18 rounds back to 1, so only the difference with the
|
||||
// coarser operand exposes the loss
|
||||
{
|
||||
STAmount const dust(usd, 15, -19);
|
||||
STAmount const one(usd, 1);
|
||||
BEAST_EXPECT(isExactSum(dust, one) == false);
|
||||
BEAST_EXPECT(isExactSum(one, dust) == false);
|
||||
}
|
||||
|
||||
// IOU sum that rounds the smaller operand away entirely
|
||||
{
|
||||
STAmount const amt1(usd, 1, 15);
|
||||
STAmount const amt2(usd, 1, -3);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// IOU sum that rounds to a different nonzero increase
|
||||
{
|
||||
STAmount const amt1(usd, std::uint64_t{1234567890123456});
|
||||
STAmount const amt2(usd, 600025, -2);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// IOU sum past the largest IOU value: the sum throws, so it is not
|
||||
// exact
|
||||
{
|
||||
STAmount const max(usd, STAmount::kMaxValue, STAmount::kMaxOffset);
|
||||
BEAST_EXPECT(max.mantissa() == STAmount::kMaxValue);
|
||||
BEAST_EXPECT(max.exponent() == STAmount::kMaxOffset);
|
||||
try
|
||||
{
|
||||
auto const _ = max + max;
|
||||
BEAST_EXPECT(false);
|
||||
}
|
||||
catch (std::overflow_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == std::string("value overflow"));
|
||||
}
|
||||
BEAST_EXPECT(isExactSum(max, max) == false);
|
||||
}
|
||||
|
||||
// Exact XRP sum
|
||||
{
|
||||
STAmount const amt1(XRPAmount(500));
|
||||
STAmount const amt2(XRPAmount(1500));
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// XRP overflow
|
||||
{
|
||||
STAmount const amt1(std::numeric_limits<XRPAmount::value_type>::max());
|
||||
STAmount const amt2(XRPAmount(1));
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// Exact MPT sum
|
||||
{
|
||||
STAmount const amt1(mpt, 500);
|
||||
STAmount const amt2(mpt, 1500);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// MPT overflow
|
||||
{
|
||||
STAmount const amt1(mpt, std::numeric_limits<MPTAmount::value_type>::max());
|
||||
STAmount const amt2(mpt, 1);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// Not comparable
|
||||
{
|
||||
STAmount const amt1(XRPAmount(1));
|
||||
STAmount const amt2(usd, 1);
|
||||
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testIsExactDifference()
|
||||
{
|
||||
testcase("is exact difference");
|
||||
|
||||
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
|
||||
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
|
||||
|
||||
// Exact IOU difference
|
||||
{
|
||||
STAmount const amt1(usd, 1500);
|
||||
STAmount const amt2(usd, 500);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// Exact IOU difference with a zero subtrahend
|
||||
{
|
||||
STAmount const amt1(usd, 15, -19);
|
||||
STAmount const amt2(usd, 0);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// Exact IOU difference of equal operands
|
||||
{
|
||||
STAmount const amt1(usd, 4000);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt1) == true);
|
||||
}
|
||||
|
||||
// IOU difference that rounds to a different nonzero decrease
|
||||
{
|
||||
STAmount const amt1(usd, std::uint64_t{1234567890123456});
|
||||
STAmount const amt2(usd, 600025, -2);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// IOU difference that drops a dust subtrahend: 1 - 1.5e-18 rounds to
|
||||
// 1 and 1 + 1.5e-18 rounds back to 1, so only the minuend minus the
|
||||
// difference exposes the loss
|
||||
{
|
||||
STAmount const amt1(usd, 1);
|
||||
STAmount const amt2(usd, 15, -19);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// IOU difference that drops a dust minuend: 1.5e-18 - 1 rounds to -1
|
||||
// and 1.5e-18 - (-1) rounds back to 1, so only the difference plus
|
||||
// the subtrahend exposes the loss
|
||||
{
|
||||
STAmount const amt1(usd, 15, -19);
|
||||
STAmount const amt2(usd, 1);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// IOU difference whose check overflows: max - 1.5e80 rounds half to
|
||||
// even to 9999999999999998e80, and adding 1.5e80 back rounds past the
|
||||
// largest IOU value, so the difference is not exact
|
||||
{
|
||||
STAmount const max(usd, STAmount::kMaxValue, STAmount::kMaxOffset);
|
||||
STAmount const claw(usd, 15, 79);
|
||||
STAmount const diff = max - claw;
|
||||
BEAST_EXPECT(diff == STAmount(usd, std::uint64_t{9999999999999998}, 80));
|
||||
try
|
||||
{
|
||||
auto const _ = diff + claw;
|
||||
BEAST_EXPECT(false);
|
||||
}
|
||||
catch (std::overflow_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(e.what() == std::string("value overflow"));
|
||||
}
|
||||
BEAST_EXPECT(isExactDifference(max, claw) == false);
|
||||
}
|
||||
|
||||
// Exact XRP difference
|
||||
{
|
||||
STAmount const amt1(XRPAmount(1500));
|
||||
STAmount const amt2(XRPAmount(500));
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// XRP underflow
|
||||
{
|
||||
STAmount const amt1(XRPAmount(1));
|
||||
STAmount const amt2(XRPAmount(2));
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// Exact MPT difference
|
||||
{
|
||||
STAmount const amt1(mpt, 1500);
|
||||
STAmount const amt2(mpt, 500);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
|
||||
}
|
||||
|
||||
// MPT underflow
|
||||
{
|
||||
STAmount const amt1(mpt, 1);
|
||||
STAmount const amt2(mpt, 2);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
|
||||
}
|
||||
|
||||
// Not comparable
|
||||
{
|
||||
STAmount const amt1(XRPAmount(1));
|
||||
STAmount const amt2(usd, 1);
|
||||
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTRateRounding()
|
||||
{
|
||||
@@ -1577,6 +1793,8 @@ public:
|
||||
testCanAddXRP();
|
||||
testCanAddIOU();
|
||||
testCanAddMPT();
|
||||
testIsExactSum();
|
||||
testIsExactDifference();
|
||||
testMPTRateRounding();
|
||||
testMPTOperandIOUResult();
|
||||
testMPTGetRate();
|
||||
|
||||
@@ -2157,6 +2157,53 @@ static RPCCallTestData const kRpcCallTestArray[] = {
|
||||
]
|
||||
})",
|
||||
},
|
||||
{
|
||||
"channel_authorize: token amount.",
|
||||
__LINE__,
|
||||
{
|
||||
"channel_authorize",
|
||||
"secret_can_be_anything",
|
||||
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
R"({"currency":"USD","issuer":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","value":"10.5"})",
|
||||
},
|
||||
RPCCallTestData::Exception::NoException,
|
||||
R"({
|
||||
"method" : "channel_authorize",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"amount" : {
|
||||
"currency" : "USD",
|
||||
"issuer" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
|
||||
"value" : "10.5"
|
||||
},
|
||||
"channel_id" : "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
"secret" : "secret_can_be_anything"
|
||||
}
|
||||
]
|
||||
})",
|
||||
},
|
||||
{
|
||||
"channel_authorize: amount JSON not an object.",
|
||||
__LINE__,
|
||||
{
|
||||
"channel_authorize",
|
||||
"secret_can_be_anything",
|
||||
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
R"(["10","USD"])",
|
||||
},
|
||||
RPCCallTestData::Exception::NoException,
|
||||
R"({
|
||||
"method" : "channel_authorize",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "channelAmtMalformed",
|
||||
"error_code" : 43,
|
||||
"error_message" : "Payment channel amount is malformed."
|
||||
}
|
||||
]
|
||||
})",
|
||||
},
|
||||
{
|
||||
"channel_authorize: too few arguments.",
|
||||
__LINE__,
|
||||
@@ -2378,6 +2425,33 @@ static RPCCallTestData const kRpcCallTestArray[] = {
|
||||
]
|
||||
})",
|
||||
},
|
||||
{
|
||||
"channel_verify: token amount.",
|
||||
__LINE__,
|
||||
{
|
||||
"channel_verify",
|
||||
"aB4BXXLuPu8DpVuyq1DBiu3SrPdtK9AYZisKhu8mvkoiUD8J9Gov",
|
||||
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
R"({"mpt_issuance_id":"00000001A407AF5856CCF3C42619DAA925813FC955C72983","value":"10"})",
|
||||
"DEADBEEF",
|
||||
},
|
||||
RPCCallTestData::Exception::NoException,
|
||||
R"({
|
||||
"method" : "channel_verify",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"amount" : {
|
||||
"mpt_issuance_id" : "00000001A407AF5856CCF3C42619DAA925813FC955C72983",
|
||||
"value" : "10"
|
||||
},
|
||||
"channel_id" : "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
|
||||
"public_key" : "aB4BXXLuPu8DpVuyq1DBiu3SrPdtK9AYZisKhu8mvkoiUD8J9Gov",
|
||||
"signature" : "DEADBEEF"
|
||||
}
|
||||
]
|
||||
})",
|
||||
},
|
||||
{
|
||||
"channel_verify: too few arguments.",
|
||||
__LINE__,
|
||||
|
||||
@@ -35,6 +35,8 @@ TEST(PayChannelTests, BuilderSettersRoundTrip)
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const destinationNodeValue = canonical_UINT64();
|
||||
auto const transferRateValue = canonical_UINT32();
|
||||
auto const issuerNodeValue = canonical_UINT64();
|
||||
|
||||
PayChannelBuilder builder{
|
||||
accountValue,
|
||||
@@ -54,6 +56,8 @@ TEST(PayChannelTests, BuilderSettersRoundTrip)
|
||||
builder.setSourceTag(sourceTagValue);
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setDestinationNode(destinationNodeValue);
|
||||
builder.setTransferRate(transferRateValue);
|
||||
builder.setIssuerNode(issuerNodeValue);
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
@@ -166,6 +170,22 @@ TEST(PayChannelTests, BuilderSettersRoundTrip)
|
||||
EXPECT_TRUE(entry.hasDestinationNode());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = transferRateValue;
|
||||
auto const actualOpt = entry.getTransferRate();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfTransferRate");
|
||||
EXPECT_TRUE(entry.hasTransferRate());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerNodeValue;
|
||||
auto const actualOpt = entry.getIssuerNode();
|
||||
ASSERT_TRUE(actualOpt.has_value());
|
||||
expectEqualField(expected, *actualOpt, "sfIssuerNode");
|
||||
EXPECT_TRUE(entry.hasIssuerNode());
|
||||
}
|
||||
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
@@ -194,6 +214,8 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip)
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const destinationNodeValue = canonical_UINT64();
|
||||
auto const transferRateValue = canonical_UINT32();
|
||||
auto const issuerNodeValue = canonical_UINT64();
|
||||
|
||||
auto sle = std::make_shared<SLE>(PayChannel::entryType, index);
|
||||
|
||||
@@ -212,6 +234,8 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip)
|
||||
sle->at(sfPreviousTxnID) = previousTxnIDValue;
|
||||
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
|
||||
sle->at(sfDestinationNode) = destinationNodeValue;
|
||||
sle->at(sfTransferRate) = transferRateValue;
|
||||
sle->at(sfIssuerNode) = issuerNodeValue;
|
||||
|
||||
PayChannelBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
@@ -390,6 +414,32 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip)
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfDestinationNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = transferRateValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getTransferRate();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getTransferRate();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfTransferRate");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfTransferRate");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = issuerNodeValue;
|
||||
|
||||
auto const fromSleOpt = entryFromSle.getIssuerNode();
|
||||
auto const fromBuilderOpt = entryFromBuilder.getIssuerNode();
|
||||
|
||||
ASSERT_TRUE(fromSleOpt.has_value());
|
||||
ASSERT_TRUE(fromBuilderOpt.has_value());
|
||||
|
||||
expectEqualField(expected, *fromSleOpt, "sfIssuerNode");
|
||||
expectEqualField(expected, *fromBuilderOpt, "sfIssuerNode");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
@@ -472,5 +522,9 @@ TEST(PayChannelTests, OptionalFieldsReturnNullopt)
|
||||
EXPECT_FALSE(entry.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(entry.hasDestinationNode());
|
||||
EXPECT_FALSE(entry.getDestinationNode().has_value());
|
||||
EXPECT_FALSE(entry.hasTransferRate());
|
||||
EXPECT_FALSE(entry.getTransferRate().has_value());
|
||||
EXPECT_FALSE(entry.hasIssuerNode());
|
||||
EXPECT_FALSE(entry.getIssuerNode().has_value());
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,13 @@ TEST(ProposalHelpers, plain_payment_is_valid)
|
||||
EXPECT_TRUE(proposal::isValidProposalTxnType(txOfType(ttPAYMENT)));
|
||||
}
|
||||
|
||||
// No TransactionType at all. STTx construction rejects this first, but the
|
||||
// helper takes any STObject, so it must return false rather than throw.
|
||||
TEST(ProposalHelpers, typeless_object_is_rejected)
|
||||
{
|
||||
EXPECT_FALSE(proposal::isValidProposalTxnType(STObject{sfGeneric}));
|
||||
}
|
||||
|
||||
// A nested TransactionProposalCreate. In practice STTx construction rejects
|
||||
// this earlier (the payload lacks TransactionProposalCreate's own template
|
||||
// fields), but the defense here re-checks that guard so the two cannot
|
||||
|
||||
@@ -129,6 +129,26 @@ struct Command
|
||||
unsigned maxParams = kUnlimitedParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the amount argument of channel_authorize or channel_verify to its
|
||||
* request field: a string of drops is kept as is, and a string holding a JSON
|
||||
* object is parsed so the server reads it as a token Amount.
|
||||
*
|
||||
* @return std::nullopt if the argument is neither.
|
||||
*/
|
||||
std::optional<json::Value>
|
||||
parseChannelAmount(json::Value const& arg)
|
||||
{
|
||||
if (!arg.isString())
|
||||
return std::nullopt;
|
||||
if (toUInt64(arg.asString()))
|
||||
return arg;
|
||||
json::Value amount;
|
||||
if (json::Reader().parse(arg.asString(), amount) && amount.isObject())
|
||||
return amount;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//
|
||||
@@ -831,7 +851,7 @@ private:
|
||||
return parseAccountRaw2(jvParams, jss::destination_account);
|
||||
}
|
||||
|
||||
// channel_authorize: <private_key> [<key_type>] <channel_id> <drops>
|
||||
// channel_authorize: <private_key> [<key_type>] <channel_id> <drops>|<amount_json>
|
||||
json::Value
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
parseChannelAuthorize(json::Value const& jvParams)
|
||||
@@ -865,9 +885,10 @@ private:
|
||||
index++;
|
||||
}
|
||||
|
||||
if (!jvParams[index].isString() || !toUInt64(jvParams[index].asString()))
|
||||
auto const amount = parseChannelAmount(jvParams[index]);
|
||||
if (!amount)
|
||||
return rpcError(RpcChannelAmtMalformed);
|
||||
jvRequest[jss::amount] = jvParams[index];
|
||||
jvRequest[jss::amount] = *amount;
|
||||
|
||||
// If additional parameters are appended, be sure to increment index
|
||||
// here
|
||||
@@ -875,7 +896,7 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// channel_verify <public_key> <channel_id> <drops> <signature>
|
||||
// channel_verify <public_key> <channel_id> <drops>|<amount_json> <signature>
|
||||
json::Value
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
|
||||
parseChannelVerify(json::Value const& jvParams)
|
||||
@@ -896,9 +917,10 @@ private:
|
||||
}
|
||||
jvRequest[jss::channel_id] = jvParams[1u].asString();
|
||||
|
||||
if (!jvParams[2u].isString() || !toUInt64(jvParams[2u].asString()))
|
||||
auto const amount = parseChannelAmount(jvParams[2u]);
|
||||
if (!amount)
|
||||
return rpcError(RpcChannelAmtMalformed);
|
||||
jvRequest[jss::amount] = jvParams[2u];
|
||||
jvRequest[jss::amount] = *amount;
|
||||
|
||||
jvRequest[jss::signature] = jvParams[3u].asString();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
@@ -20,12 +21,16 @@
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
|
||||
@@ -529,4 +534,25 @@ parseSubUnsubJson(
|
||||
return RpcSuccess;
|
||||
}
|
||||
|
||||
bool
|
||||
serializeChannelAuthorization(Serializer& msg, uint256 const& channelId, json::Value const& amount)
|
||||
{
|
||||
if (amount.isString())
|
||||
{
|
||||
std::optional<std::uint64_t> const drops = toUInt64(amount.asString());
|
||||
if (!drops)
|
||||
return false;
|
||||
serializePayChanAuthorization(msg, channelId, XRPAmount(*drops));
|
||||
return true;
|
||||
}
|
||||
|
||||
// amountFromJsonNoThrow rejects an object naming XRP; only the sign is checked here
|
||||
STAmount tokenAmount;
|
||||
if (!amount.isObject() || !amountFromJsonNoThrow(tokenAmount, amount) || tokenAmount.negative())
|
||||
return false;
|
||||
|
||||
serializePayChanAuthorization(msg, channelId, tokenAmount);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <xrpl/server/NetworkOPs.h>
|
||||
|
||||
@@ -213,6 +214,21 @@ keypairForSignature(
|
||||
json::Value& error,
|
||||
unsigned int apiVersion = kApiVersionIfUnspecified);
|
||||
|
||||
/**
|
||||
* Serialize the payment channel claim authorization message for the `amount`
|
||||
* of a channel_authorize or channel_verify request.
|
||||
*
|
||||
* @param msg Serializer the message is appended to.
|
||||
* @param channelId The channel the claim is for.
|
||||
* @param amount A string of drops for an XRP channel, or the transaction
|
||||
* Amount JSON object (currency/issuer/value or mpt_issuance_id/value) for a
|
||||
* token channel.
|
||||
* @return false, with nothing appended, if `amount` is neither a string of
|
||||
* drops nor a valid non-negative token Amount object.
|
||||
*/
|
||||
bool
|
||||
serializeChannelAuthorization(Serializer& msg, uint256 const& channelId, json::Value const& amount);
|
||||
|
||||
/**
|
||||
* Parse subscribe/unsubscribe parameters
|
||||
*/
|
||||
|
||||
@@ -6,16 +6,13 @@
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol/tokens.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -24,7 +21,7 @@ namespace xrpl {
|
||||
// {
|
||||
// public_key: <public_key>
|
||||
// channel_id: 256-bit channel id
|
||||
// drops: 64-bit uint (as string)
|
||||
// amount: drops as a string (XRP channel) or an Amount object (token channel)
|
||||
// signature: signature to verify
|
||||
// }
|
||||
json::Value
|
||||
@@ -62,14 +59,10 @@ doChannelVerify(rpc::JsonContext& context)
|
||||
if (!channelId.parseHex(params[jss::channel_id].asString()))
|
||||
return rpcError(RpcChannelMalformed);
|
||||
|
||||
std::optional<std::uint64_t> const optDrops =
|
||||
params[jss::amount].isString() ? toUInt64(params[jss::amount].asString()) : std::nullopt;
|
||||
|
||||
if (!optDrops)
|
||||
Serializer msg;
|
||||
if (!rpc::serializeChannelAuthorization(msg, channelId, params[jss::amount]))
|
||||
return rpcError(RpcChannelAmtMalformed);
|
||||
|
||||
std::uint64_t const drops = *optDrops;
|
||||
|
||||
if (!params[jss::signature].isString())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
@@ -77,9 +70,6 @@ doChannelVerify(rpc::JsonContext& context)
|
||||
if (!sig || sig->empty())
|
||||
return rpcError(RpcInvalidParams);
|
||||
|
||||
Serializer msg;
|
||||
serializePayChanAuthorization(msg, channelId, XRPAmount(drops));
|
||||
|
||||
json::Value result;
|
||||
result[jss::signature_verified] = verify(*pk, msg.slice(), makeSlice(*sig));
|
||||
return result;
|
||||
|
||||
@@ -48,8 +48,13 @@ appendChannelJson(SLE const& channel, json::Value& channels)
|
||||
jDst[jss::channel_id] = to_string(channel.key());
|
||||
jDst[jss::account] = to_string(channel[sfAccount]);
|
||||
jDst[jss::destination_account] = to_string(channel[sfDestination]);
|
||||
jDst[jss::amount] = channel[sfAmount].getText();
|
||||
jDst[jss::balance] = channel[sfBalance].getText();
|
||||
// A token amount is an Amount JSON object; an XRP amount stays a string of drops.
|
||||
auto const amountJson = [](STAmount const& amount) {
|
||||
return amount.native() ? json::Value{amount.getText()}
|
||||
: amount.getJson(JsonOptions::Values::None);
|
||||
};
|
||||
jDst[jss::amount] = amountJson(channel[sfAmount]);
|
||||
jDst[jss::balance] = amountJson(channel[sfBalance]);
|
||||
if (publicKeyType(channel[sfPublicKey]))
|
||||
{
|
||||
PublicKey const pk(channel[sfPublicKey]);
|
||||
|
||||
@@ -148,6 +148,31 @@ doGatewayBalances(rpc::JsonContext& context)
|
||||
std::map<AccountID, std::vector<STAmount>> frozenBalances;
|
||||
std::map<Currency, STAmount> locked;
|
||||
|
||||
// Accumulate one locked amount into its per-currency total, clamping to
|
||||
// the largest valid STAmount on overflow.
|
||||
auto const addLocked = [&locked](STAmount const& delta) {
|
||||
auto& bal = locked[delta.get<Issue>().currency];
|
||||
if (bal == beast::kZero)
|
||||
{
|
||||
// This is needed to set the currency code correctly
|
||||
bal = delta;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bal += delta;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
// Presumably the exception was caused by overflow.
|
||||
// On overflow return the largest valid STAmount.
|
||||
// Very large sums of STAmount are approximations
|
||||
// anyway.
|
||||
bal = STAmount(bal.get<Issue>(), STAmount::kMaxValue, STAmount::kMaxOffset);
|
||||
}
|
||||
};
|
||||
|
||||
// Traverse the cold wallet's trust lines
|
||||
{
|
||||
forEachItem(*ledger, accountID, [&](SLE::ConstRef sle) {
|
||||
@@ -158,27 +183,21 @@ doGatewayBalances(rpc::JsonContext& context)
|
||||
if (escrow.holds<MPTIssue>())
|
||||
return;
|
||||
|
||||
auto& bal = locked[escrow.get<Issue>().currency];
|
||||
if (bal == beast::kZero)
|
||||
{
|
||||
// This is needed to set the currency code correctly
|
||||
bal = escrow;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
bal += escrow;
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
// Presumably the exception was caused by overflow.
|
||||
// On overflow return the largest valid STAmount.
|
||||
// Very large sums of STAmount are approximations
|
||||
// anyway.
|
||||
bal = STAmount(bal.get<Issue>(), STAmount::kMaxValue, STAmount::kMaxOffset);
|
||||
}
|
||||
}
|
||||
addLocked(escrow);
|
||||
}
|
||||
|
||||
if (sle->getType() == ltPAYCHAN)
|
||||
{
|
||||
// Only unclaimed remainders of the account's own tokens count.
|
||||
auto const& amount = sle->getFieldAmount(sfAmount);
|
||||
if (amount.native() || amount.holds<MPTIssue>() || amount.getIssuer() != accountID)
|
||||
return;
|
||||
|
||||
auto const remainder = amount - sle->getFieldAmount(sfBalance);
|
||||
if (remainder == beast::kZero)
|
||||
return;
|
||||
|
||||
addLocked(remainder);
|
||||
}
|
||||
|
||||
auto rs = PathFindTrustLine::makeItem(accountID, sle);
|
||||
|
||||
@@ -3,21 +3,17 @@
|
||||
#include <xrpld/rpc/Role.h>
|
||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
@@ -28,7 +24,7 @@ namespace xrpl {
|
||||
// secret_key: <signing_secret_key>
|
||||
// key_type: optional; either ed25519 or secp256k1 (default to secp256k1)
|
||||
// channel_id: 256-bit channel id
|
||||
// drops: 64-bit uint (as string)
|
||||
// amount: drops as a string (XRP channel) or an Amount object (token channel)
|
||||
// }
|
||||
json::Value
|
||||
doChannelAuthorize(rpc::JsonContext& context)
|
||||
@@ -73,16 +69,9 @@ doChannelAuthorize(rpc::JsonContext& context)
|
||||
if (!channelId.parseHex(params[jss::channel_id].asString()))
|
||||
return rpcError(RpcChannelMalformed);
|
||||
|
||||
std::optional<std::uint64_t> const optDrops =
|
||||
params[jss::amount].isString() ? toUInt64(params[jss::amount].asString()) : std::nullopt;
|
||||
|
||||
if (!optDrops)
|
||||
return rpcError(RpcChannelAmtMalformed);
|
||||
|
||||
std::uint64_t const drops = *optDrops;
|
||||
|
||||
Serializer msg;
|
||||
serializePayChanAuthorization(msg, channelId, XRPAmount(drops));
|
||||
if (!rpc::serializeChannelAuthorization(msg, channelId, params[jss::amount]))
|
||||
return rpcError(RpcChannelAmtMalformed);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user