Skip to content

Commit

Permalink
feat: Multi-Purpose Tokens (MPT) (#732)
Browse files Browse the repository at this point in the history
Adds Multi-Purpose Tokens (MPT) feature.
  • Loading branch information
khancode authored Dec 12, 2024
1 parent c8c634b commit 864f7b9
Show file tree
Hide file tree
Showing 45 changed files with 2,788 additions and 1,196 deletions.
9 changes: 9 additions & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ PriceOracle
fixEmptyDID
fixXChainRewardRounding
fixPreviousTxnID
# 2.3.0-rc1 Amendments
fixAMMv1_1
Credentials
NFTokenMintOffer
MPTokensV1
fixNFTokenPageLinks
fixInnerObjTemplate2
fixEnforceNFTokenTrustline
fixReducedOffersV2

# This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode
[voting]
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Integration test

env:
POETRY_VERSION: 1.8.3
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.2.0-b3
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1

on:
push:
Expand Down Expand Up @@ -32,7 +32,7 @@ jobs:

- name: Run docker in background
run: |
docker run --detach --rm --name rippled-service -p 5005:5005 -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/opt/ripple/etc/" --health-cmd="wget localhost:6006 || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true ${{ env.RIPPLED_DOCKER_IMAGE }} /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
docker run --detach --rm -p 5005:5005 -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_nfo || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a"
- name: Install poetry
if: steps.cache-poetry.outputs.cache-hit != 'true'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [[Unreleased]]

### Added
- Support for the Multi-Purpose Tokens (MPT) amendment (XLS-33)
- Add `include_deleted` to ledger_entry request

### BREAKING CHANGE:
Expand Down
204 changes: 113 additions & 91 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "xrpl-py"
version = "3.0.0"
version = "4.0.0"
description = "A complete Python library for interacting with the XRP ledger"
readme = "README.md"
repository = "https://github.com/XRPLF/xrpl-py"
Expand Down Expand Up @@ -50,6 +50,9 @@ coverage = "^7.2.7"
Sphinx = "^7.1.2"
poethepoet = "^0.30.0"

[tool.poetry.group.dev.dependencies]
packaging = "^24.1"

[tool.isort]
# Make sure that isort's settings line up with black
profile = "black"
Expand Down
97 changes: 97 additions & 0 deletions tests/integration/transactions/test_clawback.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
fund_wallet,
fund_wallet_async,
sign_and_reliable_submission_async,
test_async_and_sync,
)
Expand All @@ -10,10 +11,18 @@
AccountSetAsfFlag,
Clawback,
IssuedCurrencyAmount,
MPTAmount,
Payment,
TrustSet,
TrustSetFlag,
)
from xrpl.models.requests import LedgerEntry, Tx
from xrpl.models.requests.ledger_entry import MPToken
from xrpl.models.transactions import (
MPTokenAuthorize,
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlag,
)
from xrpl.wallet import Wallet

HOLDER = Wallet.create()
Expand Down Expand Up @@ -71,3 +80,91 @@ async def test_basic_functionality(self, client):
client,
)
self.assertTrue(response.is_successful())

@test_async_and_sync(globals())
async def test_mptoken(self, client):
wallet2 = Wallet.create()
await fund_wallet_async(wallet2)

tx = MPTokenIssuanceCreate(
account=WALLET.classic_address,
flags=MPTokenIssuanceCreateFlag.TF_MPT_CAN_CLAWBACK,
)

create_res = await sign_and_reliable_submission_async(
tx,
WALLET,
client,
)

self.assertTrue(create_res.is_successful())
self.assertEqual(create_res.result["engine_result"], "tesSUCCESS")

tx_hash = create_res.result["tx_json"]["hash"]

tx_res = await client.request(Tx(transaction=tx_hash))
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]

auth_tx = MPTokenAuthorize(
account=wallet2.classic_address,
mptoken_issuance_id=mpt_issuance_id,
)

auth_res = await sign_and_reliable_submission_async(
auth_tx,
wallet2,
client,
)

self.assertTrue(auth_res.is_successful())
self.assertEqual(auth_res.result["engine_result"], "tesSUCCESS")

await sign_and_reliable_submission_async(
Payment(
account=WALLET.classic_address,
destination=wallet2.classic_address,
amount=MPTAmount(
mpt_issuance_id=mpt_issuance_id, value="9223372036854775807"
),
),
WALLET,
)

ledger_entry_res = await client.request(
LedgerEntry(
mptoken=MPToken(
mpt_issuance_id=mpt_issuance_id, account=wallet2.classic_address
)
)
)
self.assertEqual(
ledger_entry_res.result["node"]["MPTAmount"], "9223372036854775807"
)

# actual test - clawback
response = await sign_and_reliable_submission_async(
Clawback(
account=WALLET.classic_address,
amount=MPTAmount(
mpt_issuance_id=mpt_issuance_id,
value="500",
),
holder=wallet2.classic_address,
),
WALLET,
client,
)

self.assertTrue(response.is_successful())
self.assertEqual(auth_res.result["engine_result"], "tesSUCCESS")

ledger_entry_res = await client.request(
LedgerEntry(
mptoken=MPToken(
mpt_issuance_id=mpt_issuance_id, account=wallet2.classic_address
)
)
)
self.assertEqual(
ledger_entry_res.result["node"]["MPTAmount"], "9223372036854775307"
)
112 changes: 112 additions & 0 deletions tests/integration/transactions/test_mptoken_authorize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
fund_wallet_async,
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
from xrpl.models.requests.tx import Tx
from xrpl.models.transactions import (
MPTokenAuthorize,
MPTokenAuthorizeFlag,
MPTokenIssuanceCreate,
MPTokenIssuanceCreateFlag,
)
from xrpl.wallet.main import Wallet


class TestMPTokenAuthorize(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_basic_functionality(self, client):
tx = MPTokenIssuanceCreate(
account=WALLET.classic_address,
flags=MPTokenIssuanceCreateFlag.TF_MPT_REQUIRE_AUTH,
)

create_res = await sign_and_reliable_submission_async(
tx,
WALLET,
client,
)

self.assertTrue(create_res.is_successful())
self.assertEqual(create_res.result["engine_result"], "tesSUCCESS")

tx_hash = create_res.result["tx_json"]["hash"]

tx_res = await client.request(Tx(transaction=tx_hash))
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]

# confirm MPTokenIssuance ledger object was created
account_objects_response = await client.request(
AccountObjects(account=WALLET.address, type=AccountObjectType.MPT_ISSUANCE)
)

# subsequent integration tests (sync/async + json/websocket) add one
# MPTokenIssuance object to the account
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)

wallet2 = Wallet.create()
await fund_wallet_async(wallet2)

auth_tx = MPTokenAuthorize(
account=wallet2.classic_address,
mptoken_issuance_id=mpt_issuance_id,
)

auth_res = await sign_and_reliable_submission_async(
auth_tx,
wallet2,
client,
)

self.assertTrue(auth_res.is_successful())
self.assertEqual(auth_res.result["engine_result"], "tesSUCCESS")

# confirm MPToken ledger object was created
account_objects_response2 = await client.request(
AccountObjects(account=wallet2.address, type=AccountObjectType.MPTOKEN)
)

# subsequent integration tests (sync/async + json/websocket) add one
# MPToken object to the account
self.assertTrue(len(account_objects_response2.result["account_objects"]) > 0)

auth_tx2 = MPTokenAuthorize(
account=WALLET.classic_address,
mptoken_issuance_id=mpt_issuance_id,
holder=wallet2.classic_address,
)

auth_res2 = await sign_and_reliable_submission_async(
auth_tx2,
WALLET,
client,
)

self.assertTrue(auth_res2.is_successful())
self.assertEqual(auth_res2.result["engine_result"], "tesSUCCESS")

auth_tx3 = MPTokenAuthorize(
account=wallet2.classic_address,
mptoken_issuance_id=mpt_issuance_id,
flags=MPTokenAuthorizeFlag.TF_MPT_UNAUTHORIZE,
)

auth_res3 = await sign_and_reliable_submission_async(
auth_tx3,
wallet2,
client,
)

self.assertTrue(auth_res3.is_successful())
self.assertEqual(auth_res3.result["engine_result"], "tesSUCCESS")

# confirm MPToken ledger object is removed
account_objects_response3 = await client.request(
AccountObjects(account=wallet2.address, type=AccountObjectType.MPTOKEN)
)

# Should no longer hold an MPToken ledger object
self.assertTrue(len(account_objects_response3.result["account_objects"]) == 0)
36 changes: 36 additions & 0 deletions tests/integration/transactions/test_mptoken_issuance_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
from xrpl.models.transactions import MPTokenIssuanceCreate


class TestMPTokenIssuanceCreate(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_basic_functionality(self, client):
tx = MPTokenIssuanceCreate(
account=WALLET.classic_address,
maximum_amount="9223372036854775807", # "7fffffffffffffff"
asset_scale=2,
)

response = await sign_and_reliable_submission_async(
tx,
WALLET,
client,
)

self.assertTrue(response.is_successful())
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# confirm MPTokenIssuance ledger object was created
account_objects_response = await client.request(
AccountObjects(account=WALLET.address, type=AccountObjectType.MPT_ISSUANCE)
)

# subsequent integration tests (sync/async + json/websocket) add one
# MPTokenIssuance object to the account
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)
64 changes: 64 additions & 0 deletions tests/integration/transactions/test_mptoken_issuance_destroy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
from xrpl.models.requests.tx import Tx
from xrpl.models.transactions import MPTokenIssuanceCreate, MPTokenIssuanceDestroy


class TestMPTokenIssuanceDestroy(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_basic_functionality(self, client):
tx = MPTokenIssuanceCreate(
account=WALLET.classic_address,
)

create_res = await sign_and_reliable_submission_async(
tx,
WALLET,
client,
)

self.assertTrue(create_res.is_successful())
self.assertEqual(create_res.result["engine_result"], "tesSUCCESS")

tx_hash = create_res.result["tx_json"]["hash"]

tx_res = await client.request(Tx(transaction=tx_hash))
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]

# confirm MPTokenIssuance ledger object was created
account_objects_response = await client.request(
AccountObjects(
account=WALLET.classic_address, type=AccountObjectType.MPT_ISSUANCE
)
)

# subsequent integration tests (sync/async + json/websocket) add one
# MPTokenIssuance object to the account
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)

destroy_res = await sign_and_reliable_submission_async(
MPTokenIssuanceDestroy(
account=WALLET.classic_address,
mptoken_issuance_id=mpt_issuance_id,
),
WALLET,
client,
)

self.assertTrue(destroy_res.is_successful())
self.assertEqual(destroy_res.result["engine_result"], "tesSUCCESS")

# confirm MPToken ledger object is removed
account_objects_response3 = await client.request(
AccountObjects(
account=WALLET.classic_address, type=AccountObjectType.MPTOKEN
)
)

# Should no longer hold an MPToken ledger object
self.assertTrue(len(account_objects_response3.result["account_objects"]) == 0)
Loading

0 comments on commit 864f7b9

Please sign in to comment.