mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-30 02:20:10 +00:00
The names of the files should reflect the name of the Dir class. Co-authored-by: Zack Brunson <Zshooter@gmail.com> Co-authored-by: Ed Hennis <ed@ripple.com>
2692 lines
100 KiB
C++
2692 lines
100 KiB
C++
//------------------------------------------------------------------------------
|
||
/*
|
||
This file is part of rippled: https://github.com/ripple/rippled
|
||
Copyright (c) 2023 XRPL-Labs
|
||
|
||
Permission to use, copy, modify, and/or distribute this software for any
|
||
purpose with or without fee is hereby granted, provided that the above
|
||
copyright notice and this permission notice appear in all copies.
|
||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||
*/
|
||
//==============================================================================
|
||
|
||
#include <test/jtx.h>
|
||
#include <xrpld/core/ConfigSections.h>
|
||
#include <xrpld/ledger/Dir.h>
|
||
#include <xrpl/basics/chrono.h>
|
||
#include <xrpl/protocol/Feature.h>
|
||
#include <xrpl/protocol/Indexes.h>
|
||
#include <xrpl/protocol/TxFlags.h>
|
||
#include <xrpl/protocol/jss.h>
|
||
|
||
#include <chrono>
|
||
|
||
namespace ripple {
|
||
namespace test {
|
||
struct URIToken_test : public beast::unit_test::suite
|
||
{
|
||
static bool
|
||
inOwnerDir(
|
||
ReadView const& view,
|
||
jtx::Account const& acct,
|
||
uint256 const& tid)
|
||
{
|
||
auto const uritSle = view.read({ltURI_TOKEN, tid});
|
||
ripple::Dir const ownerDir(view, keylet::ownerDir(acct.id()));
|
||
return std::find(ownerDir.begin(), ownerDir.end(), uritSle) !=
|
||
ownerDir.end();
|
||
}
|
||
|
||
static std::size_t
|
||
ownerDirCount(ReadView const& view, jtx::Account const& acct)
|
||
{
|
||
ripple::Dir const ownerDir(view, keylet::ownerDir(acct.id()));
|
||
return std::distance(ownerDir.begin(), ownerDir.end());
|
||
};
|
||
|
||
static std::pair<uint256, std::shared_ptr<SLE const>>
|
||
uriTokenKeyAndSle(
|
||
ReadView const& view,
|
||
jtx::Account const& account,
|
||
std::string const& uri)
|
||
{
|
||
auto const k = keylet::uritoken(account, Blob(uri.begin(), uri.end()));
|
||
return {k.key, view.read(k)};
|
||
}
|
||
|
||
static STAmount
|
||
limitAmount(
|
||
jtx::Env const& env,
|
||
jtx::Account const& account,
|
||
jtx::Account const& gw,
|
||
jtx::IOU const& iou)
|
||
{
|
||
auto const aHigh = account.id() > gw.id();
|
||
auto const sle = env.le(keylet::line(account, gw, iou.currency));
|
||
if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit))
|
||
return (*sle)[aHigh ? sfLowLimit : sfHighLimit];
|
||
return STAmount(iou, 0);
|
||
}
|
||
|
||
static AccountID
|
||
tokenOwner(ReadView const& view, uint256 const& id)
|
||
{
|
||
auto const slep = view.read({ltURI_TOKEN, id});
|
||
if (!slep)
|
||
return AccountID();
|
||
return slep->getAccountID(sfOwner);
|
||
}
|
||
|
||
static uint256
|
||
tokenDigest(ReadView const& view, uint256 const& id)
|
||
{
|
||
auto const slep = view.read({ltURI_TOKEN, id});
|
||
if (!slep)
|
||
return uint256{0};
|
||
return slep->getFieldH256(sfDigest);
|
||
}
|
||
|
||
static STAmount
|
||
tokenAmount(ReadView const& view, uint256 const& id)
|
||
{
|
||
auto const slep = view.read({ltURI_TOKEN, id});
|
||
if (!slep)
|
||
return XRPAmount{-1};
|
||
if (slep->getFieldAmount(sfAmount))
|
||
return (*slep)[sfAmount];
|
||
return XRPAmount{-1};
|
||
}
|
||
|
||
static STAmount
|
||
lineBalance(
|
||
jtx::Env const& env,
|
||
jtx::Account const& account,
|
||
jtx::Account const& gw,
|
||
jtx::IOU const& iou)
|
||
{
|
||
auto const sle = env.le(keylet::line(account, gw, iou.currency));
|
||
if (sle && sle->isFieldPresent(sfBalance))
|
||
return (*sle)[sfBalance];
|
||
return STAmount(iou, 0);
|
||
}
|
||
|
||
void
|
||
testEnabled(FeatureBitset features)
|
||
{
|
||
testcase("enabled");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
// setup env
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
|
||
for (bool const withURIToken : {false, true})
|
||
{
|
||
// If the URIToken amendment is not enabled, you should not be able
|
||
// to mint, burn, buy, sell or clear uri tokens.
|
||
auto const amend =
|
||
withURIToken ? features : features - featureURIToken;
|
||
Env env{*this, amend};
|
||
|
||
env.fund(XRP(1000), alice, bob);
|
||
env.close();
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(alice, uri))};
|
||
|
||
auto const txResult =
|
||
withURIToken ? ter(tesSUCCESS) : ter(temDISABLED);
|
||
auto const ownerDir = withURIToken ? 1 : 0;
|
||
|
||
// MINT
|
||
env(uritoken::mint(alice, uri), txResult);
|
||
env.close();
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == ownerDir);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
|
||
// SELL
|
||
env(uritoken::sell(alice, id), uritoken::amt(XRP(10)), txResult);
|
||
env.close();
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == ownerDir);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
|
||
// BUY
|
||
env(uritoken::buy(bob, id), uritoken::amt(XRP(10)), txResult);
|
||
env.close();
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == ownerDir);
|
||
|
||
// SELL
|
||
env(uritoken::sell(bob, id), uritoken::amt(XRP(10)), txResult);
|
||
env.close();
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == ownerDir);
|
||
|
||
// CLEAR
|
||
env(uritoken::cancel(bob, id), txResult);
|
||
env.close();
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == ownerDir);
|
||
|
||
// BURN
|
||
env(uritoken::burn(bob, id), txResult);
|
||
env.close();
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
}
|
||
|
||
void
|
||
testMintInvalid(FeatureBitset features)
|
||
{
|
||
testcase("mint_invalid");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
// fixXahauV1
|
||
{
|
||
Env env{*this, features};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
env.fund(XRP(1000), alice, bob);
|
||
env.close();
|
||
|
||
std::string const uri(2, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
// temMALFORMED - cannot include sfDestination without sfAmount
|
||
bool const withFixXahauV1 =
|
||
env.current()->rules().enabled(fixXahauV1);
|
||
auto const txResult =
|
||
withFixXahauV1 ? ter(temMALFORMED) : ter(tefINTERNAL);
|
||
env(uritoken::mint(alice, uri), uritoken::dest(bob), txResult);
|
||
env.close();
|
||
}
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account("gw");
|
||
auto const USD = gw["USD"];
|
||
|
||
env.fund(XRP(200), gw, alice);
|
||
env.close();
|
||
env.trust(USD(100'000), alice);
|
||
env.close();
|
||
env(pay(gw, alice, USD(10'000)));
|
||
|
||
std::string const uri(2, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
//----------------------------------------------------------------------
|
||
// preflight
|
||
|
||
{
|
||
// temBAD_AMOUNT - !isLegalNet - !value.native()
|
||
// temBAD_AMOUNT - !isLegalNet - value.mantissa() <= cMaxNativeN
|
||
|
||
// temBAD_AMOUNT - negative XRP
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::amt(XRP(-100)),
|
||
ter(temBAD_AMOUNT));
|
||
env.close();
|
||
|
||
IOU const BAD{bob, badCurrency()};
|
||
// temBAD_CURRENCY - bad currency
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::amt(BAD(100)),
|
||
ter(temBAD_CURRENCY));
|
||
env.close();
|
||
|
||
// temMALFORMED - XRP cannot be 0 without sfDestination
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::amt(XRP(0)),
|
||
ter(temMALFORMED));
|
||
env.close();
|
||
|
||
// temREDUNDANT - sfDestination cannot be sfAccount
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::dest(alice),
|
||
uritoken::amt(XRP(0)),
|
||
ter(temREDUNDANT));
|
||
env.close();
|
||
|
||
// temINVALID_FLAG - invalid flags
|
||
env(uritoken::mint(alice, uri),
|
||
txflags(tfAllowXRP),
|
||
ter(temINVALID_FLAG));
|
||
env.close();
|
||
|
||
// temMALFORMED - no uri & no flags
|
||
std::string const nouri(0, '?');
|
||
env(uritoken::mint(alice, nouri), ter(temMALFORMED));
|
||
env.close();
|
||
|
||
// temMALFORMED - bad uri 257 len
|
||
std::string const longuri(maxTokenURILength + 1, '?');
|
||
env(uritoken::mint(alice, longuri), ter(temMALFORMED));
|
||
env.close();
|
||
}
|
||
|
||
//----------------------------------------------------------------------
|
||
// preclaim
|
||
|
||
{
|
||
env.fund(XRP(251), bob);
|
||
env.close();
|
||
auto const btid = uritoken::tokenid(bob, uri);
|
||
std::string const bhexid{strHex(btid)};
|
||
// tecDUPLICATE - duplicate uri token
|
||
env(uritoken::mint(bob, uri), txflags(tfBurnable));
|
||
env(uritoken::mint(bob, uri), ter(tecDUPLICATE));
|
||
env(uritoken::burn(bob, bhexid));
|
||
env.close();
|
||
}
|
||
|
||
//----------------------------------------------------------------------
|
||
// doApply
|
||
|
||
{
|
||
// tecINSUFFICIENT_RESERVE - out of xrp
|
||
env(uritoken::mint(alice, uri), ter(tecINSUFFICIENT_RESERVE));
|
||
env.close();
|
||
|
||
// tecDIR_FULL - directory full
|
||
}
|
||
}
|
||
|
||
void
|
||
testBurnInvalid(FeatureBitset features)
|
||
{
|
||
testcase("burn_invalid");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const dave = Account("dave");
|
||
env.fund(XRP(1000), alice, bob, carol);
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// preflight
|
||
// temDISABLED - ignore
|
||
|
||
// mint non burnable token
|
||
std::string const uri(2, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
|
||
// temINVALID_FLAG - invalid flags
|
||
env(uritoken::burn(alice, hexid),
|
||
txflags(tfAllowXRP),
|
||
ter(temINVALID_FLAG));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// preclaim
|
||
|
||
// tecNO_ENTRY - no exists item
|
||
std::string const neuri(3, '?');
|
||
auto const netid = uritoken::tokenid(alice, neuri);
|
||
std::string const hexneuri{strHex(netid)};
|
||
env(uritoken::burn(alice, hexneuri), ter(tecNO_ENTRY));
|
||
env.close();
|
||
|
||
// tecNO_ENTRY - no owner exists
|
||
// impossible test
|
||
|
||
// tecNO_PERMISSION - not owner and not (issuer/burnable)
|
||
env(uritoken::burn(bob, hexid), ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// doApply
|
||
|
||
// tecNO_PERMISSION - no permission
|
||
env(uritoken::burn(carol, hexid), ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
// tefBAD_LEDGER - could not remove object
|
||
}
|
||
|
||
void
|
||
testSellInvalid(FeatureBitset features)
|
||
{
|
||
testcase("sell_invalid");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account{"gateway"};
|
||
auto const ngw = Account{"ngateway"};
|
||
auto const USD = gw["USD"];
|
||
auto const NUSD = ngw["USD"];
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
// mint token
|
||
std::string const uri(2, '?');
|
||
std::string const id{strHex(uritoken::tokenid(alice, uri))};
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// preflight
|
||
|
||
// temBAD_AMOUNT - bad xrp/amount
|
||
env(uritoken::sell(alice, id),
|
||
uritoken::amt(XRP(-1)),
|
||
ter(temBAD_AMOUNT));
|
||
env.close();
|
||
|
||
// temBAD_AMOUNT - bad ft/amount
|
||
env(uritoken::sell(alice, id),
|
||
uritoken::amt(USD(-1)),
|
||
ter(temBAD_AMOUNT));
|
||
env.close();
|
||
|
||
// temBAD_CURRENCY - bad currency
|
||
IOU const BAD{gw, badCurrency()};
|
||
env(uritoken::sell(alice, id),
|
||
uritoken::amt(BAD(10)),
|
||
ter(temBAD_CURRENCY));
|
||
env.close();
|
||
|
||
// temMALFORMED - XRP cannot be 0 without sfDestination
|
||
env(uritoken::sell(alice, id),
|
||
uritoken::amt(XRP(0)),
|
||
ter(temMALFORMED));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// preclaim
|
||
// tecNO_PERMISSION - invalid account
|
||
env(uritoken::sell(bob, id),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
// tecNO_ISSUER - invalid issuer
|
||
env(uritoken::sell(alice, id),
|
||
uritoken::amt(NUSD(10)),
|
||
ter(tecNO_ISSUER));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// doApply
|
||
|
||
// tecNO_PERMISSION - invalid account
|
||
env(uritoken::sell(bob, id),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
}
|
||
|
||
void
|
||
testBuyInvalid(FeatureBitset features)
|
||
{
|
||
testcase("buy_invalid");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
// setup env
|
||
Env env{*this, envconfig(), features, nullptr};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const dave = Account("dave");
|
||
auto const echo = Account("echo");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
auto const EUR = gw["EUR"];
|
||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||
env.trust(USD(100000), alice, bob, carol);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env(pay(gw, carol, USD(1000)));
|
||
env.close();
|
||
|
||
// mint token
|
||
std::string const uri(2, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// preclaim
|
||
|
||
// tecNO_PERMISSION - not for sale
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
// set sell
|
||
env(uritoken::sell(alice, hexid),
|
||
uritoken::amt(USD(10)),
|
||
jtx::token::destination(bob));
|
||
env.close();
|
||
|
||
// tecNO_PERMISSION - for sale to dest, you are not dest
|
||
env(uritoken::buy(carol, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
// temBAD_CURRENCY - invalid buy sell amounts
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(EUR(10)),
|
||
ter(temBAD_CURRENCY));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_PAYMENT - insufficient buy offer amount
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(9)),
|
||
ter(tecINSUFFICIENT_PAYMENT));
|
||
env.close();
|
||
|
||
env(uritoken::cancel(alice, hexid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(10000)));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_FUNDS - insufficient xrp - fees
|
||
// fixXahauV1 - fix checking wrong account for insufficient xrp
|
||
env(pay(env.master, alice, XRP(10000)));
|
||
auto const txResult = env.current()->rules().enabled(fixXahauV1)
|
||
? ter(tecINSUFFICIENT_FUNDS)
|
||
: ter(tecINTERNAL);
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10000)), txResult);
|
||
env.close();
|
||
|
||
// clear sell and reset new sell
|
||
env(uritoken::cancel(alice, hexid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10000)));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_FUNDS - insufficient amount
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10000)),
|
||
ter(tecINSUFFICIENT_FUNDS));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// doApply
|
||
|
||
// clear sell
|
||
env(uritoken::cancel(alice, hexid));
|
||
env.close();
|
||
|
||
// tecNO_PERMISSION - not listed
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
// set sell
|
||
env(uritoken::sell(alice, hexid),
|
||
uritoken::amt(USD(10)),
|
||
jtx::token::destination(bob));
|
||
env.close();
|
||
|
||
// tecNO_PERMISSION - for sale to dest, you are not dest
|
||
env(uritoken::buy(carol, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
|
||
// temBAD_CURRENCY - invalid buy sell amounts
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(EUR(10)),
|
||
ter(temBAD_CURRENCY));
|
||
env.close();
|
||
|
||
// clear sell and set xrp sell
|
||
env(uritoken::cancel(alice, hexid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(1000)));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_PAYMENT - insufficient xrp sent
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(XRP(900)),
|
||
ter(tecINSUFFICIENT_PAYMENT));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_FUNDS - insufficient xrp - fees
|
||
// fixXahauV1 - fix checking wrong account for insufficient xrp
|
||
auto const txResult1 = env.current()->rules().enabled(fixXahauV1)
|
||
? ter(tecINSUFFICIENT_FUNDS)
|
||
: ter(tecINTERNAL);
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1000)), txResult1);
|
||
env.close();
|
||
|
||
// clear sell and set usd sell
|
||
env(uritoken::cancel(alice, hexid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(1000)));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_PAYMENT - insufficient amount sent
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(900)),
|
||
ter(tecINSUFFICIENT_PAYMENT));
|
||
env.close();
|
||
|
||
// tecINSUFFICIENT_FUNDS - insufficient amount sent
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10000)),
|
||
ter(tecINSUFFICIENT_FUNDS));
|
||
env.close();
|
||
|
||
// tecNO_LINE_INSUF_RESERVE - insufficient xrp to create line
|
||
{
|
||
// fund echo 251 xrp (not enough for line reserve)
|
||
env.fund(XRP(251), echo);
|
||
env.fund(XRP(301), dave);
|
||
env.close();
|
||
env.trust(USD(100000), dave);
|
||
env.close();
|
||
env(pay(gw, dave, USD(1000)));
|
||
env.close();
|
||
|
||
// mint token
|
||
std::string const uri(3, '?');
|
||
auto const tid = uritoken::tokenid(echo, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(echo, uri));
|
||
env(uritoken::sell(echo, hexid), uritoken::amt(USD(1)));
|
||
env.close();
|
||
|
||
// tecNO_LINE_INSUF_RESERVE - insufficient xrp to create line
|
||
auto const txResult = env.current()->rules().enabled(fixXahauV1)
|
||
? ter(tecINSUF_RESERVE_SELLER)
|
||
: ter(tecNO_LINE_INSUF_RESERVE);
|
||
|
||
env(noop(echo), fee(XRP(50)), ter(tesSUCCESS));
|
||
env(uritoken::buy(dave, hexid), uritoken::amt(USD(1)), txResult);
|
||
env.close();
|
||
}
|
||
|
||
// tecDIR_FULL - unknown how to test/handle
|
||
// tecINTERNAL - unknown how to test/handle
|
||
// tecINTERNAL - unknown how to test/handle
|
||
// tecINTERNAL - unknown how to test/handle
|
||
// tecINTERNAL - unknown how to test/handle
|
||
// tefBAD_LEDGER - unknown how to test/handle
|
||
// tefBAD_LEDGER - unknown how to test/handle
|
||
// tecINTERNAL - unknown how to test/handle
|
||
// tecINTERNAL - unknown how to test/handle
|
||
}
|
||
|
||
void
|
||
testClearInvalid(FeatureBitset features)
|
||
{
|
||
testcase("clear_invalid");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
auto const EUR = gw["EUR"];
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
// mint token
|
||
std::string const uri(2, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// operator preflight
|
||
// temDISABLED
|
||
|
||
// temINVALID_FLAG - invalid flag
|
||
env(uritoken::cancel(alice, hexid),
|
||
txflags(tfAllowXRP),
|
||
ter(temINVALID_FLAG));
|
||
env.close();
|
||
|
||
//----------------------------------------------------------------------
|
||
// preclaim
|
||
|
||
// tecNO_PERMISSION - not your uritoken
|
||
env(uritoken::cancel(bob, hexid), ter(tecNO_PERMISSION));
|
||
env.close();
|
||
}
|
||
|
||
void
|
||
testMintValid(FeatureBitset features)
|
||
{
|
||
testcase("mint");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob);
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
std::string const digestval =
|
||
"C16E7263F07AA41261DCC955660AF4646ADBA414E37B6F5A5BA50F75153F5CCC";
|
||
|
||
// has digest - has uri - no flags
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri),
|
||
json(sfDigest.fieldName, digestval));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(
|
||
to_string(tokenDigest(*env.current(), tid)) == digestval);
|
||
// cleanup
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
}
|
||
// has digest - has uri - burnable flag
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri),
|
||
txflags(tfBurnable),
|
||
json(sfDigest.fieldName, digestval));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(
|
||
to_string(tokenDigest(*env.current(), tid)) == digestval);
|
||
// cleanup
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
}
|
||
// has uri - no flags
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
// cleanup
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
}
|
||
// has uri - burnable flag
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri), txflags(tfBurnable));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
// cleanup
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
}
|
||
|
||
// 0 amount and destination
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::dest(bob),
|
||
uritoken::amt(XRP(0)),
|
||
txflags(tfBurnable),
|
||
ter(tesSUCCESS));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// buy
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(0)));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// cleanup
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
|
||
// has amount and destination
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::dest(bob),
|
||
uritoken::amt(XRP(10)),
|
||
txflags(tfBurnable),
|
||
ter(tesSUCCESS));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// buy
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// cleanup
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
|
||
// has amount and no destination
|
||
{
|
||
// mint
|
||
env(uritoken::mint(alice, uri),
|
||
uritoken::amt(XRP(10)),
|
||
txflags(tfBurnable),
|
||
ter(tesSUCCESS));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// buy
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// cleanup
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
}
|
||
|
||
void
|
||
testBurnValid(FeatureBitset features)
|
||
{
|
||
testcase("burn");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob);
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
// issuer can burn
|
||
{
|
||
// alice mints
|
||
env(uritoken::mint(alice, uri), txflags(tfBurnable));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(1)));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice burns
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
// issuer cannot burn
|
||
{
|
||
// alice mints
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(1)));
|
||
env.close();
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice tries to burn
|
||
env(uritoken::burn(alice, hexid), ter(tecNO_PERMISSION));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// burn for test reset
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
// owner can burn
|
||
{
|
||
// alice mints
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(1)));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
env.close();
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// bob burns
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 0);
|
||
}
|
||
}
|
||
|
||
void
|
||
testBuyValid(FeatureBitset features)
|
||
{
|
||
testcase("buy");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
Env env{*this, features};
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
// setup env
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
auto const feeDrops = env.current()->fees().base;
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
// bob can buy with XRP
|
||
{
|
||
// alice mints
|
||
const auto delta = XRP(10);
|
||
auto preAlice = env.balance(alice);
|
||
auto preBob = env.balance(bob);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
BEAST_EXPECT(env.balance(alice) == preAlice - (1 * feeDrops));
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
BEAST_EXPECT(env.balance(alice) == preAlice - (2 * feeDrops));
|
||
env.close();
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
BEAST_EXPECT(
|
||
env.balance(alice) == preAlice + delta - (2 * feeDrops));
|
||
BEAST_EXPECT(env.balance(bob) == preBob - delta - feeDrops);
|
||
BEAST_EXPECT(bob.id() == tokenOwner(*env.current(), tid));
|
||
|
||
// bob burns to reset tests
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
// bob can buy with USD
|
||
{
|
||
// alice mints
|
||
const auto delta = USD(10);
|
||
auto preAlice = env.balance(alice, USD.issue());
|
||
auto preAliceXrp = env.balance(alice);
|
||
auto preBob = env.balance(bob, USD.issue());
|
||
auto preBobXrp = env.balance(bob);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
BEAST_EXPECT(env.balance(alice) == preAliceXrp - (1 * feeDrops));
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
BEAST_EXPECT(env.balance(alice) == preAliceXrp - (2 * feeDrops));
|
||
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice);
|
||
env.close();
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + delta);
|
||
BEAST_EXPECT(env.balance(alice) == preAliceXrp - (2 * feeDrops));
|
||
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta);
|
||
BEAST_EXPECT(env.balance(bob) == preBobXrp - (1 * feeDrops));
|
||
BEAST_EXPECT(bob.id() == tokenOwner(*env.current(), tid));
|
||
|
||
// bob burns to reset tests
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
}
|
||
|
||
void
|
||
testSellValid(FeatureBitset features)
|
||
{
|
||
testcase("sell");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob, carol);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env(pay(gw, carol, USD(1000)));
|
||
env.close();
|
||
|
||
auto const feeDrops = env.current()->fees().base;
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
// alice can sell with XRP
|
||
{
|
||
// alice mints
|
||
const auto delta = XRP(10);
|
||
auto preAlice = env.balance(alice);
|
||
auto preBob = env.balance(bob);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(delta == tokenAmount(*env.current(), tid));
|
||
// alice clears and sells again at a higher price
|
||
env(uritoken::cancel(alice, hexid));
|
||
BEAST_EXPECT(XRPAmount{-1} == tokenAmount(*env.current(), tid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(11)));
|
||
env.close();
|
||
BEAST_EXPECT(XRP(11) == tokenAmount(*env.current(), tid));
|
||
// bob tries to buy at original price and fails
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(delta),
|
||
ter(tecINSUFFICIENT_PAYMENT));
|
||
// bob buys at higher price
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(11)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
BEAST_EXPECT(
|
||
env.balance(alice) == preAlice + XRP(11) - (4 * feeDrops));
|
||
BEAST_EXPECT(env.balance(bob) == preBob - XRP(11) - (2 * feeDrops));
|
||
BEAST_EXPECT(bob.id() == tokenOwner(*env.current(), tid));
|
||
|
||
// bob burns to reset tests
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
// alice can sell with XRP and dest
|
||
{
|
||
// alice mints
|
||
const auto delta = XRP(10);
|
||
auto preAlice = env.balance(alice);
|
||
auto preBob = env.balance(bob);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid),
|
||
uritoken::amt(delta),
|
||
jtx::token::destination(bob));
|
||
env.close();
|
||
BEAST_EXPECT(delta == tokenAmount(*env.current(), tid));
|
||
// carol tries to buy but cannot
|
||
env(uritoken::buy(carol, hexid),
|
||
uritoken::amt(delta),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
BEAST_EXPECT(
|
||
env.balance(alice) == preAlice + delta - (2 * feeDrops));
|
||
BEAST_EXPECT(env.balance(bob) == preBob - delta - (1 * feeDrops));
|
||
BEAST_EXPECT(bob.id() == tokenOwner(*env.current(), tid));
|
||
|
||
// bob burns to reset tests
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
|
||
// alice can sell with USD
|
||
{
|
||
// alice mints
|
||
const auto delta = USD(10);
|
||
auto preAlice = env.balance(alice, USD.issue());
|
||
auto preAliceXrp = env.balance(alice);
|
||
auto preBob = env.balance(bob, USD.issue());
|
||
auto preBobXrp = env.balance(bob);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(delta == tokenAmount(*env.current(), tid));
|
||
// alice clears and sells again at a higher price
|
||
env(uritoken::cancel(alice, hexid));
|
||
BEAST_EXPECT(XRPAmount{-1} == tokenAmount(*env.current(), tid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(11)));
|
||
env.close();
|
||
BEAST_EXPECT(USD(11) == tokenAmount(*env.current(), tid));
|
||
// bob tries to buy at original price and fails
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(delta),
|
||
ter(tecINSUFFICIENT_PAYMENT));
|
||
// bob buys at higher price
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(11)));
|
||
env.close();
|
||
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + USD(11));
|
||
BEAST_EXPECT(env.balance(alice) == preAliceXrp - (4 * feeDrops));
|
||
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - USD(11));
|
||
BEAST_EXPECT(env.balance(bob) == preBobXrp - (2 * feeDrops));
|
||
BEAST_EXPECT(bob.id() == tokenOwner(*env.current(), tid));
|
||
|
||
// bob burns to reset tests
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
// alice can sell with USD and dest
|
||
{
|
||
// alice mints
|
||
const auto delta = USD(10);
|
||
auto preAlice = env.balance(alice, USD.issue());
|
||
auto preAliceXrp = env.balance(alice);
|
||
auto preBob = env.balance(bob, USD.issue());
|
||
auto preBobXrp = env.balance(bob);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid),
|
||
uritoken::amt(delta),
|
||
jtx::token::destination(bob));
|
||
env.close();
|
||
BEAST_EXPECT(delta == tokenAmount(*env.current(), tid));
|
||
// carol tries to buy but cannot
|
||
env(uritoken::buy(carol, hexid),
|
||
uritoken::amt(delta),
|
||
ter(tecNO_PERMISSION));
|
||
env.close();
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + delta);
|
||
BEAST_EXPECT(env.balance(alice) == preAliceXrp - (2 * feeDrops));
|
||
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta);
|
||
BEAST_EXPECT(env.balance(bob) == preBobXrp - (1 * feeDrops));
|
||
BEAST_EXPECT(bob.id() == tokenOwner(*env.current(), tid));
|
||
|
||
// bob burns to reset tests
|
||
env(uritoken::burn(bob, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
}
|
||
|
||
void
|
||
testClearValid(FeatureBitset features)
|
||
{
|
||
testcase("clear");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
// setup env
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob, carol);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env(pay(gw, carol, USD(1000)));
|
||
env.close();
|
||
|
||
// auto const feeDrops = env.current()->fees().base;
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
// alice can clear / reset XRP amount
|
||
{
|
||
// alice mints
|
||
const auto delta = XRP(10);
|
||
auto preAlice = env.balance(alice);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(delta == tokenAmount(*env.current(), tid));
|
||
// alice clears the sell amount
|
||
env(uritoken::cancel(alice, hexid));
|
||
BEAST_EXPECT(XRPAmount{-1} == tokenAmount(*env.current(), tid));
|
||
// alice sets sell for higher amount
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(11)));
|
||
env.close();
|
||
BEAST_EXPECT(XRP(11) == tokenAmount(*env.current(), tid));
|
||
// alice clears the sell amount
|
||
env(uritoken::cancel(alice, hexid));
|
||
BEAST_EXPECT(XRPAmount{-1} == tokenAmount(*env.current(), tid));
|
||
|
||
// alice burns to reset tests
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
// alice can clear / reset USD amount
|
||
{
|
||
// alice mints
|
||
const auto delta = USD(10);
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(delta == tokenAmount(*env.current(), tid));
|
||
// alice clears the sell amount
|
||
env(uritoken::cancel(alice, hexid));
|
||
BEAST_EXPECT(XRPAmount{-1} == tokenAmount(*env.current(), tid));
|
||
// alice sets sell for higher amount
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(11)));
|
||
env.close();
|
||
BEAST_EXPECT(USD(11) == tokenAmount(*env.current(), tid));
|
||
// alice clears the sell amount
|
||
env(uritoken::cancel(alice, hexid));
|
||
BEAST_EXPECT(XRPAmount{-1} == tokenAmount(*env.current(), tid));
|
||
|
||
// alice burns to reset tests
|
||
env(uritoken::burn(alice, hexid));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
}
|
||
}
|
||
|
||
void
|
||
testMetaAndOwnership(FeatureBitset features)
|
||
{
|
||
testcase("metadata_and_onwnership");
|
||
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
uint256 const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
{
|
||
// Test without adding the uritoken to the recipient's owner
|
||
// directory
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sets the sell offer
|
||
// bob sets the buy offer
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
}
|
||
{
|
||
// Test with adding the uritoken to the recipient's owner
|
||
// directory
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2);
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 1);
|
||
// alice sets the sell offer
|
||
// bob sets the buy offer
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), bob) == 2);
|
||
}
|
||
}
|
||
|
||
void
|
||
testAccountDelete(FeatureBitset features)
|
||
{
|
||
testcase("account_delete");
|
||
using namespace test::jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
auto rmAccount = [this](
|
||
Env& env,
|
||
Account const& toRm,
|
||
Account const& dst,
|
||
TER expectedTer = tesSUCCESS) {
|
||
// only allow an account to be deleted if the account's sequence
|
||
// number is at least 256 less than the current ledger sequence
|
||
for (auto minRmSeq = env.seq(toRm) + 257;
|
||
env.current()->seq() < minRmSeq;
|
||
env.close())
|
||
{
|
||
}
|
||
|
||
env(acctdelete(toRm, dst),
|
||
fee(drops(env.current()->fees().increment)),
|
||
ter(expectedTer));
|
||
env.close();
|
||
this->BEAST_EXPECT(
|
||
isTesSuccess(expectedTer) ==
|
||
!env.closed()->exists(keylet::account(toRm.id())));
|
||
};
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, carol, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob, carol);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env(pay(gw, carol, USD(1000)));
|
||
env.close();
|
||
|
||
// mint a uritoken from alice
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// alice has trustline + mint + sell
|
||
rmAccount(env, alice, bob, tecHAS_OBLIGATIONS);
|
||
|
||
// bob buys uri token
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tesSUCCESS));
|
||
env.close();
|
||
BEAST_EXPECT(!inOwnerDir(*env.current(), alice, tid));
|
||
|
||
// alice still has a trustline
|
||
rmAccount(env, alice, bob, tecHAS_OBLIGATIONS);
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 1);
|
||
|
||
// drain pay all back and drain trustline
|
||
env.trust(USD(0), alice);
|
||
env(pay(alice, gw, env.balance(alice, USD.issue())));
|
||
BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 0);
|
||
|
||
// alice cant delete account - lsfURITokenIssuer
|
||
rmAccount(env, alice, bob, tecHAS_OBLIGATIONS);
|
||
|
||
// bob can mint same token with different tokenid
|
||
env(uritoken::mint(bob, uri));
|
||
env.close();
|
||
auto const btid = uritoken::tokenid(bob, uri);
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, btid));
|
||
|
||
// token ids are not the same
|
||
BEAST_EXPECT(btid != tid);
|
||
}
|
||
}
|
||
|
||
void
|
||
testTickets(FeatureBitset features)
|
||
{
|
||
testcase("tickets");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
Env env{*this, features};
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account{"gateway"};
|
||
auto USD = gw["USD"];
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
// alice and bob grab enough tickets for all of the following
|
||
// transactions. Note that once the tickets are acquired alice's
|
||
// and bob's account sequence numbers should not advance.
|
||
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
|
||
env(ticket::create(alice, 10));
|
||
std::uint32_t const aliceSeq{env.seq(alice)};
|
||
|
||
std::uint32_t bobTicketSeq{env.seq(bob) + 1};
|
||
env(ticket::create(bob, 10));
|
||
std::uint32_t const bobSeq{env.seq(bob)};
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
env(uritoken::mint(alice, uri), ticket::use(aliceTicketSeq++));
|
||
env(uritoken::sell(alice, hexid),
|
||
uritoken::amt(USD(1000)),
|
||
ticket::use(aliceTicketSeq++));
|
||
|
||
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
|
||
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
|
||
// A transaction that generates a tec still consumes its ticket.
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(1500)),
|
||
ticket::use(bobTicketSeq++),
|
||
ter(tecINSUFFICIENT_FUNDS));
|
||
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
|
||
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(1000)),
|
||
ticket::use(bobTicketSeq++));
|
||
env.require(tickets(bob, env.seq(bob) - bobTicketSeq));
|
||
BEAST_EXPECT(env.seq(bob) == bobSeq);
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), bob, tid));
|
||
}
|
||
|
||
void
|
||
testRippleState(FeatureBitset features)
|
||
{
|
||
testcase("ripple_state");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
//
|
||
// USE lineBalance(env, ...) over env.balance(...)
|
||
// I did this to check the exact sign "-/+"
|
||
//
|
||
|
||
struct TestAccountData
|
||
{
|
||
Account src;
|
||
Account dst;
|
||
Account gw;
|
||
bool hasTrustline;
|
||
bool negative;
|
||
};
|
||
|
||
std::array<TestAccountData, 8> tests = {{
|
||
// src > dst && src > issuer && dst no trustline
|
||
{Account("alice2"), Account("bob0"), Account{"gw0"}, false, true},
|
||
// src < dst && src < issuer && dst no trustline
|
||
{Account("carol0"), Account("dan1"), Account{"gw1"}, false, false},
|
||
// dst > src && dst > issuer && dst no trustline
|
||
{Account("dan1"), Account("alice2"), Account{"gw0"}, false, true},
|
||
// dst < src && dst < issuer && dst no trustline
|
||
{Account("bob0"), Account("carol0"), Account{"gw1"}, false, false},
|
||
// src > dst && src > issuer && dst has trustline
|
||
{Account("alice2"), Account("bob0"), Account{"gw0"}, true, true},
|
||
// src < dst && src < issuer && dst has trustline
|
||
{Account("carol0"), Account("dan1"), Account{"gw1"}, true, false},
|
||
// dst > src && dst > issuer && dst has trustline
|
||
{Account("dan1"), Account("alice2"), Account{"gw0"}, true, true},
|
||
// dst < src && dst < issuer && dst has trustline
|
||
{Account("bob0"), Account("carol0"), Account{"gw1"}, true, false},
|
||
}};
|
||
|
||
for (auto const& t : tests)
|
||
{
|
||
Env env{*this, features};
|
||
auto const USD = t.gw["USD"];
|
||
env.fund(XRP(5000), t.src, t.dst, t.gw);
|
||
env.close();
|
||
if (t.hasTrustline)
|
||
env.trust(USD(100000), t.src, t.dst);
|
||
else
|
||
env.trust(USD(100000), t.src);
|
||
env.close();
|
||
|
||
env(pay(t.gw, t.src, USD(10000)));
|
||
if (t.hasTrustline)
|
||
env(pay(t.gw, t.dst, USD(10000)));
|
||
env.close();
|
||
|
||
// dst can create uritoken
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(t.dst, uri))};
|
||
env(uritoken::mint(t.dst, uri));
|
||
env.close();
|
||
|
||
// dst can create sell
|
||
auto const delta = USD(1000);
|
||
auto const preSrc = lineBalance(env, t.src, t.gw, USD);
|
||
auto const preDst = lineBalance(env, t.dst, t.gw, USD);
|
||
env(uritoken::sell(t.dst, id), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(preDst == preDst);
|
||
|
||
// src can create buy
|
||
env(uritoken::buy(t.src, id), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(
|
||
lineBalance(env, t.src, t.gw, USD) ==
|
||
(t.negative ? (preSrc + delta) : (preSrc - delta)));
|
||
BEAST_EXPECT(
|
||
lineBalance(env, t.dst, t.gw, USD) ==
|
||
(t.negative ? (preDst - delta) : (preDst + delta)));
|
||
}
|
||
}
|
||
|
||
void
|
||
testGateway(FeatureBitset features)
|
||
{
|
||
testcase("gateway");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
struct TestAccountData
|
||
{
|
||
Account acct;
|
||
Account gw;
|
||
bool hasTrustline;
|
||
bool negative;
|
||
};
|
||
|
||
std::array<TestAccountData, 4> tests = {{
|
||
// acct no trustline
|
||
// acct > issuer
|
||
{Account("alice2"), Account{"gw0"}, false, true},
|
||
// acct < issuer
|
||
{Account("carol0"), Account{"gw1"}, false, false},
|
||
|
||
// acct has trustline
|
||
// acct > issuer
|
||
{Account("alice2"), Account{"gw0"}, true, true},
|
||
// acct < issuer
|
||
{Account("carol0"), Account{"gw1"}, true, false},
|
||
}};
|
||
|
||
// test gateway is buyer
|
||
for (auto const& t : tests)
|
||
{
|
||
Env env{*this, features};
|
||
auto const USD = t.gw["USD"];
|
||
env.fund(XRP(5000), t.acct, t.gw);
|
||
env.close();
|
||
|
||
if (t.hasTrustline)
|
||
env.trust(USD(100000), t.acct);
|
||
|
||
env.close();
|
||
|
||
if (t.hasTrustline)
|
||
env(pay(t.gw, t.acct, USD(10000)));
|
||
env.close();
|
||
|
||
// acct can create uritoken
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(t.acct, uri))};
|
||
env(uritoken::mint(t.acct, uri));
|
||
env.close();
|
||
|
||
// acct can create sell w/out token
|
||
auto const delta = USD(1000);
|
||
auto const preAcct = lineBalance(env, t.acct, t.gw, USD);
|
||
auto const preGw = lineBalance(env, t.gw, t.acct, USD);
|
||
env(uritoken::sell(t.acct, id), uritoken::amt(delta));
|
||
env.close();
|
||
auto const preAmount = t.hasTrustline ? 10000 : 0;
|
||
BEAST_EXPECT(
|
||
preAcct == (t.negative ? -USD(preAmount) : USD(preAmount)));
|
||
|
||
// gw can create buy
|
||
env(uritoken::buy(t.gw, id), uritoken::amt(delta));
|
||
env.close();
|
||
auto const postAmount = t.hasTrustline ? 11000 : 1000;
|
||
BEAST_EXPECT(
|
||
lineBalance(env, t.acct, t.gw, USD) ==
|
||
(t.negative ? -USD(postAmount) : USD(postAmount)));
|
||
BEAST_EXPECT(
|
||
lineBalance(env, t.gw, t.acct, USD) ==
|
||
(t.negative ? -USD(postAmount) : USD(postAmount)));
|
||
}
|
||
|
||
// test gateway is seller
|
||
// ignore hasTrustline
|
||
for (auto const& t : tests)
|
||
{
|
||
Env env{*this, features};
|
||
auto const USD = t.gw["USD"];
|
||
env.fund(XRP(5000), t.acct, t.gw);
|
||
env.close();
|
||
env.trust(USD(100000), t.acct);
|
||
env.close();
|
||
env(pay(t.gw, t.acct, USD(10000)));
|
||
env.close();
|
||
|
||
// gw can create uritoken
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(t.gw, uri))};
|
||
env(uritoken::mint(t.gw, uri));
|
||
env.close();
|
||
|
||
// gw can create sell w/out token
|
||
auto const delta = USD(1000);
|
||
auto const preAcct = lineBalance(env, t.acct, t.gw, USD);
|
||
auto const preGw = lineBalance(env, t.gw, t.acct, USD);
|
||
env(uritoken::sell(t.gw, id), uritoken::amt(delta));
|
||
env.close();
|
||
auto const preAmount = 10000;
|
||
BEAST_EXPECT(
|
||
preAcct == (t.negative ? -USD(preAmount) : USD(preAmount)));
|
||
|
||
// acct can create buy
|
||
env(uritoken::buy(t.acct, id), uritoken::amt(delta));
|
||
env.close();
|
||
auto const postAmount = 9000;
|
||
BEAST_EXPECT(
|
||
lineBalance(env, t.acct, t.gw, USD) ==
|
||
(t.negative ? -USD(postAmount) : USD(postAmount)));
|
||
BEAST_EXPECT(
|
||
lineBalance(env, t.gw, t.acct, USD) ==
|
||
(t.negative ? -USD(postAmount) : USD(postAmount)));
|
||
}
|
||
}
|
||
|
||
void
|
||
testRequireAuth(FeatureBitset features)
|
||
{
|
||
testcase("require_auth");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
auto const aliceUSD = alice["USD"];
|
||
auto const bobUSD = bob["USD"];
|
||
|
||
// test asfRequireAuth
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env(fset(gw, asfRequireAuth));
|
||
env.close();
|
||
env(trust(gw, bobUSD(10000)), txflags(tfSetfAuth));
|
||
env(trust(bob, USD(10000)));
|
||
env.close();
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// bob cannot buy because alice's trustline is not authorized
|
||
// all parties must be authorized
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecNO_AUTH));
|
||
env.close();
|
||
|
||
env(trust(gw, aliceUSD(10000)), txflags(tfSetfAuth));
|
||
env(trust(alice, USD(10000)));
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env.close();
|
||
|
||
// bob can now buy because alice's trustline is authorized
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
}
|
||
}
|
||
|
||
void
|
||
testFreeze(FeatureBitset features)
|
||
{
|
||
testcase("freeze");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
// test Global Freeze
|
||
{
|
||
// setup env
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
env(fset(gw, asfGlobalFreeze));
|
||
env.close();
|
||
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// bob cannot buy
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecINSUFFICIENT_FUNDS));
|
||
env.close();
|
||
|
||
// clear global freeze
|
||
env(fclear(gw, asfGlobalFreeze));
|
||
env.close();
|
||
|
||
// bob can buy
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
}
|
||
// test Individual Freeze
|
||
{
|
||
// Env Setup
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
{
|
||
// IOU bob(frozen) -> alice
|
||
|
||
// set freeze on bob trustline
|
||
env(trust(gw, USD(10000), bob, tfSetFreeze));
|
||
env.close();
|
||
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// buy uritoken fails - frozen trustline
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecINSUFFICIENT_FUNDS));
|
||
env.close();
|
||
|
||
// clear freeze on bob trustline
|
||
env(trust(gw, USD(10000), bob, tfClearFreeze));
|
||
env.close();
|
||
|
||
// buy uri success
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
}
|
||
{
|
||
// IOU alice -> bob(frozen)
|
||
|
||
// set freeze on bob trustline
|
||
env(trust(gw, USD(10000), bob, tfSetFreeze));
|
||
env.close();
|
||
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(bob, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(bob, uri));
|
||
env(uritoken::sell(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// buy uritoken fails - frozen trustline
|
||
env(uritoken::buy(alice, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecFROZEN));
|
||
env.close();
|
||
|
||
// clear freeze on bob trustline
|
||
env(trust(gw, USD(10000), bob, tfClearFreeze));
|
||
env.close();
|
||
|
||
// buy uri success
|
||
env(uritoken::buy(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
}
|
||
}
|
||
// test DeepFreeze
|
||
{
|
||
// Env Setup
|
||
Env env{*this, features};
|
||
env.fund(XRP(1000), alice, bob, gw);
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
{
|
||
// set freeze on bob trustline
|
||
env(trust(gw, USD(10000), bob, tfSetFreeze | tfSetDeepFreeze));
|
||
env.close();
|
||
|
||
// IOU bob(frozen) -> alice
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// buy uritoken fails - frozen trustline
|
||
env(uritoken::buy(bob, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecINSUFFICIENT_FUNDS));
|
||
env.close();
|
||
|
||
// clear freeze on bob trustline
|
||
env(trust(
|
||
gw, USD(10000), bob, tfClearFreeze | tfClearDeepFreeze));
|
||
env.close();
|
||
|
||
// buy uri success
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
}
|
||
{
|
||
// set freeze on bob trustline
|
||
env(trust(gw, USD(10000), bob, tfSetFreeze | tfSetDeepFreeze));
|
||
env.close();
|
||
|
||
// IOU alice -> bob(frozen)
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(bob, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(bob, uri));
|
||
env(uritoken::sell(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
|
||
// buy uritoken fails - frozen trustline
|
||
env(uritoken::buy(alice, hexid),
|
||
uritoken::amt(USD(10)),
|
||
ter(tecFROZEN));
|
||
env.close();
|
||
|
||
// clear freeze on bob trustline
|
||
env(trust(
|
||
gw, USD(10000), bob, tfClearFreeze | tfClearDeepFreeze));
|
||
env.close();
|
||
|
||
// buy uri success
|
||
env(uritoken::buy(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
}
|
||
}
|
||
}
|
||
|
||
void
|
||
testTransferRate(FeatureBitset features)
|
||
{
|
||
testcase("transfer_rate");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
|
||
struct TestRateData
|
||
{
|
||
double rate;
|
||
STAmount delta;
|
||
std::string multiply;
|
||
std::string divide;
|
||
};
|
||
std::array<TestRateData, 9> testCases = {{
|
||
{1, USD(100), "1100", "1100"},
|
||
{1.1, USD(100), "1110", "1090.909090909091"},
|
||
{1.0005, USD(100), "1100.05", "1099.950024987506"},
|
||
{1.005, USD(100), "1100.4999999", "1099.502487661197"},
|
||
{1.25, USD(100), "1125", "1080"},
|
||
{2, USD(100), "1200", "1050"},
|
||
{1.25, USD(1), "1001.25", "1000.8"},
|
||
{1.25, USD(0.1), "1000.125", "1000.08"},
|
||
{1.25, USD(0.0001), "1000.000125", "1000.00008"},
|
||
}};
|
||
|
||
for (auto const& tc : testCases)
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(10000), alice, bob, gw);
|
||
env(rate(gw, tc.rate));
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
auto const preAlice = env.balance(alice, USD.issue());
|
||
auto const preBob = env.balance(bob, USD.issue());
|
||
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(alice, uri))};
|
||
auto const delta = tc.delta;
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, id), uritoken::amt(delta));
|
||
env.close();
|
||
|
||
env(uritoken::buy(bob, id), uritoken::amt(delta));
|
||
env.close();
|
||
auto const postAlice = env.balance(alice, USD.issue());
|
||
if (!env.current()->rules().enabled(fixXahauV1))
|
||
{
|
||
BEAST_EXPECT(to_string(postAlice.value()) == tc.multiply);
|
||
}
|
||
else
|
||
{
|
||
BEAST_EXPECT(to_string(postAlice.value()) == tc.divide);
|
||
}
|
||
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta);
|
||
}
|
||
|
||
// test dust amount
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(10000), alice, bob, gw);
|
||
env(rate(gw, 1.50));
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(1000)));
|
||
env(pay(gw, bob, USD(1000)));
|
||
env.close();
|
||
|
||
auto const preAlice = env.balance(alice, USD.issue());
|
||
auto const preBob = env.balance(bob, USD.issue());
|
||
|
||
// setup mint
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(alice, uri))};
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
auto sellTx = uritoken::sell(alice, id);
|
||
sellTx[jss::Amount][jss::issuer] = gw.human();
|
||
sellTx[jss::Amount][jss::currency] = "USD";
|
||
sellTx[jss::Amount][jss::value] = "1e-81";
|
||
env(sellTx);
|
||
env.close();
|
||
|
||
auto buyTx = uritoken::buy(bob, id);
|
||
buyTx[jss::Amount][jss::issuer] = gw.human();
|
||
buyTx[jss::Amount][jss::currency] = "USD";
|
||
buyTx[jss::Amount][jss::value] = "1e-81";
|
||
env(buyTx);
|
||
env.close();
|
||
auto const postAlice = env.balance(alice, USD.issue());
|
||
|
||
if (!env.current()->rules().enabled(fixXahauV1))
|
||
{
|
||
BEAST_EXPECT(postAlice.value() == preAlice);
|
||
}
|
||
else
|
||
{
|
||
BEAST_EXPECT(postAlice.value() == preAlice);
|
||
}
|
||
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - USD(0));
|
||
}
|
||
|
||
// test rate change
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(10000), alice, bob, gw);
|
||
env(rate(gw, 1.25));
|
||
env.close();
|
||
env.trust(USD(100000), alice, bob);
|
||
env.close();
|
||
env(pay(gw, alice, USD(10000)));
|
||
env(pay(gw, bob, USD(10000)));
|
||
env.close();
|
||
|
||
// setup
|
||
std::string const uri(maxTokenURILength, '?');
|
||
std::string const id{strHex(uritoken::tokenid(alice, uri))};
|
||
auto const delta = USD(100);
|
||
auto preBob = env.balance(bob, USD.issue());
|
||
// alice mints and sells
|
||
env(uritoken::mint(alice, uri));
|
||
env(uritoken::sell(alice, id), uritoken::amt(delta));
|
||
env.close();
|
||
|
||
// issuer changes rate lower
|
||
env(rate(gw, 1.00));
|
||
env.close();
|
||
|
||
// bob buys at higher rate and burns
|
||
env(uritoken::buy(bob, id), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(env.balance(alice, USD.issue()) == USD(10100));
|
||
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta);
|
||
env(uritoken::burn(bob, id));
|
||
}
|
||
// test issuer doesnt pay own rate
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(10000), alice, gw);
|
||
env(rate(gw, 1.25));
|
||
env.close();
|
||
env.trust(USD(100000), alice);
|
||
env.close();
|
||
env(pay(gw, alice, USD(10000)));
|
||
env.close();
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
auto const delta = USD(10);
|
||
auto const preAlice = env.balance(alice, USD.issue());
|
||
|
||
// alice mints
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
|
||
// gw buys
|
||
env(uritoken::buy(gw, hexid), uritoken::amt(delta));
|
||
env.close();
|
||
BEAST_EXPECT(env.balance(alice, USD.issue()) == preAlice + delta);
|
||
}
|
||
}
|
||
|
||
void
|
||
testDisallowXRP(FeatureBitset features)
|
||
{
|
||
// auth amount defaults to balance if not present
|
||
testcase("disallow_xrp");
|
||
using namespace jtx;
|
||
using namespace std::literals::chrono_literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
|
||
{
|
||
// Create a channel where src/dst disallows XRP
|
||
// Ignore that flag, since it's just advisory.
|
||
Env env(*this, features);
|
||
env.fund(XRP(10000), alice, bob);
|
||
env(fset(alice, asfDisallowXRP));
|
||
env(fset(bob, asfDisallowXRP));
|
||
env.close();
|
||
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
|
||
// alice mints
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
|
||
// alice sells
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(XRP(10)));
|
||
env.close();
|
||
|
||
// bob buys
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10)));
|
||
env.close();
|
||
}
|
||
}
|
||
|
||
void
|
||
testLimitAmount(FeatureBitset features)
|
||
{
|
||
testcase("limit_amount");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
auto const carol = Account("carol");
|
||
auto const gw = Account{"gateway"};
|
||
auto const USD = gw["USD"];
|
||
{
|
||
Env env{*this, features};
|
||
env.fund(XRP(10000), alice, bob, carol, gw);
|
||
env.close();
|
||
env.trust(USD(1000), bob);
|
||
env.trust(USD(1000), carol);
|
||
env.close();
|
||
env(pay(gw, bob, USD(1000)));
|
||
env(pay(gw, carol, USD(1000)));
|
||
env.close();
|
||
std::string const uri(maxTokenURILength, '?');
|
||
auto const tid = uritoken::tokenid(alice, uri);
|
||
std::string const hexid{strHex(tid)};
|
||
env(uritoken::mint(alice, uri));
|
||
env.close();
|
||
BEAST_EXPECT(inOwnerDir(*env.current(), alice, tid));
|
||
env(uritoken::sell(alice, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
auto preLimit = limitAmount(env, alice, gw, USD);
|
||
BEAST_EXPECT(preLimit == USD(0));
|
||
env(uritoken::buy(bob, hexid), uritoken::amt(USD(10)));
|
||
env.close();
|
||
auto const postLimit = limitAmount(env, bob, gw, USD);
|
||
BEAST_EXPECT(postLimit == preLimit);
|
||
env(pay(alice, carol, USD(1)), ter(tecPATH_DRY));
|
||
}
|
||
}
|
||
|
||
void
|
||
testURIUTF8(FeatureBitset features)
|
||
{
|
||
// https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
|
||
testcase("uri_utf8");
|
||
using namespace test::jtx;
|
||
using namespace std::literals;
|
||
|
||
auto const alice = Account("alice");
|
||
auto const bob = Account("bob");
|
||
|
||
Env env{*this, features};
|
||
env.fund(XRP(10000), alice, bob);
|
||
env.close();
|
||
|
||
std::string uri = "";
|
||
|
||
// test utf-8 success
|
||
{
|
||
// case: kosme
|
||
uri = "κόσμε";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: single ASCII character
|
||
uri = "a";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: single non-ASCII character
|
||
uri = "é";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: valid multi-byte UTF-8 sequence
|
||
uri = "€";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: ipfs cid
|
||
uri = "QmaCtDKZFVvvfufvbdy4estZbhQH7DXh16CTpv1howmBGy";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: empty ipfs cid url
|
||
uri = "ipfs://";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: ipfs cid url
|
||
uri = "ipfs://QmaCtDKZFVvvfufvbdy4estZbhQH7DXh16CTpv1howmBGy";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// case: ipfs metadata url
|
||
uri = "https://example.com/ipfs/";
|
||
env(uritoken::mint(alice, uri));
|
||
|
||
// BOUNDRY - START
|
||
// ----------------------------------------------------------------
|
||
|
||
// case: 1 byte (U-00000000)
|
||
uri = "\x00";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
// case: 2 bytes (U-00000080)
|
||
uri = "\xC2\x80";
|
||
env(uritoken::mint(alice, uri));
|
||
// case: 3 bytes (U-00000800)
|
||
uri = "\xE0\xA0\x80";
|
||
env(uritoken::mint(alice, uri));
|
||
// case: 4 bytes (U-00010000)
|
||
uri = "\xF0\x90\x80\x80";
|
||
env(uritoken::mint(alice, uri));
|
||
// case: 5 bytes (U-00200000)
|
||
uri = "\xF8\x88\x80\x80\x80";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
// case: 6 bytes (U-04000000)
|
||
uri = "\xFC\x84\x80\x80\x80\x80";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
|
||
// BOUNDRY - END
|
||
// ----------------------------------------------------------------
|
||
|
||
// case: 1 byte (U-0000007F)
|
||
uri = "\x7F";
|
||
env(uritoken::mint(alice, uri));
|
||
// case: 2 bytes (U-000007FF)
|
||
uri = "\xDF\xBF";
|
||
env(uritoken::mint(alice, uri));
|
||
// case: 3 bytes (U-0000FFFF)
|
||
uri = "\xEF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
// case: 4 bytes (U-001FFFFF)
|
||
uri = "\xF7\xBF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
// case: 5 bytes (U-03FFFFFF)
|
||
uri = "\xFB\xBF\xBF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
// case: 6 bytes (U-7FFFFFFF)
|
||
uri = "\xFD\xBF\xBF\xBF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
|
||
// // BOUNDRY - OTHER
|
||
// ----------------------------------------------------------------
|
||
// case: 1 bytes (U-0000D7FF)
|
||
uri = "\xD7\xFF";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
// case: 2 bytes (U-0000E000)
|
||
uri = "\xEE\x80\x80";
|
||
env(uritoken::mint(alice, uri));
|
||
// case: 3 bytes (U-0000FFFD)
|
||
uri = "\xEF\xBF\xBD";
|
||
env(uritoken::mint(alice, uri));
|
||
// // case: 4 bytes (U-0010FFFF)
|
||
uri = "\xF4\x8F\xBF\xBF";
|
||
env(uritoken::mint(alice, uri));
|
||
// // case: 4 bytes (U-00110000)
|
||
uri = "\xF4\x90\x80\x80";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||
}
|
||
// test utf8 malformed
|
||
{
|
||
// MALFORMED - END
|
||
// ----------------------------------------------------------------
|
||
// First continuation byte 0x80:
|
||
uri = "\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// Last continuation byte 0xbf
|
||
uri = "\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// 2 continuation bytes
|
||
uri = "<EFBFBD><EFBFBD>";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// 3 continuation bytes
|
||
uri = "<EFBFBD><EFBFBD><EFBFBD>";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// 4 continuation bytes
|
||
uri = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// 5 continuation bytes
|
||
uri = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// 6 continuation bytes
|
||
uri = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// 7 continuation bytes
|
||
uri = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// Sequence of all 64 possible continuation bytes (0x80-0xbf)
|
||
uri =
|
||
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E"
|
||
"\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D"
|
||
"\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC"
|
||
"\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB"
|
||
"\xBC\xBD\xBE\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// TODO: REVIEW - THIS IS NOT THE CORRECT 32 byte sequence.
|
||
// All 32 first bytes of 2-byte sequences (0xc0-0xdf), each followed
|
||
// by a space character
|
||
// uri = "\xE0\x80\x80 \xE0\x80\x81 \xE0\x80\x82 \xE0\x80\x83
|
||
// \xE0\x80\x84 \xE0\x80\x85 \xE0\x80\x86 \xE0\x80\x87 \xE0\x80\x88
|
||
// \xE0\x80\x89 \xE0\x80\x8A \xE0\x80\x8B \xE0\x80\x8C \xE0\x80\x8D
|
||
// \xE0\x80\x8E \xE0\x80\x8F \xE0\x80\x90";
|
||
// env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// All 16 first bytes of 3-byte sequences (0xe0-0xef), each followed
|
||
// by a space character
|
||
uri =
|
||
"\xE0\x80\x80 \xE0\x80\x81 \xE0\x80\x82 \xE0\x80\x83 "
|
||
"\xE0\x80\x84 \xE0\x80\x85 \xE0\x80\x86 \xE0\x80\x87 "
|
||
"\xE0\x80\x88 \xE0\x80\x89 \xE0\x80\x8A \xE0\x80\x8B "
|
||
"\xE0\x80\x8C \xE0\x80\x8D \xE0\x80\x8E \xE0\x80\x8F "
|
||
"\xE0\x80\x90";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// All 8 first bytes of 4-byte sequences (0xf0-0xf7), each followed
|
||
// by a space character
|
||
uri =
|
||
"\xF0\x90\x80\x80 \xF0\x90\x80\x81 \xF0\x90\x80\x82 "
|
||
"\xF0\x90\x80\x83 \xF0\x90\x80\x84 \xF0\x90\x80\x85 "
|
||
"\xF0\x90\x80\x86 \xF0\x90\x80\x87";
|
||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// All 4 first bytes of 5-byte sequences (0xf8-0xfb), each followed
|
||
// by a space character
|
||
uri =
|
||
"\xF8\x88\x80\x80\x80 \xF8\x88\x80\x80\x81 "
|
||
"\xF8\x88\x80\x80\x82 \xF8\x88\x80\x80\x83";
|
||
env(uritoken::mint(alice, uri),
|
||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD FAIL
|
||
|
||
// All 2 first bytes of 6-byte sequences (0xfc-0xfd), each followed
|
||
// by a space character
|
||
uri = "\xFC\x84\x80\x80\x80\x80 \xFC\x84\x80\x80\x80\x81";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// Sequences with last continuation byte missing
|
||
|
||
// Concatenation of incomplete sequences
|
||
|
||
// Impossible bytes
|
||
uri = "\xFE";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
uri = "\xFF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
uri = "\xFE\xFE\xFF\xFF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// Examples of an overlong ASCII character
|
||
// case: (U+002F)
|
||
uri = "\xC0\xAF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+002F)
|
||
uri = "\xE0\x80\xAF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+002F)
|
||
uri = "\xF0\x80\x80\xAF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+002F)
|
||
uri = "\xF0\x80\x80\x80\xAF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+002F)
|
||
uri = "\xF0\x80\x80\x80\x80\xAF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// Maximum overlong sequences
|
||
// case: (U+0000007F)
|
||
uri = "\xC1\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+000007FF)
|
||
uri = "\xE0\x9F\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+0000FFFF)
|
||
uri = "\xF0\x8F\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+001FFFFF)
|
||
uri = "\xF8\x87\xBF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+03FFFFFF)
|
||
uri = "\xFC\x83\xBF\xBF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// Overlong representation of the NUL character
|
||
// case: (U+0000)
|
||
uri = "\xC0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+0000)
|
||
uri = "\xC0\x80\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+0000)
|
||
uri = "\xC0\x80\x80\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+0000)
|
||
uri = "\xC0\x80\x80\x80\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+0000)
|
||
uri = "\xC0\x80\x80\x80\x80\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// Single UTF-16 surrogates
|
||
// case: (U+D800)
|
||
uri = "\xED\xA0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DB7F)
|
||
uri = "\xED\xAD\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DB80)
|
||
uri = "\xED\xAE\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DBFF)
|
||
uri = "\xED\xAF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DC00)
|
||
uri = "\xED\xB0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DF80)
|
||
uri = "\xED\xBE\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DFFF)
|
||
uri = "\xED\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// Paired UTF-16 surrogates
|
||
// case: (U+D800 U+DC00)
|
||
uri = "\xED\xA0\x80\xED\xB0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+D800 U+DFFF)
|
||
uri = "\xED\xA0\x80\xED\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DB7F U+DC00)
|
||
uri = "\xED\xAD\xBF\xED\xB0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DB7F U+DFFF)
|
||
uri = "\xED\xAD\xBF\xED\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DB80 U+DC00)
|
||
uri = "\xED\xAE\x80\xED\xB0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DB80 U+DFFF)
|
||
uri = "\xED\xAE\x80\xED\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DBFF U+DC00)
|
||
uri = "\xED\xAF\xBF\xED\xB0\x80";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+DBFF U+DFFF)
|
||
uri = "\xED\xAF\xBF\xED\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
|
||
// problematic noncharacters in 16-bit applications
|
||
// case: (U+FFFE)
|
||
uri = "\xEF\xBF\xBE";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
// case: (U+FFFF)
|
||
uri = "\xEF\xBF\xBF";
|
||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||
}
|
||
}
|
||
void
|
||
testWithFeats(FeatureBitset features)
|
||
{
|
||
testEnabled(features);
|
||
testMintInvalid(features);
|
||
testBurnInvalid(features);
|
||
testSellInvalid(features);
|
||
testBuyInvalid(features);
|
||
testClearInvalid(features);
|
||
testMintValid(features);
|
||
testBurnValid(features);
|
||
testBuyValid(features);
|
||
testSellValid(features);
|
||
testClearValid(features);
|
||
testMetaAndOwnership(features);
|
||
testAccountDelete(features);
|
||
testTickets(features);
|
||
testRippleState(features);
|
||
testGateway(features);
|
||
testRequireAuth(features);
|
||
testFreeze(features);
|
||
testTransferRate(features);
|
||
testDisallowXRP(features);
|
||
testLimitAmount(features);
|
||
testURIUTF8(features);
|
||
}
|
||
|
||
public:
|
||
void
|
||
run() override
|
||
{
|
||
using namespace test::jtx;
|
||
auto const sa = supported_amendments();
|
||
testWithFeats(sa);
|
||
testWithFeats(sa - fixXahauV1);
|
||
}
|
||
};
|
||
|
||
BEAST_DEFINE_TESTSUITE(URIToken, app, ripple);
|
||
} // namespace test
|
||
} // namespace ripple
|