mirror of
https://github.com/XRPLF/rippled.git
synced 2026-02-03 13:35:25 +00:00
3178 lines
119 KiB
C++
3178 lines
119 KiB
C++
#include <test/jtx.h>
|
|
#include <test/jtx/credentials.h>
|
|
#include <test/jtx/permissioned_domains.h>
|
|
#include <test/jtx/trust.h>
|
|
#include <test/jtx/xchain_bridge.h>
|
|
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/beast/utility/Zero.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
namespace xrpl {
|
|
namespace test {
|
|
|
|
class MPToken_test : public beast::unit_test::suite
|
|
{
|
|
void
|
|
testCreateValidation(FeatureBitset features)
|
|
{
|
|
testcase("Create Validate");
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
|
|
// test preflight of MPTokenIssuanceCreate
|
|
{
|
|
// If the MPT amendment is not enabled, you should not be able to
|
|
// create MPTokenIssuances
|
|
Env env{*this, features - featureMPTokensV1};
|
|
MPTTester mptAlice(env, alice);
|
|
|
|
mptAlice.create({.ownerCount = 0, .err = temDISABLED});
|
|
}
|
|
|
|
// test preflight of MPTokenIssuanceCreate
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
|
|
mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG});
|
|
|
|
// tries to set a txfee while not enabling in the flag
|
|
mptAlice.create(
|
|
{.maxAmt = 100, .assetScale = 0, .transferFee = 1, .metadata = "test", .err = temMALFORMED});
|
|
|
|
if (!features[featureSingleAssetVault])
|
|
{
|
|
// tries to set DomainID when SAV is disabled
|
|
mptAlice.create(
|
|
{.maxAmt = 100,
|
|
.assetScale = 0,
|
|
.metadata = "test",
|
|
.flags = tfMPTRequireAuth,
|
|
.domainID = uint256(42),
|
|
.err = temDISABLED});
|
|
}
|
|
else if (!features[featurePermissionedDomains])
|
|
{
|
|
// tries to set DomainID when PD is disabled
|
|
mptAlice.create(
|
|
{.maxAmt = 100,
|
|
.assetScale = 0,
|
|
.metadata = "test",
|
|
.flags = tfMPTRequireAuth,
|
|
.domainID = uint256(42),
|
|
.err = temDISABLED});
|
|
}
|
|
else
|
|
{
|
|
// tries to set DomainID when RequireAuth is not set
|
|
mptAlice.create(
|
|
{.maxAmt = 100, .assetScale = 0, .metadata = "test", .domainID = uint256(42), .err = temMALFORMED});
|
|
|
|
// tries to set zero DomainID
|
|
mptAlice.create(
|
|
{.maxAmt = 100,
|
|
.assetScale = 0,
|
|
.metadata = "test",
|
|
.flags = tfMPTRequireAuth,
|
|
.domainID = beast::zero,
|
|
.err = temMALFORMED});
|
|
}
|
|
|
|
// tries to set a txfee greater than max
|
|
mptAlice.create(
|
|
{.maxAmt = 100,
|
|
.assetScale = 0,
|
|
.transferFee = maxTransferFee + 1,
|
|
.metadata = "test",
|
|
.flags = tfMPTCanTransfer,
|
|
.err = temBAD_TRANSFER_FEE});
|
|
|
|
// tries to set a txfee while not enabling transfer
|
|
mptAlice.create(
|
|
{.maxAmt = 100,
|
|
.assetScale = 0,
|
|
.transferFee = maxTransferFee,
|
|
.metadata = "test",
|
|
.err = temMALFORMED});
|
|
|
|
// empty metadata returns error
|
|
mptAlice.create({.maxAmt = 100, .assetScale = 0, .transferFee = 0, .metadata = "", .err = temMALFORMED});
|
|
|
|
// MaximumAmount of 0 returns error
|
|
mptAlice.create({.maxAmt = 0, .assetScale = 1, .transferFee = 1, .metadata = "test", .err = temMALFORMED});
|
|
|
|
// MaximumAmount larger than 63 bit returns error
|
|
mptAlice.create(
|
|
{.maxAmt = 0xFFFF'FFFF'FFFF'FFF0, // 18'446'744'073'709'551'600
|
|
.assetScale = 0,
|
|
.transferFee = 0,
|
|
.metadata = "test",
|
|
.err = temMALFORMED});
|
|
mptAlice.create(
|
|
{.maxAmt = maxMPTokenAmount + 1, // 9'223'372'036'854'775'808
|
|
.assetScale = 0,
|
|
.transferFee = 0,
|
|
.metadata = "test",
|
|
.err = temMALFORMED});
|
|
}
|
|
}
|
|
|
|
void
|
|
testCreateEnabled(FeatureBitset features)
|
|
{
|
|
testcase("Create Enabled");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
|
|
{
|
|
// If the MPT amendment IS enabled, you should be able to create
|
|
// MPTokenIssuances
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create(
|
|
{.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807
|
|
.assetScale = 1,
|
|
.transferFee = 10,
|
|
.metadata = "123",
|
|
.ownerCount = 1,
|
|
.flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer |
|
|
tfMPTCanClawback});
|
|
|
|
// Get the hash for the most recent transaction.
|
|
std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
|
|
|
Json::Value const result = env.rpc("tx", txHash)[jss::result];
|
|
BEAST_EXPECT(result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
|
|
}
|
|
|
|
if (features[featureSingleAssetVault])
|
|
{
|
|
// Add permissioned domain
|
|
Account const credIssuer1{"credIssuer1"};
|
|
std::string const credType = "credential";
|
|
|
|
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
{
|
|
Env env{*this, features};
|
|
env.fund(XRP(1000), credIssuer1);
|
|
|
|
env(pdomain::setTx(credIssuer1, credentials1));
|
|
auto const domainId1 = [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create({
|
|
.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807
|
|
.assetScale = 1,
|
|
.transferFee = 10,
|
|
.metadata = "123",
|
|
.ownerCount = 1,
|
|
.flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer |
|
|
tfMPTCanClawback,
|
|
.domainID = domainId1,
|
|
});
|
|
|
|
// Get the hash for the most recent transaction.
|
|
std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
|
|
|
Json::Value const result = env.rpc("tx", txHash)[jss::result];
|
|
BEAST_EXPECT(result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testDestroyValidation(FeatureBitset features)
|
|
{
|
|
testcase("Destroy Validate");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
// MPTokenIssuanceDestroy (preflight)
|
|
{
|
|
Env env{*this, features - featureMPTokensV1};
|
|
MPTTester mptAlice(env, alice);
|
|
auto const id = makeMptID(env.seq(alice), alice);
|
|
mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED});
|
|
|
|
env.enableFeature(featureMPTokensV1);
|
|
|
|
mptAlice.destroy({.id = id, .flags = 0x00000001, .err = temINVALID_FLAG});
|
|
}
|
|
|
|
// MPTokenIssuanceDestroy (preclaim)
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.destroy({.id = makeMptID(env.seq(alice), alice), .ownerCount = 0, .err = tecOBJECT_NOT_FOUND});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
// a non-issuer tries to destroy a mptissuance they didn't issue
|
|
mptAlice.destroy({.issuer = bob, .err = tecNO_PERMISSION});
|
|
|
|
// Make sure that issuer can't delete issuance when it still has
|
|
// outstanding balance
|
|
{
|
|
// bob now holds a mptoken object
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
mptAlice.destroy({.err = tecHAS_OBLIGATIONS});
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testDestroyEnabled(FeatureBitset features)
|
|
{
|
|
testcase("Destroy Enabled");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
|
|
// If the MPT amendment IS enabled, you should be able to destroy
|
|
// MPTokenIssuances
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
mptAlice.destroy({.ownerCount = 0});
|
|
}
|
|
|
|
void
|
|
testAuthorizeValidation(FeatureBitset features)
|
|
{
|
|
testcase("Validate authorize transaction");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const cindy("cindy");
|
|
// Validate amendment enable in MPTokenAuthorize (preflight)
|
|
{
|
|
Env env{*this, features - featureMPTokensV1};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.authorize({.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED});
|
|
}
|
|
|
|
// Validate fields in MPTokenAuthorize (preflight)
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
// The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which
|
|
// has a value of 1
|
|
mptAlice.authorize({.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG});
|
|
|
|
mptAlice.authorize({.account = bob, .holder = bob, .err = temMALFORMED});
|
|
|
|
mptAlice.authorize({.holder = alice, .err = temMALFORMED});
|
|
}
|
|
|
|
// Try authorizing when MPTokenIssuance doesn't exist in
|
|
// MPTokenAuthorize (preclaim)
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
auto const id = makeMptID(env.seq(alice), alice);
|
|
|
|
mptAlice.authorize({.holder = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
|
|
|
|
mptAlice.authorize({.account = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
// Test bad scenarios without allowlisting in MPTokenAuthorize
|
|
// (preclaim)
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
// bob submits a tx with a holder field
|
|
mptAlice.authorize({.account = bob, .holder = alice, .err = tecNO_PERMISSION});
|
|
|
|
// alice tries to hold onto her own token
|
|
mptAlice.authorize({.account = alice, .err = tecNO_PERMISSION});
|
|
|
|
// the mpt does not enable allowlisting
|
|
mptAlice.authorize({.holder = bob, .err = tecNO_AUTH});
|
|
|
|
// bob now holds a mptoken object
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// bob cannot create the mptoken the second time
|
|
mptAlice.authorize({.account = bob, .err = tecDUPLICATE});
|
|
|
|
// Check that bob cannot delete MPToken when his balance is
|
|
// non-zero
|
|
{
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// bob tries to delete his MPToken, but fails since he still
|
|
// holds tokens
|
|
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS});
|
|
|
|
// bob pays back alice 100 tokens
|
|
mptAlice.pay(bob, alice, 100);
|
|
}
|
|
|
|
// bob deletes/unauthorizes his MPToken
|
|
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
|
|
|
|
// bob receives error when he tries to delete his MPToken that has
|
|
// already been deleted
|
|
mptAlice.authorize(
|
|
{.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize, .err = tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
// Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim)
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});
|
|
|
|
// alice submits a tx without specifying a holder's account
|
|
mptAlice.authorize({.err = tecNO_PERMISSION});
|
|
|
|
// alice submits a tx to authorize a holder that hasn't created
|
|
// a mptoken yet
|
|
mptAlice.authorize({.holder = bob, .err = tecOBJECT_NOT_FOUND});
|
|
|
|
// alice specifies a holder acct that doesn't exist
|
|
mptAlice.authorize({.holder = cindy, .err = tecNO_DST});
|
|
|
|
// bob now holds a mptoken object
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// alice tries to unauthorize bob.
|
|
// although tx is successful,
|
|
// but nothing happens because bob hasn't been authorized yet
|
|
mptAlice.authorize({.holder = bob, .flags = tfMPTUnauthorize});
|
|
|
|
// alice authorizes bob
|
|
// make sure bob's mptoken has set lsfMPTAuthorized
|
|
mptAlice.authorize({.holder = bob});
|
|
|
|
// alice tries authorizes bob again.
|
|
// tx is successful, but bob is already authorized,
|
|
// so no changes
|
|
mptAlice.authorize({.holder = bob});
|
|
|
|
// bob deletes his mptoken
|
|
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
|
|
}
|
|
|
|
// Test mptoken reserve requirement - first two mpts free (doApply)
|
|
{
|
|
Env env{*this, features};
|
|
auto const acctReserve = env.current()->fees().reserve;
|
|
auto const incReserve = env.current()->fees().increment;
|
|
|
|
// 1 drop
|
|
BEAST_EXPECT(incReserve > XRPAmount(1));
|
|
MPTTester mptAlice1(env, alice, {.holders = {bob}, .xrpHolders = acctReserve + (incReserve - 1)});
|
|
mptAlice1.create();
|
|
|
|
MPTTester mptAlice2(env, alice, {.fund = false});
|
|
mptAlice2.create();
|
|
|
|
MPTTester mptAlice3(env, alice, {.fund = false});
|
|
mptAlice3.create({.ownerCount = 3});
|
|
|
|
// first mpt for free
|
|
mptAlice1.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// second mpt free
|
|
mptAlice2.authorize({.account = bob, .holderCount = 2});
|
|
|
|
mptAlice3.authorize({.account = bob, .err = tecINSUFFICIENT_RESERVE});
|
|
|
|
env(pay(env.master, bob, drops(incReserve + incReserve + incReserve)));
|
|
env.close();
|
|
|
|
mptAlice3.authorize({.account = bob, .holderCount = 3});
|
|
}
|
|
}
|
|
|
|
void
|
|
testAuthorizeEnabled(FeatureBitset features)
|
|
{
|
|
testcase("Authorize Enabled");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
// Basic authorization without allowlisting
|
|
{
|
|
Env env{*this, features};
|
|
|
|
// alice create mptissuance without allowisting
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
// bob creates a mptoken
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
mptAlice.authorize({.account = bob, .holderCount = 1, .err = tecDUPLICATE});
|
|
|
|
// bob deletes his mptoken
|
|
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
|
|
}
|
|
|
|
// With allowlisting
|
|
{
|
|
Env env{*this, features};
|
|
|
|
// alice creates a mptokenissuance that requires authorization
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});
|
|
|
|
// bob creates a mptoken
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// alice authorizes bob
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// Unauthorize bob's mptoken
|
|
mptAlice.authorize({.account = alice, .holder = bob, .holderCount = 1, .flags = tfMPTUnauthorize});
|
|
|
|
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
|
|
}
|
|
|
|
// Holder can have dangling MPToken even if issuance has been destroyed.
|
|
// Make sure they can still delete/unauthorize the MPToken
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
// bob creates a mptoken
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// alice deletes her issuance
|
|
mptAlice.destroy({.ownerCount = 0});
|
|
|
|
// bob can delete his mptoken even though issuance is no longer
|
|
// existent
|
|
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
|
|
}
|
|
}
|
|
|
|
void
|
|
testSetValidation(FeatureBitset features)
|
|
{
|
|
testcase("Validate set transaction");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice"); // issuer
|
|
Account const bob("bob"); // holder
|
|
Account const cindy("cindy");
|
|
// Validate fields in MPTokenIssuanceSet (preflight)
|
|
{
|
|
Env env{*this, features - featureMPTokensV1};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.set({.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED});
|
|
|
|
env.enableFeature(featureMPTokensV1);
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// test invalid flag - only valid flags are tfMPTLock (1) and Unlock
|
|
// (2)
|
|
mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG});
|
|
|
|
if (!features[featureSingleAssetVault] && !features[featureDynamicMPT] &&
|
|
!features[featureConfidentialTransfer])
|
|
{
|
|
// test invalid flags - nothing is being changed
|
|
mptAlice.set({.account = alice, .flags = 0x00000000, .err = tecNO_PERMISSION});
|
|
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = 0x00000000, .err = tecNO_PERMISSION});
|
|
|
|
// cannot set DomainID since SAV is not enabled
|
|
mptAlice.set({.account = alice, .domainID = uint256(42), .err = temDISABLED});
|
|
}
|
|
else
|
|
{
|
|
// test invalid flags - nothing is being changed
|
|
mptAlice.set({.account = alice, .flags = 0x00000000, .err = temMALFORMED});
|
|
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = 0x00000000, .err = temMALFORMED});
|
|
|
|
if (!features[featurePermissionedDomains] || !features[featureSingleAssetVault])
|
|
{
|
|
// cannot set DomainID since PD is not enabled
|
|
mptAlice.set({.account = alice, .domainID = uint256(42), .err = temDISABLED});
|
|
}
|
|
else if (features[featureSingleAssetVault])
|
|
{
|
|
// cannot set DomainID since Holder is set
|
|
mptAlice.set({.account = alice, .holder = bob, .domainID = uint256(42), .err = temMALFORMED});
|
|
}
|
|
}
|
|
|
|
// set both lock and unlock flags at the same time will fail
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock | tfMPTUnlock, .err = temINVALID_FLAG});
|
|
|
|
// if the holder is the same as the acct that submitted the tx,
|
|
// tx fails
|
|
mptAlice.set({.account = alice, .holder = alice, .flags = tfMPTLock, .err = temMALFORMED});
|
|
}
|
|
|
|
// Validate fields in MPTokenIssuanceSet (preclaim)
|
|
// test when a mptokenissuance has disabled locking
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
// alice tries to lock a mptissuance that has disabled locking
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION});
|
|
|
|
// alice tries to unlock mptissuance that has disabled locking
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
|
|
|
|
// issuer tries to lock a bob's mptoken that has disabled
|
|
// locking
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
|
|
|
|
// issuer tries to unlock a bob's mptoken that has disabled
|
|
// locking
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
|
|
}
|
|
|
|
// Validate fields in MPTokenIssuanceSet (preclaim)
|
|
// test when mptokenissuance has enabled locking
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// alice trying to set when the mptissuance doesn't exist yet
|
|
mptAlice.set({.id = makeMptID(env.seq(alice), alice), .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND});
|
|
|
|
// create a mptokenissuance with locking
|
|
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock});
|
|
|
|
// a non-issuer acct tries to set the mptissuance
|
|
mptAlice.set({.account = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
|
|
|
|
// trying to set a holder who doesn't have a mptoken
|
|
mptAlice.set({.holder = bob, .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND});
|
|
|
|
// trying to set a holder who doesn't exist
|
|
mptAlice.set({.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST});
|
|
}
|
|
|
|
if (features[featureSingleAssetVault] && features[featurePermissionedDomains])
|
|
{
|
|
// Add permissioned domain
|
|
Account const credIssuer1{"credIssuer1"};
|
|
std::string const credType = "credential";
|
|
|
|
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create({});
|
|
|
|
// Trying to set DomainID on a public MPTokenIssuance
|
|
mptAlice.set({.domainID = uint256(42), .err = tecNO_PERMISSION});
|
|
|
|
mptAlice.set({.domainID = beast::zero, .err = tecNO_PERMISSION});
|
|
}
|
|
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create({.flags = tfMPTRequireAuth});
|
|
|
|
// Trying to set non-existing DomainID
|
|
mptAlice.set({.domainID = uint256(42), .err = tecOBJECT_NOT_FOUND});
|
|
|
|
// Trying to lock but locking is disabled
|
|
mptAlice.set({.flags = tfMPTUnlock, .domainID = uint256(42), .err = tecNO_PERMISSION});
|
|
|
|
mptAlice.set({.flags = tfMPTUnlock, .domainID = beast::zero, .err = tecNO_PERMISSION});
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testSetEnabled(FeatureBitset features)
|
|
{
|
|
testcase("Enabled set transaction");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice"); // issuer
|
|
Account const bob("bob"); // holder
|
|
|
|
{
|
|
// Test locking and unlocking
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// create a mptokenissuance with locking
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
|
|
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// locks bob's mptoken
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
|
|
// trying to lock bob's mptoken again will still succeed
|
|
// but no changes to the objects
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
|
|
// alice locks the mptissuance
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
|
|
// alice tries to lock up both mptissuance and mptoken again
|
|
// it will not change the flags and both will remain locked.
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
|
|
// alice unlocks bob's mptoken
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
|
|
|
// locks up bob's mptoken again
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
if (!features[featureSingleAssetVault])
|
|
{
|
|
// Delete bobs' mptoken even though it is locked
|
|
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
|
|
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecOBJECT_NOT_FOUND});
|
|
|
|
return;
|
|
}
|
|
|
|
// Cannot delete locked MPToken
|
|
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize, .err = tecNO_PERMISSION});
|
|
|
|
// alice unlocks mptissuance
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
|
|
|
// alice unlocks bob's mptoken
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
|
|
|
// alice unlocks mptissuance and bob's mptoken again despite that
|
|
// they are already unlocked. Make sure this will not change the
|
|
// flags
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
|
}
|
|
|
|
if (features[featureSingleAssetVault])
|
|
{
|
|
// Add permissioned domain
|
|
std::string const credType = "credential";
|
|
|
|
// Test setting and resetting domain ID
|
|
Env env{*this, features};
|
|
|
|
auto const domainId1 = [&]() {
|
|
Account const credIssuer1{"credIssuer1"};
|
|
env.fund(XRP(1000), credIssuer1);
|
|
|
|
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
env(pdomain::setTx(credIssuer1, credentials1));
|
|
return [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
}();
|
|
|
|
auto const domainId2 = [&]() {
|
|
Account const credIssuer2{"credIssuer2"};
|
|
env.fund(XRP(1000), credIssuer2);
|
|
|
|
pdomain::Credentials const credentials2{{.issuer = credIssuer2, .credType = credType}};
|
|
|
|
env(pdomain::setTx(credIssuer2, credentials2));
|
|
return [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
}();
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// create a mptokenissuance with auth.
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth});
|
|
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
|
|
|
|
// reset "domain not set" to "domain not set", i.e. no change
|
|
mptAlice.set({.domainID = beast::zero});
|
|
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
|
|
|
|
// reset "domain not set" to domain1
|
|
mptAlice.set({.domainID = domainId1});
|
|
BEAST_EXPECT(mptAlice.checkDomainID(domainId1));
|
|
|
|
// reset domain1 to domain2
|
|
mptAlice.set({.domainID = domainId2});
|
|
BEAST_EXPECT(mptAlice.checkDomainID(domainId2));
|
|
|
|
// reset domain to "domain not set"
|
|
mptAlice.set({.domainID = beast::zero});
|
|
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
|
|
}
|
|
}
|
|
|
|
void
|
|
testPayment(FeatureBitset features)
|
|
{
|
|
testcase("Payment");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice"); // issuer
|
|
Account const bob("bob"); // holder
|
|
Account const carol("carol"); // holder
|
|
|
|
// preflight validation
|
|
|
|
// MPT is disabled
|
|
{
|
|
Env env{*this, features - featureMPTokensV1};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
|
|
env.fund(XRP(1'000), alice);
|
|
env.fund(XRP(1'000), bob);
|
|
STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
|
|
|
|
env(pay(alice, bob, mpt), ter(temDISABLED));
|
|
}
|
|
|
|
// MPT is disabled, unsigned request
|
|
{
|
|
Env env{*this, features - featureMPTokensV1};
|
|
Account const alice("alice"); // issuer
|
|
Account const carol("carol");
|
|
auto const USD = alice["USD"];
|
|
|
|
env.fund(XRP(1'000), alice);
|
|
env.fund(XRP(1'000), carol);
|
|
STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
|
|
|
|
Json::Value jv;
|
|
jv[jss::secret] = alice.name();
|
|
jv[jss::tx_json] = pay(alice, carol, mpt);
|
|
jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
|
|
auto const jrr = env.rpc("json", "submit", to_string(jv));
|
|
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED");
|
|
}
|
|
|
|
// Invalid flag
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
auto const MPT = mptAlice["MPT"];
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
for (auto flags : {tfNoRippleDirect, tfLimitQuality})
|
|
env(pay(alice, bob, MPT(10)), txflags(flags), ter(temINVALID_FLAG));
|
|
}
|
|
|
|
// Invalid combination of send, sendMax, deliverMin, paths
|
|
{
|
|
Env env{*this, features};
|
|
Account const alice("alice");
|
|
Account const carol("carol");
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
// sendMax and DeliverMin are valid XRP amount,
|
|
// but is invalid combination with MPT amount
|
|
auto const MPT = mptAlice["MPT"];
|
|
env(pay(alice, carol, MPT(100)), sendmax(XRP(100)), ter(temMALFORMED));
|
|
env(pay(alice, carol, MPT(100)), deliver_min(XRP(100)), ter(temBAD_AMOUNT));
|
|
// sendMax MPT is invalid with IOU or XRP
|
|
auto const USD = alice["USD"];
|
|
env(pay(alice, carol, USD(100)), sendmax(MPT(100)), ter(temMALFORMED));
|
|
env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), ter(temMALFORMED));
|
|
env(pay(alice, carol, USD(100)), deliver_min(MPT(100)), ter(temBAD_AMOUNT));
|
|
env(pay(alice, carol, XRP(100)), deliver_min(MPT(100)), ter(temBAD_AMOUNT));
|
|
// sendmax and amount are different MPT issue
|
|
test::jtx::MPT const MPT1("MPT", makeMptID(env.seq(alice) + 10, alice));
|
|
env(pay(alice, carol, MPT1(100)), sendmax(MPT(100)), ter(temMALFORMED));
|
|
// paths is invalid
|
|
env(pay(alice, carol, MPT(100)), path(~USD), ter(temMALFORMED));
|
|
}
|
|
|
|
// build_path is invalid if MPT
|
|
{
|
|
Env env{*this, features};
|
|
Account const alice("alice");
|
|
Account const carol("carol");
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
auto const MPT = mptAlice["MPT"];
|
|
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
Json::Value payment;
|
|
payment[jss::secret] = alice.name();
|
|
payment[jss::tx_json] = pay(alice, carol, MPT(100));
|
|
|
|
payment[jss::build_path] = true;
|
|
auto jrr = env.rpc("json", "submit", to_string(payment));
|
|
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
|
BEAST_EXPECT(jrr[jss::result][jss::error_message] == "Field 'build_path' not allowed in this context.");
|
|
}
|
|
|
|
// Can't pay negative amount
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
auto const MPT = mptAlice["MPT"];
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
mptAlice.pay(alice, bob, -1, temBAD_AMOUNT);
|
|
|
|
mptAlice.pay(bob, carol, -1, temBAD_AMOUNT);
|
|
|
|
mptAlice.pay(bob, alice, -1, temBAD_AMOUNT);
|
|
|
|
env(pay(alice, bob, MPT(10)), sendmax(MPT(-1)), ter(temBAD_AMOUNT));
|
|
}
|
|
|
|
// Pay to self
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
mptAlice.pay(bob, bob, 10, temREDUNDANT);
|
|
}
|
|
|
|
// preclaim validation
|
|
|
|
// Destination doesn't exist
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
Account const bad{"bad"};
|
|
env.memoize(bad);
|
|
|
|
mptAlice.pay(bob, bad, 10, tecNO_DST);
|
|
}
|
|
|
|
// apply validation
|
|
|
|
// If RequireAuth is enabled, Payment fails if the receiver is not
|
|
// authorized
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
|
|
}
|
|
|
|
// If RequireAuth is enabled, Payment fails if the sender is not
|
|
// authorized
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
|
|
|
|
// bob creates an empty MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice authorizes bob to hold funds
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// alice sends 100 MPT to bob
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// alice UNAUTHORIZES bob
|
|
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
|
|
|
// bob fails to send back to alice because he is no longer
|
|
// authorize to move his funds!
|
|
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
|
}
|
|
|
|
if (features[featureSingleAssetVault] && features[featurePermissionedDomains])
|
|
{
|
|
// If RequireAuth is enabled and domain is a match, payment succeeds
|
|
{
|
|
Env env{*this, features};
|
|
std::string const credType = "credential";
|
|
Account const credIssuer1{"credIssuer1"};
|
|
env.fund(XRP(1000), credIssuer1, bob);
|
|
|
|
auto const domainId1 = [&]() {
|
|
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
env(pdomain::setTx(credIssuer1, credentials1));
|
|
return [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
}();
|
|
// bob is authorized via domain
|
|
env(credentials::create(bob, credIssuer1, credType));
|
|
env(credentials::accept(bob, credIssuer1, credType));
|
|
env.close();
|
|
|
|
MPTTester mptAlice(env, alice);
|
|
env.close();
|
|
|
|
mptAlice.create({
|
|
.ownerCount = 1,
|
|
.holderCount = 0,
|
|
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
|
|
.domainID = domainId1,
|
|
});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
env.close();
|
|
|
|
// bob is authorized via domain
|
|
mptAlice.pay(alice, bob, 100);
|
|
mptAlice.set({.domainID = beast::zero});
|
|
|
|
// bob is no longer authorized
|
|
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
|
|
}
|
|
|
|
{
|
|
Env env{*this, features};
|
|
std::string const credType = "credential";
|
|
Account const credIssuer1{"credIssuer1"};
|
|
env.fund(XRP(1000), credIssuer1, bob);
|
|
|
|
auto const domainId1 = [&]() {
|
|
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
env(pdomain::setTx(credIssuer1, credentials1));
|
|
return [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
}();
|
|
// bob is authorized via domain
|
|
env(credentials::create(bob, credIssuer1, credType));
|
|
env(credentials::accept(bob, credIssuer1, credType));
|
|
env.close();
|
|
|
|
MPTTester mptAlice(env, alice);
|
|
env.close();
|
|
|
|
mptAlice.create({
|
|
.ownerCount = 1,
|
|
.holderCount = 0,
|
|
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
|
|
.domainID = domainId1,
|
|
});
|
|
|
|
// bob creates an empty MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice authorizes bob to hold funds
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// alice sends 100 MPT to bob
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// alice UNAUTHORIZES bob
|
|
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
|
|
|
// bob is still authorized, via domain
|
|
mptAlice.pay(bob, alice, 10);
|
|
|
|
mptAlice.set({.domainID = beast::zero});
|
|
|
|
// bob fails to send back to alice because he is no longer
|
|
// authorize to move his funds!
|
|
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
|
|
}
|
|
|
|
{
|
|
Env env{*this, features};
|
|
std::string const credType = "credential";
|
|
// credIssuer1 is the owner of domainId1 and a credential issuer
|
|
Account const credIssuer1{"credIssuer1"};
|
|
// credIssuer2 is the owner of domainId2 and a credential issuer
|
|
// Note, domainId2 also lists credentials issued by credIssuer1
|
|
Account const credIssuer2{"credIssuer2"};
|
|
env.fund(XRP(1000), credIssuer1, credIssuer2, bob, carol);
|
|
|
|
auto const domainId1 = [&]() {
|
|
pdomain::Credentials const credentials{{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
env(pdomain::setTx(credIssuer1, credentials));
|
|
return [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
}();
|
|
|
|
auto const domainId2 = [&]() {
|
|
pdomain::Credentials const credentials{
|
|
{.issuer = credIssuer1, .credType = credType}, {.issuer = credIssuer2, .credType = credType}};
|
|
|
|
env(pdomain::setTx(credIssuer2, credentials));
|
|
return [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
}();
|
|
|
|
// bob is authorized via credIssuer1 which is recognized by both
|
|
// domainId1 and domainId2
|
|
env(credentials::create(bob, credIssuer1, credType));
|
|
env(credentials::accept(bob, credIssuer1, credType));
|
|
env.close();
|
|
|
|
// carol is authorized via credIssuer2, only recognized by
|
|
// domainId2
|
|
env(credentials::create(carol, credIssuer2, credType));
|
|
env(credentials::accept(carol, credIssuer2, credType));
|
|
env.close();
|
|
|
|
MPTTester mptAlice(env, alice);
|
|
env.close();
|
|
|
|
mptAlice.create({
|
|
.ownerCount = 1,
|
|
.holderCount = 0,
|
|
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
|
|
.domainID = domainId1,
|
|
});
|
|
|
|
// bob and carol create an empty MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
env.close();
|
|
|
|
// alice sends 50 MPT to bob but cannot send to carol
|
|
mptAlice.pay(alice, bob, 50);
|
|
mptAlice.pay(alice, carol, 50, tecNO_AUTH);
|
|
env.close();
|
|
|
|
// bob cannot send to carol because they are not on the same
|
|
// domain (since credIssuer2 is not recognized by domainId1)
|
|
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
|
|
env.close();
|
|
|
|
// alice updates domainID to domainId2 which recognizes both
|
|
// credIssuer1 and credIssuer2
|
|
mptAlice.set({.domainID = domainId2});
|
|
// alice can now send to carol
|
|
mptAlice.pay(alice, carol, 10);
|
|
env.close();
|
|
|
|
// bob can now send to carol because both are in the same
|
|
// domain
|
|
mptAlice.pay(bob, carol, 10);
|
|
env.close();
|
|
|
|
// bob loses his authorization and can no longer send MPT
|
|
env(credentials::deleteCred(credIssuer1, bob, credIssuer1, credType));
|
|
env.close();
|
|
|
|
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
|
|
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
|
|
}
|
|
}
|
|
|
|
// Non-issuer cannot send to each other if MPTCanTransfer isn't set
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const cindy{"cindy"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, cindy}});
|
|
|
|
// alice creates issuance without MPTCanTransfer
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
// bob creates a MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// cindy creates a MPToken
|
|
mptAlice.authorize({.account = cindy});
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// bob tries to send cindy 10 tokens, but fails because canTransfer
|
|
// is off
|
|
mptAlice.pay(bob, cindy, 10, tecNO_AUTH);
|
|
|
|
// bob can send back to alice(issuer) just fine
|
|
mptAlice.pay(bob, alice, 10);
|
|
}
|
|
|
|
// Holder is not authorized
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
// issuer to holder
|
|
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
|
|
|
|
// holder to issuer
|
|
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
|
|
|
// holder to holder
|
|
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
|
|
}
|
|
|
|
// Payer doesn't have enough funds
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// Pay to another holder
|
|
mptAlice.pay(bob, carol, 101, tecPATH_PARTIAL);
|
|
|
|
// Pay to the issuer
|
|
mptAlice.pay(bob, alice, 101, tecPATH_PARTIAL);
|
|
}
|
|
|
|
// MPT is locked
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
mptAlice.pay(alice, bob, 100);
|
|
mptAlice.pay(alice, carol, 100);
|
|
|
|
// Global lock
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
// Can't send between holders
|
|
mptAlice.pay(bob, carol, 1, tecLOCKED);
|
|
mptAlice.pay(carol, bob, 2, tecLOCKED);
|
|
// Issuer can send
|
|
mptAlice.pay(alice, bob, 3);
|
|
// Holder can send back to issuer
|
|
mptAlice.pay(bob, alice, 4);
|
|
|
|
// Global unlock
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
|
// Individual lock
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
// Can't send between holders
|
|
mptAlice.pay(bob, carol, 5, tecLOCKED);
|
|
mptAlice.pay(carol, bob, 6, tecLOCKED);
|
|
// Issuer can send
|
|
mptAlice.pay(alice, bob, 7);
|
|
// Holder can send back to issuer
|
|
mptAlice.pay(bob, alice, 8);
|
|
}
|
|
|
|
// Transfer fee
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
// Transfer fee is 10%
|
|
mptAlice.create({.transferFee = 10'000, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
// Holders create MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
// Payment between the issuer and the holder, no transfer fee.
|
|
mptAlice.pay(alice, bob, 2'000);
|
|
|
|
// Payment between the holder and the issuer, no transfer fee.
|
|
mptAlice.pay(bob, alice, 1'000);
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 1'000));
|
|
|
|
// Payment between the holders. The sender doesn't have
|
|
// enough funds to cover the transfer fee.
|
|
mptAlice.pay(bob, carol, 1'000, tecPATH_PARTIAL);
|
|
|
|
// Payment between the holders. The sender has enough funds
|
|
// but SendMax is not included.
|
|
mptAlice.pay(bob, carol, 100, tecPATH_PARTIAL);
|
|
|
|
auto const MPT = mptAlice["MPT"];
|
|
// SendMax doesn't cover the fee
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(109)), ter(tecPATH_PARTIAL));
|
|
|
|
// Payment succeeds if sufficient SendMax is included.
|
|
// 100 to carol, 10 to issuer
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(110)));
|
|
// 100 to carol, 10 to issuer
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(115)));
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 780));
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 200));
|
|
// Payment succeeds if partial payment even if
|
|
// SendMax is less than deliver amount
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(90)), txflags(tfPartialPayment));
|
|
// 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) =
|
|
// 82)
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 282));
|
|
}
|
|
|
|
// Insufficient SendMax with no transfer fee
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
// Holders create MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
mptAlice.pay(alice, bob, 1'000);
|
|
|
|
auto const MPT = mptAlice["MPT"];
|
|
// SendMax is less than the amount
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL));
|
|
env(pay(bob, alice, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL));
|
|
|
|
// Payment succeeds if sufficient SendMax is included.
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(100)));
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 100));
|
|
// Payment succeeds if partial payment
|
|
env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), txflags(tfPartialPayment));
|
|
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 199));
|
|
}
|
|
|
|
// DeliverMin
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
// Holders create MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
mptAlice.pay(alice, bob, 1'000);
|
|
|
|
auto const MPT = mptAlice["MPT"];
|
|
// Fails even with the partial payment because
|
|
// deliver amount < deliverMin
|
|
env(pay(bob, alice, MPT(100)),
|
|
sendmax(MPT(99)),
|
|
deliver_min(MPT(100)),
|
|
txflags(tfPartialPayment),
|
|
ter(tecPATH_PARTIAL));
|
|
// Payment succeeds if deliver amount >= deliverMin
|
|
env(pay(bob, alice, MPT(100)), sendmax(MPT(99)), deliver_min(MPT(99)), txflags(tfPartialPayment));
|
|
}
|
|
|
|
// Issuer fails trying to send more than the maximum amount allowed
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.maxAmt = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// issuer sends holder the max amount allowed
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// issuer tries to exceed max amount
|
|
mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL);
|
|
}
|
|
|
|
// Issuer fails trying to send more than the default maximum
|
|
// amount allowed
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// issuer sends holder the default max amount allowed
|
|
mptAlice.pay(alice, bob, maxMPTokenAmount);
|
|
|
|
// issuer tries to exceed max amount
|
|
mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL);
|
|
}
|
|
|
|
// Pay more than max amount fails in the json parser before
|
|
// transactor is called
|
|
{
|
|
Env env{*this, features};
|
|
env.fund(XRP(1'000), alice, bob);
|
|
STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
|
|
Json::Value jv;
|
|
jv[jss::secret] = alice.name();
|
|
jv[jss::tx_json] = pay(alice, bob, mpt);
|
|
jv[jss::tx_json][jss::Amount][jss::value] = std::to_string(maxMPTokenAmount + 1);
|
|
auto const jrr = env.rpc("json", "submit", to_string(jv));
|
|
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
|
}
|
|
|
|
// Pay maximum amount with the transfer fee, SendMax, and
|
|
// partial payment
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create(
|
|
{.maxAmt = 10'000, .transferFee = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
auto const MPT = mptAlice["MPT"];
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
// issuer sends holder the max amount allowed
|
|
mptAlice.pay(alice, bob, 10'000);
|
|
|
|
// payment between the holders
|
|
env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), txflags(tfPartialPayment));
|
|
// Verify the metadata
|
|
auto const meta = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName];
|
|
// Issuer got 10 in the transfer fees
|
|
BEAST_EXPECT(
|
|
meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName][sfOutstandingAmount.fieldName] == "9990");
|
|
// Destination account got 9'990
|
|
BEAST_EXPECT(meta[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName][sfMPTAmount.fieldName] == "9990");
|
|
// Source account spent 10'000
|
|
BEAST_EXPECT(
|
|
meta[2u][sfModifiedNode.fieldName][sfPreviousFields.fieldName][sfMPTAmount.fieldName] == "10000");
|
|
BEAST_EXPECT(!meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName].isMember(sfMPTAmount.fieldName));
|
|
|
|
// payment between the holders fails without
|
|
// partial payment
|
|
env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), ter(tecPATH_PARTIAL));
|
|
}
|
|
|
|
// Pay maximum allowed amount
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.maxAmt = maxMPTokenAmount, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
auto const MPT = mptAlice["MPT"];
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
// issuer sends holder the max amount allowed
|
|
mptAlice.pay(alice, bob, maxMPTokenAmount);
|
|
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount));
|
|
|
|
// payment between the holders
|
|
mptAlice.pay(bob, carol, maxMPTokenAmount);
|
|
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount));
|
|
// holder pays back to the issuer
|
|
mptAlice.pay(carol, alice, maxMPTokenAmount);
|
|
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(0));
|
|
}
|
|
|
|
// Issuer fails trying to send fund after issuance was destroyed
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice destroys issuance
|
|
mptAlice.destroy({.ownerCount = 0});
|
|
|
|
// alice tries to send bob fund after issuance is destroyed, should
|
|
// fail.
|
|
mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
|
|
}
|
|
|
|
// Non-existent issuance
|
|
{
|
|
Env env{*this, features};
|
|
|
|
env.fund(XRP(1'000), alice, bob);
|
|
|
|
STAmount const mpt{MPTID{0}, 100};
|
|
env(pay(alice, bob, mpt), ter(tecOBJECT_NOT_FOUND));
|
|
}
|
|
|
|
// Issuer fails trying to send to an account, which doesn't own MPT for
|
|
// an issuance that was destroyed
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
// alice destroys issuance
|
|
mptAlice.destroy({.ownerCount = 0});
|
|
|
|
// alice tries to send bob who doesn't own the MPT after issuance is
|
|
// destroyed, it should fail
|
|
mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
|
|
}
|
|
|
|
// Issuers issues maximum amount of MPT to a holder, the holder should
|
|
// be able to transfer the max amount to someone else
|
|
{
|
|
Env env{*this, features};
|
|
Account const alice("alice");
|
|
Account const carol("bob");
|
|
Account const bob("carol");
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// transfer max amount to another holder
|
|
mptAlice.pay(bob, carol, 100);
|
|
}
|
|
|
|
// Simple payment
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
// issuer to holder
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// holder to issuer
|
|
mptAlice.pay(bob, alice, 100);
|
|
|
|
// holder to holder
|
|
mptAlice.pay(alice, bob, 100);
|
|
mptAlice.pay(bob, carol, 50);
|
|
}
|
|
}
|
|
|
|
void
|
|
testDepositPreauth(FeatureBitset features)
|
|
{
|
|
using namespace test::jtx;
|
|
Account const alice("alice"); // issuer
|
|
Account const bob("bob"); // holder
|
|
Account const diana("diana");
|
|
Account const dpIssuer("dpIssuer"); // holder
|
|
|
|
char const credType[] = "abcde";
|
|
|
|
if (features[featureCredentials])
|
|
{
|
|
testcase("DepositPreauth");
|
|
|
|
Env env(*this, features);
|
|
|
|
env.fund(XRP(50000), diana, dpIssuer);
|
|
env.close();
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
|
|
|
|
env(pay(diana, bob, XRP(500)));
|
|
env.close();
|
|
|
|
// bob creates an empty MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
// alice authorizes bob to hold funds
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
|
|
// alice try to send 100 MPT to bob, not authorized
|
|
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
|
|
env.close();
|
|
|
|
// Bob authorize alice
|
|
env(deposit::auth(bob, alice));
|
|
env.close();
|
|
|
|
// alice sends 100 MPT to bob
|
|
mptAlice.pay(alice, bob, 100);
|
|
env.close();
|
|
|
|
// Create credentials
|
|
env(credentials::create(alice, dpIssuer, credType));
|
|
env.close();
|
|
env(credentials::accept(alice, dpIssuer, credType));
|
|
env.close();
|
|
auto const jv = credentials::ledgerEntry(env, alice, dpIssuer, credType);
|
|
std::string const credIdx = jv[jss::result][jss::index].asString();
|
|
|
|
// alice sends 100 MPT to bob with credentials which aren't required
|
|
mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
|
|
env.close();
|
|
|
|
// Bob revoke authorization
|
|
env(deposit::unauth(bob, alice));
|
|
env.close();
|
|
|
|
// alice try to send 100 MPT to bob, not authorized
|
|
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
|
|
env.close();
|
|
|
|
// alice sends 100 MPT to bob with credentials, not authorized
|
|
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}});
|
|
env.close();
|
|
|
|
// Bob authorize credentials
|
|
env(deposit::authCredentials(bob, {{dpIssuer, credType}}));
|
|
env.close();
|
|
|
|
// alice try to send 100 MPT to bob, not authorized
|
|
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
|
|
env.close();
|
|
|
|
// alice sends 100 MPT to bob with credentials
|
|
mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
|
|
env.close();
|
|
}
|
|
|
|
testcase("DepositPreauth disabled featureCredentials");
|
|
{
|
|
Env env(*this, testable_amendments() - featureCredentials);
|
|
|
|
std::string const credIdx =
|
|
"D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6"
|
|
"E2";
|
|
|
|
env.fund(XRP(50000), diana, dpIssuer);
|
|
env.close();
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
|
|
|
|
env(pay(diana, bob, XRP(500)));
|
|
env.close();
|
|
|
|
// bob creates an empty MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
// alice authorizes bob to hold funds
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
|
|
// alice try to send 100 MPT to bob, not authorized
|
|
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
|
|
env.close();
|
|
|
|
// alice try to send 100 MPT to bob with credentials, amendment
|
|
// disabled
|
|
mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
|
|
env.close();
|
|
|
|
// Bob authorize alice
|
|
env(deposit::auth(bob, alice));
|
|
env.close();
|
|
|
|
// alice sends 100 MPT to bob
|
|
mptAlice.pay(alice, bob, 100);
|
|
env.close();
|
|
|
|
// alice sends 100 MPT to bob with credentials, amendment disabled
|
|
mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
|
|
env.close();
|
|
|
|
// Bob revoke authorization
|
|
env(deposit::unauth(bob, alice));
|
|
env.close();
|
|
|
|
// alice try to send 100 MPT to bob
|
|
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
|
|
env.close();
|
|
|
|
// alice sends 100 MPT to bob with credentials, amendment disabled
|
|
mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testMPTInvalidInTx(FeatureBitset features)
|
|
{
|
|
testcase("MPT Issue Invalid in Transaction");
|
|
using namespace test::jtx;
|
|
|
|
// Validate that every transaction with an amount/issue field,
|
|
// which doesn't support MPT, fails.
|
|
|
|
// keyed by transaction + amount/issue field
|
|
std::set<std::string> txWithAmounts;
|
|
for (auto const& format : TxFormats::getInstance())
|
|
{
|
|
for (auto const& e : format.getSOTemplate())
|
|
{
|
|
// Transaction has amount/issue fields.
|
|
// Exclude pseudo-transaction SetFee. Don't consider
|
|
// the Fee field since it's included in every transaction.
|
|
if (e.supportMPT() == soeMPTNotSupported && e.sField().getName() != jss::Fee &&
|
|
format.getName() != jss::SetFee)
|
|
{
|
|
txWithAmounts.insert(format.getName() + e.sField().fieldName);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Account const alice("alice");
|
|
auto const USD = alice["USD"];
|
|
Account const carol("carol");
|
|
MPTIssue issue(makeMptID(1, alice));
|
|
STAmount mpt{issue, UINT64_C(100)};
|
|
auto const jvb = bridge(alice, USD, alice, USD);
|
|
for (auto const& feature : {features, features - featureMPTokensV1})
|
|
{
|
|
Env env{*this, feature};
|
|
env.fund(XRP(1'000), alice);
|
|
env.fund(XRP(1'000), carol);
|
|
auto test = [&](Json::Value const& jv, std::string const& mptField) {
|
|
txWithAmounts.erase(jv[jss::TransactionType].asString() + mptField);
|
|
|
|
// tx is signed
|
|
auto jtx = env.jt(jv);
|
|
Serializer s;
|
|
jtx.stx->add(s);
|
|
auto jrr = env.rpc("submit", strHex(s.slice()));
|
|
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidTransaction");
|
|
|
|
// tx is unsigned
|
|
Json::Value jv1;
|
|
jv1[jss::secret] = alice.name();
|
|
jv1[jss::tx_json] = jv;
|
|
jrr = env.rpc("json", "submit", to_string(jv1));
|
|
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
|
|
|
jrr = env.rpc("json", "sign", to_string(jv1));
|
|
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
|
};
|
|
auto toSFieldRef = [](SField const& field) { return std::ref(field); };
|
|
auto setMPTFields = [&](SField const& field, Json::Value& jv, bool withAmount = true) {
|
|
jv[jss::Asset] = to_json(xrpIssue());
|
|
jv[jss::Asset2] = to_json(USD.issue());
|
|
if (withAmount)
|
|
jv[field.fieldName] = USD(10).value().getJson(JsonOptions::none);
|
|
if (field == sfAsset)
|
|
jv[jss::Asset] = to_json(mpt.get<MPTIssue>());
|
|
else if (field == sfAsset2)
|
|
jv[jss::Asset2] = to_json(mpt.get<MPTIssue>());
|
|
else
|
|
jv[field.fieldName] = mpt.getJson(JsonOptions::none);
|
|
};
|
|
// All transactions with sfAmount, which don't support MPT.
|
|
// Transactions with amount fields, which can't be MPT.
|
|
// Transactions with issue fields, which can't be MPT.
|
|
|
|
// AMMCreate
|
|
auto ammCreate = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMCreate;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::Amount] =
|
|
(field.fieldName == sfAmount.fieldName) ? mpt.getJson(JsonOptions::none) : "100000000";
|
|
jv[jss::Amount2] =
|
|
(field.fieldName == sfAmount2.fieldName) ? mpt.getJson(JsonOptions::none) : "100000000";
|
|
jv[jss::TradingFee] = 0;
|
|
test(jv, field.fieldName);
|
|
};
|
|
ammCreate(sfAmount);
|
|
ammCreate(sfAmount2);
|
|
// AMMDeposit
|
|
auto ammDeposit = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMDeposit;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::Flags] = tfSingleAsset;
|
|
setMPTFields(field, jv);
|
|
test(jv, field.fieldName);
|
|
};
|
|
for (SField const& field :
|
|
{toSFieldRef(sfAmount),
|
|
toSFieldRef(sfAmount2),
|
|
toSFieldRef(sfEPrice),
|
|
toSFieldRef(sfLPTokenOut),
|
|
toSFieldRef(sfAsset),
|
|
toSFieldRef(sfAsset2)})
|
|
ammDeposit(field);
|
|
// AMMWithdraw
|
|
auto ammWithdraw = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMWithdraw;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::Flags] = tfSingleAsset;
|
|
setMPTFields(field, jv);
|
|
test(jv, field.fieldName);
|
|
};
|
|
ammWithdraw(sfAmount);
|
|
for (SField const& field :
|
|
{toSFieldRef(sfAmount2),
|
|
toSFieldRef(sfEPrice),
|
|
toSFieldRef(sfLPTokenIn),
|
|
toSFieldRef(sfAsset),
|
|
toSFieldRef(sfAsset2)})
|
|
ammWithdraw(field);
|
|
// AMMBid
|
|
auto ammBid = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMBid;
|
|
jv[jss::Account] = alice.human();
|
|
setMPTFields(field, jv);
|
|
test(jv, field.fieldName);
|
|
};
|
|
for (SField const& field :
|
|
{toSFieldRef(sfBidMin), toSFieldRef(sfBidMax), toSFieldRef(sfAsset), toSFieldRef(sfAsset2)})
|
|
ammBid(field);
|
|
// AMMClawback
|
|
auto ammClawback = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMClawback;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::Holder] = carol.human();
|
|
setMPTFields(field, jv);
|
|
test(jv, field.fieldName);
|
|
};
|
|
for (SField const& field : {toSFieldRef(sfAmount), toSFieldRef(sfAsset), toSFieldRef(sfAsset2)})
|
|
ammClawback(field);
|
|
// AMMDelete
|
|
auto ammDelete = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMDelete;
|
|
jv[jss::Account] = alice.human();
|
|
setMPTFields(field, jv, false);
|
|
test(jv, field.fieldName);
|
|
};
|
|
ammDelete(sfAsset);
|
|
ammDelete(sfAsset2);
|
|
// AMMVote
|
|
auto ammVote = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::AMMVote;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::TradingFee] = 100;
|
|
setMPTFields(field, jv, false);
|
|
test(jv, field.fieldName);
|
|
};
|
|
ammVote(sfAsset);
|
|
ammVote(sfAsset2);
|
|
// CheckCash
|
|
auto checkCash = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::CheckCash;
|
|
jv[jss::Account] = alice.human();
|
|
jv[sfCheckID.fieldName] = to_string(uint256{1});
|
|
jv[field.fieldName] = mpt.getJson(JsonOptions::none);
|
|
test(jv, field.fieldName);
|
|
};
|
|
checkCash(sfAmount);
|
|
checkCash(sfDeliverMin);
|
|
// CheckCreate
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::CheckCreate;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::Destination] = carol.human();
|
|
jv[jss::SendMax] = mpt.getJson(JsonOptions::none);
|
|
test(jv, jss::SendMax.c_str());
|
|
}
|
|
// OfferCreate
|
|
{
|
|
Json::Value jv = offer(alice, USD(100), mpt);
|
|
test(jv, jss::TakerPays.c_str());
|
|
jv = offer(alice, mpt, USD(100));
|
|
test(jv, jss::TakerGets.c_str());
|
|
}
|
|
// 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::none);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// 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::none);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// 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::none);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// NFTokenCreateOffer
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::NFTokenCreateOffer;
|
|
jv[jss::Account] = alice.human();
|
|
jv[sfNFTokenID.fieldName] = to_string(uint256{1});
|
|
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// NFTokenAcceptOffer
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::NFTokenAcceptOffer;
|
|
jv[jss::Account] = alice.human();
|
|
jv[sfNFTokenBrokerFee.fieldName] = mpt.getJson(JsonOptions::none);
|
|
test(jv, sfNFTokenBrokerFee.fieldName);
|
|
}
|
|
// NFTokenMint
|
|
{
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::NFTokenMint;
|
|
jv[jss::Account] = alice.human();
|
|
jv[sfNFTokenTaxon.fieldName] = 1;
|
|
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// TrustSet
|
|
auto trustSet = [&](SField const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = jss::TrustSet;
|
|
jv[jss::Account] = alice.human();
|
|
jv[jss::Flags] = 0;
|
|
jv[field.fieldName] = mpt.getJson(JsonOptions::none);
|
|
test(jv, field.fieldName);
|
|
};
|
|
trustSet(sfLimitAmount);
|
|
trustSet(sfFee);
|
|
// XChainCommit
|
|
{
|
|
Json::Value const jv = xchain_commit(alice, jvb, 1, mpt);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// XChainClaim
|
|
{
|
|
Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice);
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// XChainCreateClaimID
|
|
{
|
|
Json::Value const jv = xchain_create_claim_id(alice, jvb, mpt, alice);
|
|
test(jv, sfSignatureReward.fieldName);
|
|
}
|
|
// XChainAddClaimAttestation
|
|
{
|
|
Json::Value const jv = claim_attestation(alice, jvb, alice, mpt, alice, true, 1, alice, signer(alice));
|
|
test(jv, jss::Amount.c_str());
|
|
}
|
|
// XChainAddAccountCreateAttestation
|
|
{
|
|
Json::Value jv =
|
|
create_account_attestation(alice, jvb, alice, mpt, XRP(10), alice, false, 1, alice, signer(alice));
|
|
for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName})
|
|
{
|
|
jv[field] = mpt.getJson(JsonOptions::none);
|
|
test(jv, field);
|
|
}
|
|
}
|
|
// XChainAccountCreateCommit
|
|
{
|
|
Json::Value jv = sidechain_xchain_account_create(alice, jvb, alice, mpt, XRP(10));
|
|
for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName})
|
|
{
|
|
jv[field] = mpt.getJson(JsonOptions::none);
|
|
test(jv, field);
|
|
}
|
|
}
|
|
// XChain[Create|Modify]Bridge
|
|
auto bridgeTx = [&](Json::StaticString const& tt,
|
|
STAmount const& rewardAmount,
|
|
STAmount const& minAccountAmount,
|
|
std::string const& field) {
|
|
Json::Value jv;
|
|
jv[jss::TransactionType] = tt;
|
|
jv[jss::Account] = alice.human();
|
|
jv[sfXChainBridge.fieldName] = jvb;
|
|
jv[sfSignatureReward.fieldName] = rewardAmount.getJson(JsonOptions::none);
|
|
jv[sfMinAccountCreateAmount.fieldName] = minAccountAmount.getJson(JsonOptions::none);
|
|
test(jv, field);
|
|
};
|
|
auto reward = STAmount{sfSignatureReward, mpt};
|
|
auto minAmount = STAmount{sfMinAccountCreateAmount, USD(10)};
|
|
for (SField const& field : {std::ref(sfSignatureReward), std::ref(sfMinAccountCreateAmount)})
|
|
{
|
|
bridgeTx(jss::XChainCreateBridge, reward, minAmount, field.fieldName);
|
|
bridgeTx(jss::XChainModifyBridge, reward, minAmount, field.fieldName);
|
|
reward = STAmount{sfSignatureReward, USD(10)};
|
|
minAmount = STAmount{sfMinAccountCreateAmount, mpt};
|
|
}
|
|
}
|
|
BEAST_EXPECT(txWithAmounts.empty());
|
|
}
|
|
|
|
void
|
|
testTxJsonMetaFields(FeatureBitset features)
|
|
{
|
|
// checks synthetically injected mptissuanceid from `tx` response
|
|
testcase("Test synthetic fields from tx response");
|
|
|
|
using namespace test::jtx;
|
|
|
|
Account const alice{"alice"};
|
|
|
|
auto cfg = envconfig();
|
|
cfg->FEES.reference_fee = 10;
|
|
Env env{*this, std::move(cfg), features};
|
|
MPTTester mptAlice(env, alice);
|
|
|
|
mptAlice.create();
|
|
|
|
std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
|
BEAST_EXPECTS(
|
|
txHash ==
|
|
"E11F0E0CA14219922B7881F060B9CEE67CFBC87E4049A441ED2AE348FF8FAC"
|
|
"0E",
|
|
txHash);
|
|
Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
|
|
auto const id = meta[jss::mpt_issuance_id].asString();
|
|
// Expect mpt_issuance_id field
|
|
BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id));
|
|
BEAST_EXPECT(id == to_string(mptAlice.issuanceID()));
|
|
BEAST_EXPECTS(id == "00000004AE123A8556F3CF91154711376AFB0F894F832B3D", id);
|
|
}
|
|
|
|
void
|
|
testClawbackValidation(FeatureBitset features)
|
|
{
|
|
testcase("MPT clawback validations");
|
|
using namespace test::jtx;
|
|
|
|
// Make sure clawback cannot work when featureMPTokensV1 is disabled
|
|
{
|
|
Env env(*this, features - featureMPTokensV1);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
env.fund(XRP(1000), alice, bob);
|
|
env.close();
|
|
|
|
auto const USD = alice["USD"];
|
|
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
|
|
|
|
env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED));
|
|
env.close();
|
|
|
|
env(claw(alice, mpt(5)), ter(temDISABLED));
|
|
env.close();
|
|
|
|
env(claw(alice, mpt(5), bob), ter(temDISABLED));
|
|
env.close();
|
|
}
|
|
|
|
// Test preflight
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
env.fund(XRP(1000), alice, bob);
|
|
env.close();
|
|
|
|
auto const USD = alice["USD"];
|
|
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
|
|
|
|
// clawing back IOU from a MPT holder fails
|
|
env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED));
|
|
env.close();
|
|
|
|
// clawing back MPT without specifying a holder fails
|
|
env(claw(alice, mpt(5)), ter(temMALFORMED));
|
|
env.close();
|
|
|
|
// clawing back zero amount fails
|
|
env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT));
|
|
env.close();
|
|
|
|
// alice can't claw back from herself
|
|
env(claw(alice, mpt(5), alice), ter(temMALFORMED));
|
|
env.close();
|
|
|
|
// can't clawback negative amount
|
|
env(claw(alice, mpt(-1), bob), ter(temBAD_AMOUNT));
|
|
env.close();
|
|
}
|
|
|
|
// Preclaim - clawback fails when MPTCanClawback is disabled on issuance
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// enable asfAllowTrustLineClawback for alice
|
|
env(fset(alice, asfAllowTrustLineClawback));
|
|
env.close();
|
|
env.require(flags(alice, asfAllowTrustLineClawback));
|
|
|
|
// Create issuance without enabling clawback
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// alice cannot clawback before she didn't enable MPTCanClawback
|
|
// asfAllowTrustLineClawback has no effect
|
|
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
|
|
}
|
|
|
|
// Preclaim - test various scenarios
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(1000), carol);
|
|
env.close();
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
auto const fakeMpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
|
|
|
|
// issuer tries to clawback MPT where issuance doesn't exist
|
|
env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND));
|
|
env.close();
|
|
|
|
// alice creates issuance
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});
|
|
|
|
// alice tries to clawback from someone who doesn't have MPToken
|
|
mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND);
|
|
|
|
// bob creates a MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// clawback fails because bob currently has a balance of zero
|
|
mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// carol fails tries to clawback from bob because he is not the
|
|
// issuer
|
|
mptAlice.claw(carol, bob, 1, tecNO_PERMISSION);
|
|
}
|
|
|
|
// clawback more than max amount
|
|
// fails in the json parser before
|
|
// transactor is called
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
env.fund(XRP(1000), alice, bob);
|
|
env.close();
|
|
|
|
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
|
|
|
|
Json::Value jv = claw(alice, mpt(1), bob);
|
|
jv[jss::Amount][jss::value] = std::to_string(maxMPTokenAmount + 1);
|
|
Json::Value jv1;
|
|
jv1[jss::secret] = alice.name();
|
|
jv1[jss::tx_json] = jv;
|
|
auto const jrr = env.rpc("json", "submit", to_string(jv1));
|
|
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
|
|
}
|
|
}
|
|
|
|
void
|
|
testClawback(FeatureBitset features)
|
|
{
|
|
testcase("MPT Clawback");
|
|
using namespace test::jtx;
|
|
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// alice creates issuance
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});
|
|
|
|
// bob creates a MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
mptAlice.claw(alice, bob, 1);
|
|
|
|
mptAlice.claw(alice, bob, 1000);
|
|
|
|
// clawback fails because bob currently has a balance of zero
|
|
mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
|
|
}
|
|
|
|
// Test that globally locked funds can be clawed
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// alice creates issuance
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback});
|
|
|
|
// bob creates a MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
|
|
mptAlice.claw(alice, bob, 100);
|
|
}
|
|
|
|
// Test that individually locked funds can be clawed
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// alice creates issuance
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback});
|
|
|
|
// bob creates a MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
|
|
mptAlice.claw(alice, bob, 100);
|
|
}
|
|
|
|
// Test that unauthorized funds can be clawed back
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
// alice creates issuance
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback | tfMPTRequireAuth});
|
|
|
|
// bob creates a MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// alice authorizes bob
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// alice unauthorizes bob
|
|
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
|
|
|
mptAlice.claw(alice, bob, 100);
|
|
}
|
|
}
|
|
|
|
void
|
|
testTokensEquality()
|
|
{
|
|
using namespace test::jtx;
|
|
testcase("Tokens Equality");
|
|
Currency const cur1{to_currency("CU1")};
|
|
Currency const cur2{to_currency("CU2")};
|
|
Account const gw1{"gw1"};
|
|
Account const gw2{"gw2"};
|
|
MPTID const mpt1 = makeMptID(1, gw1);
|
|
MPTID const mpt1a = makeMptID(1, gw1);
|
|
MPTID const mpt2 = makeMptID(1, gw2);
|
|
MPTID const mpt3 = makeMptID(2, gw2);
|
|
Asset const assetCur1Gw1{Issue{cur1, gw1}};
|
|
Asset const assetCur1Gw1a{Issue{cur1, gw1}};
|
|
Asset const assetCur2Gw1{Issue{cur2, gw1}};
|
|
Asset const assetCur2Gw2{Issue{cur2, gw2}};
|
|
Asset const assetMpt1Gw1{mpt1};
|
|
Asset const assetMpt1Gw1a{mpt1a};
|
|
Asset const assetMpt1Gw2{mpt2};
|
|
Asset const assetMpt2Gw2{mpt3};
|
|
|
|
// Assets holding Issue
|
|
// Currencies are equal regardless of the issuer
|
|
BEAST_EXPECT(equalTokens(assetCur1Gw1, assetCur1Gw1a));
|
|
BEAST_EXPECT(equalTokens(assetCur2Gw1, assetCur2Gw2));
|
|
// Currencies are different regardless of whether the issuers
|
|
// are the same or not
|
|
BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw1));
|
|
BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw2));
|
|
|
|
// Assets holding MPTIssue
|
|
// MPTIDs are the same if the sequence and the issuer are the same
|
|
BEAST_EXPECT(equalTokens(assetMpt1Gw1, assetMpt1Gw1a));
|
|
// MPTIDs are different if sequence and the issuer don't match
|
|
BEAST_EXPECT(!equalTokens(assetMpt1Gw1, assetMpt1Gw2));
|
|
BEAST_EXPECT(!equalTokens(assetMpt1Gw2, assetMpt2Gw2));
|
|
|
|
// Assets holding Issue and MPTIssue
|
|
BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetMpt1Gw1));
|
|
BEAST_EXPECT(!equalTokens(assetMpt2Gw2, assetCur2Gw2));
|
|
}
|
|
|
|
void
|
|
testHelperFunctions()
|
|
{
|
|
using namespace test::jtx;
|
|
Account const gw{"gw"};
|
|
Asset const asset1{makeMptID(1, gw)};
|
|
Asset const asset2{makeMptID(2, gw)};
|
|
Asset const asset3{makeMptID(3, gw)};
|
|
STAmount const amt1{asset1, 100};
|
|
STAmount const amt2{asset2, 100};
|
|
STAmount const amt3{asset3, 10'000};
|
|
|
|
{
|
|
testcase("Test STAmount MPT arithmetic");
|
|
using namespace std::string_literals;
|
|
STAmount res = multiply(amt1, amt2, asset3);
|
|
BEAST_EXPECT(res == amt3);
|
|
|
|
res = mulRound(amt1, amt2, asset3, true);
|
|
BEAST_EXPECT(res == amt3);
|
|
|
|
res = mulRoundStrict(amt1, amt2, asset3, true);
|
|
BEAST_EXPECT(res == amt3);
|
|
|
|
// overflow, any value > 3037000499ull
|
|
STAmount mptOverflow{asset2, UINT64_C(3037000500)};
|
|
try
|
|
{
|
|
res = multiply(mptOverflow, mptOverflow, asset3);
|
|
fail("should throw runtime exception 1");
|
|
}
|
|
catch (std::runtime_error const& e)
|
|
{
|
|
BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
|
|
}
|
|
// overflow, (v1 >> 32) * v2 > 2147483648ull
|
|
mptOverflow = STAmount{asset2, UINT64_C(2147483648)};
|
|
uint64_t const mantissa = (2ull << 32) + 2;
|
|
try
|
|
{
|
|
res = multiply(STAmount{asset1, mantissa}, mptOverflow, asset3);
|
|
fail("should throw runtime exception 2");
|
|
}
|
|
catch (std::runtime_error const& e)
|
|
{
|
|
BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
|
|
}
|
|
}
|
|
|
|
{
|
|
testcase("Test MPTAmount arithmetic");
|
|
MPTAmount mptAmt1{100};
|
|
MPTAmount const mptAmt2{100};
|
|
BEAST_EXPECT((mptAmt1 += mptAmt2) == MPTAmount{200});
|
|
BEAST_EXPECT(mptAmt1 == 200);
|
|
BEAST_EXPECT((mptAmt1 -= mptAmt2) == mptAmt1);
|
|
BEAST_EXPECT(mptAmt1 == mptAmt2);
|
|
BEAST_EXPECT(mptAmt1 == 100);
|
|
BEAST_EXPECT(MPTAmount::minPositiveAmount() == MPTAmount{1});
|
|
}
|
|
|
|
{
|
|
testcase("Test MPTIssue from/to Json");
|
|
MPTIssue const issue1{asset1.get<MPTIssue>()};
|
|
Json::Value const jv = to_json(issue1);
|
|
BEAST_EXPECT(jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
|
|
BEAST_EXPECT(issue1 == mptIssueFromJson(jv));
|
|
}
|
|
|
|
{
|
|
testcase("Test Asset from/to Json");
|
|
Json::Value const jv = to_json(asset1);
|
|
BEAST_EXPECT(jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
|
|
BEAST_EXPECT(
|
|
to_string(jv) ==
|
|
"{\"mpt_issuance_id\":"
|
|
"\"00000001A407AF5856CCF3C42619DAA925813FC955C72983\"}");
|
|
BEAST_EXPECT(asset1 == assetFromJson(jv));
|
|
}
|
|
}
|
|
|
|
void
|
|
testInvalidCreateDynamic(FeatureBitset features)
|
|
{
|
|
testcase("invalid MPTokenIssuanceCreate for DynamicMPT");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
|
|
// Can not provide MutableFlags when DynamicMPT amendment is not enabled
|
|
{
|
|
Env env{*this, features - featureDynamicMPT};
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create({.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED});
|
|
mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED});
|
|
}
|
|
|
|
// MutableFlags contains invalid values
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
|
|
// Value 1 is reserved for MPT lock.
|
|
mptAlice.create({.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG});
|
|
mptAlice.create({.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG});
|
|
mptAlice.create({.ownerCount = 0, .mutableFlags = 65535, .err = temINVALID_FLAG});
|
|
|
|
// MutableFlags can not be 0
|
|
mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG});
|
|
}
|
|
}
|
|
|
|
void
|
|
testInvalidSetDynamic(FeatureBitset features)
|
|
{
|
|
testcase("invalid MPTokenIssuanceSet for DynamicMPT");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
|
|
// Can not provide MutableFlags, MPTokenMetadata or TransferFee when
|
|
// DynamicMPT amendment is not enabled
|
|
{
|
|
Env env{*this, features - featureDynamicMPT};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
auto const mptID = makeMptID(env.seq(alice), alice);
|
|
|
|
// MutableFlags is not allowed when DynamicMPT is not enabled
|
|
mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 2, .err = temDISABLED});
|
|
mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 0, .err = temDISABLED});
|
|
|
|
// MPTokenMetadata is not allowed when DynamicMPT is not enabled
|
|
mptAlice.set({.account = alice, .id = mptID, .metadata = "test", .err = temDISABLED});
|
|
mptAlice.set({.account = alice, .id = mptID, .metadata = "", .err = temDISABLED});
|
|
|
|
// TransferFee is not allowed when DynamicMPT is not enabled
|
|
mptAlice.set({.account = alice, .id = mptID, .transferFee = 100, .err = temDISABLED});
|
|
mptAlice.set({.account = alice, .id = mptID, .transferFee = 0, .err = temDISABLED});
|
|
}
|
|
|
|
// Can not provide holder when MutableFlags, MPTokenMetadata or
|
|
// TransferFee is present
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
auto const mptID = makeMptID(env.seq(alice), alice);
|
|
|
|
// Holder is not allowed when MutableFlags is present
|
|
mptAlice.set({.account = alice, .holder = bob, .id = mptID, .mutableFlags = 2, .err = temMALFORMED});
|
|
|
|
// Holder is not allowed when MPTokenMetadata is present
|
|
mptAlice.set({.account = alice, .holder = bob, .id = mptID, .metadata = "test", .err = temMALFORMED});
|
|
|
|
// Holder is not allowed when TransferFee is present
|
|
mptAlice.set({.account = alice, .holder = bob, .id = mptID, .transferFee = 100, .err = temMALFORMED});
|
|
}
|
|
|
|
// Can not set Flags when MutableFlags, MPTokenMetadata or
|
|
// TransferFee is present
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create(
|
|
{.ownerCount = 1,
|
|
.mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanMutateCanLock | tmfMPTCanMutateTransferFee});
|
|
|
|
// Setting flags is not allowed when MutableFlags is present
|
|
mptAlice.set({.account = alice, .flags = tfMPTCanLock, .mutableFlags = 2, .err = temMALFORMED});
|
|
|
|
// Setting flags is not allowed when MPTokenMetadata is present
|
|
mptAlice.set({.account = alice, .flags = tfMPTCanLock, .metadata = "test", .err = temMALFORMED});
|
|
|
|
// setting flags is not allowed when TransferFee is present
|
|
mptAlice.set({.account = alice, .flags = tfMPTCanLock, .transferFee = 100, .err = temMALFORMED});
|
|
}
|
|
|
|
// Flags being 0 or tfFullyCanonicalSig is fine
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create(
|
|
{.transferFee = 10,
|
|
.ownerCount = 1,
|
|
.flags = tfMPTCanTransfer,
|
|
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateMetadata});
|
|
|
|
mptAlice.set({.account = alice, .flags = 0, .transferFee = 100, .metadata = "test"});
|
|
mptAlice.set({.account = alice, .flags = tfFullyCanonicalSig, .transferFee = 200, .metadata = "test2"});
|
|
}
|
|
|
|
// Invalid MutableFlags
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
auto const mptID = makeMptID(env.seq(alice), alice);
|
|
|
|
for (auto const flags : {10000, 0, 5000})
|
|
{
|
|
mptAlice.set({.account = alice, .id = mptID, .mutableFlags = flags, .err = temINVALID_FLAG});
|
|
}
|
|
}
|
|
|
|
// Can not set and clear the same mutable flag
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
auto const mptID = makeMptID(env.seq(alice), alice);
|
|
|
|
auto const flagCombinations = {
|
|
tmfMPTSetCanLock | tmfMPTClearCanLock,
|
|
tmfMPTSetRequireAuth | tmfMPTClearRequireAuth,
|
|
tmfMPTSetCanEscrow | tmfMPTClearCanEscrow,
|
|
tmfMPTSetCanTrade | tmfMPTClearCanTrade,
|
|
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer,
|
|
tmfMPTSetCanClawback | tmfMPTClearCanClawback,
|
|
tmfMPTSetPrivacy | tmfMPTClearPrivacy,
|
|
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade,
|
|
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanEscrow | tmfMPTClearCanClawback};
|
|
|
|
for (auto const& mutableFlags : flagCombinations)
|
|
{
|
|
mptAlice.set({.account = alice, .id = mptID, .mutableFlags = mutableFlags, .err = temINVALID_FLAG});
|
|
}
|
|
}
|
|
|
|
// Can not mutate flag which is not mutable
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
|
|
auto const mutableFlags = {
|
|
tmfMPTSetCanLock,
|
|
tmfMPTClearCanLock,
|
|
tmfMPTSetRequireAuth,
|
|
tmfMPTClearRequireAuth,
|
|
tmfMPTSetCanEscrow,
|
|
tmfMPTClearCanEscrow,
|
|
tmfMPTSetCanTrade,
|
|
tmfMPTClearCanTrade,
|
|
tmfMPTSetCanTransfer,
|
|
tmfMPTClearCanTransfer,
|
|
tmfMPTSetCanClawback,
|
|
tmfMPTClearCanClawback};
|
|
|
|
for (auto const& mutableFlag : mutableFlags)
|
|
{
|
|
mptAlice.set({.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION});
|
|
}
|
|
}
|
|
|
|
// Metadata exceeding max length
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
|
|
|
|
std::string metadata(maxMPTokenMetadataLength + 1, 'a');
|
|
mptAlice.set({.account = alice, .metadata = metadata, .err = temMALFORMED});
|
|
}
|
|
|
|
// Can not mutate metadata when it is not mutable
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1});
|
|
mptAlice.set({.account = alice, .metadata = "test", .err = tecNO_PERMISSION});
|
|
}
|
|
|
|
// Transfer fee exceeding the max value
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
auto const mptID = makeMptID(env.seq(alice), alice);
|
|
|
|
mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee});
|
|
|
|
mptAlice.set(
|
|
{.account = alice, .id = mptID, .transferFee = maxTransferFee + 1, .err = temBAD_TRANSFER_FEE});
|
|
}
|
|
|
|
// Test setting non-zero transfer fee and clearing MPTCanTransfer at the
|
|
// same time
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create(
|
|
{.transferFee = 100,
|
|
.ownerCount = 1,
|
|
.flags = tfMPTCanTransfer,
|
|
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});
|
|
|
|
// Can not set non-zero transfer fee and clear MPTCanTransfer at the
|
|
// same time
|
|
mptAlice.set(
|
|
{.account = alice, .mutableFlags = tmfMPTClearCanTransfer, .transferFee = 1, .err = temMALFORMED});
|
|
|
|
// Can set transfer fee to zero and clear MPTCanTransfer at the same
|
|
// time. tfMPTCanTransfer will be cleared and TransferFee field will
|
|
// be removed.
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer, .transferFee = 0});
|
|
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
|
|
}
|
|
|
|
// Can not set non-zero transfer fee when MPTCanTransfer is not set
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});
|
|
|
|
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
|
|
|
|
// Can not set transfer fee even when trying to set MPTCanTransfer
|
|
// at the same time. MPTCanTransfer must be set first, then transfer
|
|
// fee can be set in a separate transaction.
|
|
mptAlice.set(
|
|
{.account = alice, .mutableFlags = tmfMPTSetCanTransfer, .transferFee = 100, .err = tecNO_PERMISSION});
|
|
}
|
|
|
|
// Can not mutate transfer fee when it is not mutable
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.transferFee = 10, .ownerCount = 1, .flags = tfMPTCanTransfer});
|
|
|
|
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
|
|
|
|
mptAlice.set({.account = alice, .transferFee = 0, .err = tecNO_PERMISSION});
|
|
}
|
|
|
|
// Set some flags mutable. Can not mutate the others
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create(
|
|
{.ownerCount = 1,
|
|
.mutableFlags = tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateMetadata});
|
|
|
|
// Can not mutate transfer fee
|
|
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
|
|
|
|
auto const invalidFlags = {
|
|
tmfMPTSetCanLock,
|
|
tmfMPTClearCanLock,
|
|
tmfMPTSetRequireAuth,
|
|
tmfMPTClearRequireAuth,
|
|
tmfMPTSetCanEscrow,
|
|
tmfMPTClearCanEscrow,
|
|
tmfMPTSetCanClawback,
|
|
tmfMPTClearCanClawback};
|
|
|
|
// Can not mutate flags which are not mutable
|
|
for (auto const& mutableFlag : invalidFlags)
|
|
{
|
|
mptAlice.set({.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION});
|
|
}
|
|
|
|
// Can mutate MPTCanTrade
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade});
|
|
|
|
// Can mutate MPTCanTransfer
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
|
|
|
|
// Can mutate metadata
|
|
mptAlice.set({.account = alice, .metadata = "test"});
|
|
mptAlice.set({.account = alice, .metadata = ""});
|
|
}
|
|
}
|
|
|
|
void
|
|
testMutateMPT(FeatureBitset features)
|
|
{
|
|
testcase("Mutate MPT");
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
|
|
// Mutate metadata
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
|
|
|
|
std::vector<std::string> metadatas = {
|
|
"mutate metadata",
|
|
"mutate metadata 2",
|
|
"mutate metadata 3",
|
|
"mutate metadata 3",
|
|
"test",
|
|
"mutate metadata"};
|
|
|
|
for (auto const& metadata : metadatas)
|
|
{
|
|
mptAlice.set({.account = alice, .metadata = metadata});
|
|
BEAST_EXPECT(mptAlice.checkMetadata(metadata));
|
|
}
|
|
|
|
// Metadata being empty will remove the field
|
|
mptAlice.set({.account = alice, .metadata = ""});
|
|
BEAST_EXPECT(!mptAlice.isMetadataPresent());
|
|
}
|
|
|
|
// Mutate transfer fee
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
mptAlice.create(
|
|
{.transferFee = 100,
|
|
.metadata = "test",
|
|
.ownerCount = 1,
|
|
.flags = tfMPTCanTransfer,
|
|
.mutableFlags = tmfMPTCanMutateTransferFee});
|
|
|
|
for (std::uint16_t const fee :
|
|
std::initializer_list<std::uint16_t>{1, 10, 100, 200, 500, 1000, maxTransferFee})
|
|
{
|
|
mptAlice.set({.account = alice, .transferFee = fee});
|
|
BEAST_EXPECT(mptAlice.checkTransferFee(fee));
|
|
}
|
|
|
|
// Setting TransferFee to zero will remove the field
|
|
mptAlice.set({.account = alice, .transferFee = 0});
|
|
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
|
|
|
|
// Set transfer fee again
|
|
mptAlice.set({.account = alice, .transferFee = 10});
|
|
BEAST_EXPECT(mptAlice.checkTransferFee(10));
|
|
}
|
|
|
|
// Test flag toggling
|
|
{
|
|
auto testFlagToggle = [&](std::uint32_t createFlags, std::uint32_t setFlags, std::uint32_t clearFlags) {
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice);
|
|
|
|
// Create the MPT object with the specified initial flags
|
|
mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = createFlags});
|
|
|
|
// Set and clear the flag multiple times
|
|
mptAlice.set({.account = alice, .mutableFlags = setFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = setFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = setFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = setFlags});
|
|
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
|
|
};
|
|
|
|
testFlagToggle(tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock);
|
|
testFlagToggle(tmfMPTCanMutateRequireAuth, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth);
|
|
testFlagToggle(tmfMPTCanMutateCanEscrow, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow);
|
|
testFlagToggle(tmfMPTCanMutateCanTrade, tmfMPTSetCanTrade, tmfMPTClearCanTrade);
|
|
testFlagToggle(tmfMPTCanMutateCanTransfer, tmfMPTSetCanTransfer, tmfMPTClearCanTransfer);
|
|
testFlagToggle(tmfMPTCanMutateCanClawback, tmfMPTSetCanClawback, tmfMPTClearCanClawback);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMutateCanLock(FeatureBitset features)
|
|
{
|
|
testcase("Mutate MPTCanLock");
|
|
using namespace test::jtx;
|
|
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
|
|
// Individual lock
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create(
|
|
{.ownerCount = 1,
|
|
.holderCount = 0,
|
|
.flags = tfMPTCanLock | tfMPTCanTransfer,
|
|
.mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanTrade | tmfMPTCanMutateTransferFee});
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// Lock bob's mptoken
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
|
|
// Can mutate the mutable flags and fields
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade});
|
|
mptAlice.set({.account = alice, .transferFee = 200});
|
|
}
|
|
|
|
// Global lock
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create(
|
|
{.ownerCount = 1,
|
|
.holderCount = 0,
|
|
.flags = tfMPTCanLock,
|
|
.mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata});
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// Lock issuance
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
|
|
// Can mutate the mutable flags and fields
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback});
|
|
mptAlice.set({.account = alice, .metadata = "mutate"});
|
|
}
|
|
|
|
// Test lock and unlock after mutating MPTCanLock
|
|
{
|
|
Env env{*this, features};
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create(
|
|
{.ownerCount = 1,
|
|
.holderCount = 0,
|
|
.flags = tfMPTCanLock,
|
|
.mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata});
|
|
mptAlice.authorize({.account = bob, .holderCount = 1});
|
|
|
|
// Can lock and unlock
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
|
|
|
// Clear lsfMPTCanLock
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
|
|
|
|
// Can not lock or unlock
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION});
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
|
|
|
|
// Set MPTCanLock again
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
|
|
|
|
// Can lock and unlock again
|
|
mptAlice.set({.account = alice, .flags = tfMPTLock});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
|
|
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
|
|
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
|
|
}
|
|
}
|
|
|
|
void
|
|
testMutateRequireAuth(FeatureBitset features)
|
|
{
|
|
testcase("Mutate MPTRequireAuth");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth, .mutableFlags = tmfMPTCanMutateRequireAuth});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// Pay to bob
|
|
mptAlice.pay(alice, bob, 1000);
|
|
|
|
// Unauthorize bob
|
|
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
|
|
|
|
// Can not pay to bob
|
|
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
|
|
|
// Clear RequireAuth
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth});
|
|
|
|
// Can pay to bob
|
|
mptAlice.pay(alice, bob, 1000);
|
|
|
|
// Set RequireAuth again
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
|
|
|
|
// Can not pay to bob since he is not authorized
|
|
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
|
|
|
|
// Authorize bob again
|
|
mptAlice.authorize({.account = alice, .holder = bob});
|
|
|
|
// Can pay to bob again
|
|
mptAlice.pay(alice, bob, 100);
|
|
}
|
|
|
|
void
|
|
testMutateCanEscrow(FeatureBitset features)
|
|
{
|
|
testcase("Mutate MPTCanEscrow");
|
|
using namespace test::jtx;
|
|
using namespace std::literals;
|
|
|
|
Env env{*this, features};
|
|
auto const baseFee = env.current()->fees().base;
|
|
auto const alice = Account("alice");
|
|
auto const bob = Account("bob");
|
|
auto const carol = Account("carol");
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {carol, bob}});
|
|
mptAlice.create(
|
|
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer, .mutableFlags = tmfMPTCanMutateCanEscrow});
|
|
mptAlice.authorize({.account = carol});
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
auto const MPT = mptAlice["MPT"];
|
|
env(pay(alice, carol, MPT(10'000)));
|
|
env(pay(alice, bob, MPT(10'000)));
|
|
env.close();
|
|
|
|
// MPTCanEscrow is not enabled
|
|
env(escrow::create(carol, bob, MPT(3)),
|
|
escrow::condition(escrow::cb1),
|
|
escrow::finish_time(env.now() + 1s),
|
|
fee(baseFee * 150),
|
|
ter(tecNO_PERMISSION));
|
|
|
|
// MPTCanEscrow is enabled now
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanEscrow});
|
|
env(escrow::create(carol, bob, MPT(3)),
|
|
escrow::condition(escrow::cb1),
|
|
escrow::finish_time(env.now() + 1s),
|
|
fee(baseFee * 150));
|
|
|
|
// Clear MPTCanEscrow
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanEscrow});
|
|
env(escrow::create(carol, bob, MPT(3)),
|
|
escrow::condition(escrow::cb1),
|
|
escrow::finish_time(env.now() + 1s),
|
|
fee(baseFee * 150),
|
|
ter(tecNO_PERMISSION));
|
|
}
|
|
|
|
void
|
|
testMutateCanTransfer(FeatureBitset features)
|
|
{
|
|
testcase("Mutate MPTCanTransfer");
|
|
|
|
using namespace test::jtx;
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
Account const carol("carol");
|
|
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee});
|
|
|
|
mptAlice.authorize({.account = bob});
|
|
mptAlice.authorize({.account = carol});
|
|
|
|
// Pay to bob
|
|
mptAlice.pay(alice, bob, 1000);
|
|
|
|
// Bob can not pay carol since MPTCanTransfer is not set
|
|
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
|
|
|
|
// Can not set non-zero transfer fee when MPTCanTransfer is not set
|
|
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
|
|
|
|
// Can not set non-zero transfer fee even when trying to set
|
|
// MPTCanTransfer at the same time
|
|
mptAlice.set(
|
|
{.account = alice, .mutableFlags = tmfMPTSetCanTransfer, .transferFee = 100, .err = tecNO_PERMISSION});
|
|
|
|
// Alice sets MPTCanTransfer
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
|
|
|
|
// Can set transfer fee now
|
|
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
|
|
mptAlice.set({.account = alice, .transferFee = 100});
|
|
BEAST_EXPECT(mptAlice.isTransferFeePresent());
|
|
|
|
// Bob can pay carol
|
|
mptAlice.pay(bob, carol, 50);
|
|
|
|
// Alice clears MPTCanTransfer
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
|
|
|
|
// TransferFee field is removed when MPTCanTransfer is cleared
|
|
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
|
|
|
|
// Bob can not pay
|
|
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
|
|
}
|
|
|
|
// Can set transfer fee to zero when MPTCanTransfer is not set, but
|
|
// tmfMPTCanMutateTransferFee is set.
|
|
{
|
|
Env env{*this, features};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
|
mptAlice.create(
|
|
{.transferFee = 100,
|
|
.ownerCount = 1,
|
|
.flags = tfMPTCanTransfer,
|
|
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});
|
|
|
|
BEAST_EXPECT(mptAlice.checkTransferFee(100));
|
|
|
|
// Clear MPTCanTransfer and transfer fee is removed
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
|
|
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
|
|
|
|
// Can still set transfer fee to zero, although it is already zero
|
|
mptAlice.set({.account = alice, .transferFee = 0});
|
|
|
|
// TransferFee field is still not present
|
|
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
|
|
}
|
|
}
|
|
|
|
void
|
|
testMutateCanClawback(FeatureBitset features)
|
|
{
|
|
testcase("Mutate MPTCanClawback");
|
|
|
|
using namespace test::jtx;
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
|
|
|
mptAlice.create({.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanMutateCanClawback});
|
|
|
|
// Bob creates an MPToken
|
|
mptAlice.authorize({.account = bob});
|
|
|
|
// Alice pays bob 100 tokens
|
|
mptAlice.pay(alice, bob, 100);
|
|
|
|
// MPTCanClawback is not enabled
|
|
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
|
|
|
|
// Enable MPTCanClawback
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});
|
|
|
|
// Can clawback now
|
|
mptAlice.claw(alice, bob, 1);
|
|
|
|
// Clear MPTCanClawback
|
|
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback});
|
|
|
|
// Can not clawback
|
|
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
using namespace test::jtx;
|
|
FeatureBitset const all{testable_amendments()};
|
|
|
|
// MPTokenIssuanceCreate
|
|
testCreateValidation(all - featureSingleAssetVault);
|
|
testCreateValidation(all - featurePermissionedDomains);
|
|
testCreateValidation(all);
|
|
testCreateEnabled(all - featureSingleAssetVault);
|
|
testCreateEnabled(all);
|
|
|
|
// MPTokenIssuanceDestroy
|
|
testDestroyValidation(all - featureSingleAssetVault);
|
|
testDestroyValidation(all);
|
|
testDestroyEnabled(all - featureSingleAssetVault);
|
|
testDestroyEnabled(all);
|
|
|
|
// MPTokenAuthorize
|
|
testAuthorizeValidation(all - featureSingleAssetVault);
|
|
testAuthorizeValidation(all);
|
|
testAuthorizeEnabled(all - featureSingleAssetVault);
|
|
testAuthorizeEnabled(all);
|
|
|
|
// MPTokenIssuanceSet
|
|
testSetValidation(all - featureSingleAssetVault - featureDynamicMPT);
|
|
testSetValidation(all - featureSingleAssetVault);
|
|
testSetValidation(all - featureDynamicMPT);
|
|
testSetValidation(all - featurePermissionedDomains);
|
|
testSetValidation(all);
|
|
|
|
testSetEnabled(all - featureSingleAssetVault);
|
|
testSetEnabled(all);
|
|
|
|
// MPT clawback
|
|
testClawbackValidation(all);
|
|
testClawback(all);
|
|
|
|
// Test Direct Payment
|
|
testPayment(all);
|
|
testDepositPreauth(all);
|
|
testDepositPreauth(all - featureCredentials);
|
|
|
|
// Test MPT Amount is invalid in Tx, which don't support MPT
|
|
testMPTInvalidInTx(all);
|
|
|
|
// Test parsed MPTokenIssuanceID in API response metadata
|
|
testTxJsonMetaFields(all);
|
|
|
|
// Test tokens equality
|
|
testTokensEquality();
|
|
|
|
// Test helpers
|
|
testHelperFunctions();
|
|
|
|
// Dynamic MPT
|
|
testInvalidCreateDynamic(all);
|
|
testInvalidSetDynamic(all);
|
|
testMutateMPT(all);
|
|
testMutateCanLock(all);
|
|
testMutateRequireAuth(all);
|
|
testMutateCanEscrow(all);
|
|
testMutateCanTransfer(all);
|
|
testMutateCanClawback(all);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE_PRIO(MPToken, app, xrpl, 2);
|
|
|
|
} // namespace test
|
|
} // namespace xrpl
|