mirror of
https://github.com/XRPLF/rippled.git
synced 2026-02-01 12:35:22 +00:00
Update lsfMPTNoConfidentialTransfer to lsfMPTPrivacy Add flag lsmfMPTPrivacy to control the mutability of lsfMPTPrivacy. disallow mutating lsfMPTPrivacy when lsfMPTPrivacy is not set. disallow mutating lsfMPTPrivacy when there's confidential outstanding amount.
3684 lines
128 KiB
C++
3684 lines
128 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 ripple {
|
|
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});
|
|
|
|
// MaximumAmout 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 specifys 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)),
|
|
delivermin(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)),
|
|
delivermin(MPT(100)),
|
|
ter(temBAD_AMOUNT));
|
|
env(pay(alice, carol, XRP(100)),
|
|
delivermin(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)),
|
|
delivermin(MPT(100)),
|
|
txflags(tfPartialPayment),
|
|
ter(tecPATH_PARTIAL));
|
|
// Payment succeeds if deliver amount >= deliverMin
|
|
env(pay(bob, alice, MPT(100)),
|
|
sendmax(MPT(99)),
|
|
delivermin(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] =
|
|
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 preauthorization
|
|
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 preauthorization
|
|
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 = ripple::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 = ripple::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 = ripple::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 = ripple::test::jtx::MPT(
|
|
alice.name(), makeMptID(env.seq(alice), alice));
|
|
|
|
Json::Value jv = claw(alice, mpt(1), bob);
|
|
jv[jss::Amount][jss::value] = 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 arithmetics");
|
|
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 arithmetics");
|
|
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, ripple, 2);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|