Skip to content

Commit 864f7b9

Browse files
authored
feat: Multi-Purpose Tokens (MPT) (#732)
Adds Multi-Purpose Tokens (MPT) feature.
1 parent c8c634b commit 864f7b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2788
-1196
lines changed

.ci-config/rippled.cfg

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ PriceOracle
182182
fixEmptyDID
183183
fixXChainRewardRounding
184184
fixPreviousTxnID
185+
# 2.3.0-rc1 Amendments
186+
fixAMMv1_1
187+
Credentials
188+
NFTokenMintOffer
189+
MPTokensV1
190+
fixNFTokenPageLinks
191+
fixInnerObjTemplate2
192+
fixEnforceNFTokenTrustline
193+
fixReducedOffersV2
185194

186195
# This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode
187196
[voting]

.github/workflows/integration_test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Integration test
22

33
env:
44
POETRY_VERSION: 1.8.3
5-
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.2.0-b3
5+
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
66

77
on:
88
push:
@@ -32,7 +32,7 @@ jobs:
3232

3333
- name: Run docker in background
3434
run: |
35-
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
35+
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"
3636
3737
- name: Install poetry
3838
if: steps.cache-poetry.outputs.cache-hit != 'true'

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [[Unreleased]]
99

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

1314
### BREAKING CHANGE:

poetry.lock

Lines changed: 113 additions & 91 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "xrpl-py"
3-
version = "3.0.0"
3+
version = "4.0.0"
44
description = "A complete Python library for interacting with the XRP ledger"
55
readme = "README.md"
66
repository = "https://github.com/XRPLF/xrpl-py"
@@ -50,6 +50,9 @@ coverage = "^7.2.7"
5050
Sphinx = "^7.1.2"
5151
poethepoet = "^0.30.0"
5252

53+
[tool.poetry.group.dev.dependencies]
54+
packaging = "^24.1"
55+
5356
[tool.isort]
5457
# Make sure that isort's settings line up with black
5558
profile = "black"

tests/integration/transactions/test_clawback.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from tests.integration.integration_test_case import IntegrationTestCase
22
from tests.integration.it_utils import (
33
fund_wallet,
4+
fund_wallet_async,
45
sign_and_reliable_submission_async,
56
test_async_and_sync,
67
)
@@ -10,10 +11,18 @@
1011
AccountSetAsfFlag,
1112
Clawback,
1213
IssuedCurrencyAmount,
14+
MPTAmount,
1315
Payment,
1416
TrustSet,
1517
TrustSetFlag,
1618
)
19+
from xrpl.models.requests import LedgerEntry, Tx
20+
from xrpl.models.requests.ledger_entry import MPToken
21+
from xrpl.models.transactions import (
22+
MPTokenAuthorize,
23+
MPTokenIssuanceCreate,
24+
MPTokenIssuanceCreateFlag,
25+
)
1726
from xrpl.wallet import Wallet
1827

1928
HOLDER = Wallet.create()
@@ -71,3 +80,91 @@ async def test_basic_functionality(self, client):
7180
client,
7281
)
7382
self.assertTrue(response.is_successful())
83+
84+
@test_async_and_sync(globals())
85+
async def test_mptoken(self, client):
86+
wallet2 = Wallet.create()
87+
await fund_wallet_async(wallet2)
88+
89+
tx = MPTokenIssuanceCreate(
90+
account=WALLET.classic_address,
91+
flags=MPTokenIssuanceCreateFlag.TF_MPT_CAN_CLAWBACK,
92+
)
93+
94+
create_res = await sign_and_reliable_submission_async(
95+
tx,
96+
WALLET,
97+
client,
98+
)
99+
100+
self.assertTrue(create_res.is_successful())
101+
self.assertEqual(create_res.result["engine_result"], "tesSUCCESS")
102+
103+
tx_hash = create_res.result["tx_json"]["hash"]
104+
105+
tx_res = await client.request(Tx(transaction=tx_hash))
106+
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]
107+
108+
auth_tx = MPTokenAuthorize(
109+
account=wallet2.classic_address,
110+
mptoken_issuance_id=mpt_issuance_id,
111+
)
112+
113+
auth_res = await sign_and_reliable_submission_async(
114+
auth_tx,
115+
wallet2,
116+
client,
117+
)
118+
119+
self.assertTrue(auth_res.is_successful())
120+
self.assertEqual(auth_res.result["engine_result"], "tesSUCCESS")
121+
122+
await sign_and_reliable_submission_async(
123+
Payment(
124+
account=WALLET.classic_address,
125+
destination=wallet2.classic_address,
126+
amount=MPTAmount(
127+
mpt_issuance_id=mpt_issuance_id, value="9223372036854775807"
128+
),
129+
),
130+
WALLET,
131+
)
132+
133+
ledger_entry_res = await client.request(
134+
LedgerEntry(
135+
mptoken=MPToken(
136+
mpt_issuance_id=mpt_issuance_id, account=wallet2.classic_address
137+
)
138+
)
139+
)
140+
self.assertEqual(
141+
ledger_entry_res.result["node"]["MPTAmount"], "9223372036854775807"
142+
)
143+
144+
# actual test - clawback
145+
response = await sign_and_reliable_submission_async(
146+
Clawback(
147+
account=WALLET.classic_address,
148+
amount=MPTAmount(
149+
mpt_issuance_id=mpt_issuance_id,
150+
value="500",
151+
),
152+
holder=wallet2.classic_address,
153+
),
154+
WALLET,
155+
client,
156+
)
157+
158+
self.assertTrue(response.is_successful())
159+
self.assertEqual(auth_res.result["engine_result"], "tesSUCCESS")
160+
161+
ledger_entry_res = await client.request(
162+
LedgerEntry(
163+
mptoken=MPToken(
164+
mpt_issuance_id=mpt_issuance_id, account=wallet2.classic_address
165+
)
166+
)
167+
)
168+
self.assertEqual(
169+
ledger_entry_res.result["node"]["MPTAmount"], "9223372036854775307"
170+
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
fund_wallet_async,
4+
sign_and_reliable_submission_async,
5+
test_async_and_sync,
6+
)
7+
from tests.integration.reusable_values import WALLET
8+
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
9+
from xrpl.models.requests.tx import Tx
10+
from xrpl.models.transactions import (
11+
MPTokenAuthorize,
12+
MPTokenAuthorizeFlag,
13+
MPTokenIssuanceCreate,
14+
MPTokenIssuanceCreateFlag,
15+
)
16+
from xrpl.wallet.main import Wallet
17+
18+
19+
class TestMPTokenAuthorize(IntegrationTestCase):
20+
@test_async_and_sync(globals())
21+
async def test_basic_functionality(self, client):
22+
tx = MPTokenIssuanceCreate(
23+
account=WALLET.classic_address,
24+
flags=MPTokenIssuanceCreateFlag.TF_MPT_REQUIRE_AUTH,
25+
)
26+
27+
create_res = await sign_and_reliable_submission_async(
28+
tx,
29+
WALLET,
30+
client,
31+
)
32+
33+
self.assertTrue(create_res.is_successful())
34+
self.assertEqual(create_res.result["engine_result"], "tesSUCCESS")
35+
36+
tx_hash = create_res.result["tx_json"]["hash"]
37+
38+
tx_res = await client.request(Tx(transaction=tx_hash))
39+
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]
40+
41+
# confirm MPTokenIssuance ledger object was created
42+
account_objects_response = await client.request(
43+
AccountObjects(account=WALLET.address, type=AccountObjectType.MPT_ISSUANCE)
44+
)
45+
46+
# subsequent integration tests (sync/async + json/websocket) add one
47+
# MPTokenIssuance object to the account
48+
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)
49+
50+
wallet2 = Wallet.create()
51+
await fund_wallet_async(wallet2)
52+
53+
auth_tx = MPTokenAuthorize(
54+
account=wallet2.classic_address,
55+
mptoken_issuance_id=mpt_issuance_id,
56+
)
57+
58+
auth_res = await sign_and_reliable_submission_async(
59+
auth_tx,
60+
wallet2,
61+
client,
62+
)
63+
64+
self.assertTrue(auth_res.is_successful())
65+
self.assertEqual(auth_res.result["engine_result"], "tesSUCCESS")
66+
67+
# confirm MPToken ledger object was created
68+
account_objects_response2 = await client.request(
69+
AccountObjects(account=wallet2.address, type=AccountObjectType.MPTOKEN)
70+
)
71+
72+
# subsequent integration tests (sync/async + json/websocket) add one
73+
# MPToken object to the account
74+
self.assertTrue(len(account_objects_response2.result["account_objects"]) > 0)
75+
76+
auth_tx2 = MPTokenAuthorize(
77+
account=WALLET.classic_address,
78+
mptoken_issuance_id=mpt_issuance_id,
79+
holder=wallet2.classic_address,
80+
)
81+
82+
auth_res2 = await sign_and_reliable_submission_async(
83+
auth_tx2,
84+
WALLET,
85+
client,
86+
)
87+
88+
self.assertTrue(auth_res2.is_successful())
89+
self.assertEqual(auth_res2.result["engine_result"], "tesSUCCESS")
90+
91+
auth_tx3 = MPTokenAuthorize(
92+
account=wallet2.classic_address,
93+
mptoken_issuance_id=mpt_issuance_id,
94+
flags=MPTokenAuthorizeFlag.TF_MPT_UNAUTHORIZE,
95+
)
96+
97+
auth_res3 = await sign_and_reliable_submission_async(
98+
auth_tx3,
99+
wallet2,
100+
client,
101+
)
102+
103+
self.assertTrue(auth_res3.is_successful())
104+
self.assertEqual(auth_res3.result["engine_result"], "tesSUCCESS")
105+
106+
# confirm MPToken ledger object is removed
107+
account_objects_response3 = await client.request(
108+
AccountObjects(account=wallet2.address, type=AccountObjectType.MPTOKEN)
109+
)
110+
111+
# Should no longer hold an MPToken ledger object
112+
self.assertTrue(len(account_objects_response3.result["account_objects"]) == 0)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
sign_and_reliable_submission_async,
4+
test_async_and_sync,
5+
)
6+
from tests.integration.reusable_values import WALLET
7+
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
8+
from xrpl.models.transactions import MPTokenIssuanceCreate
9+
10+
11+
class TestMPTokenIssuanceCreate(IntegrationTestCase):
12+
@test_async_and_sync(globals())
13+
async def test_basic_functionality(self, client):
14+
tx = MPTokenIssuanceCreate(
15+
account=WALLET.classic_address,
16+
maximum_amount="9223372036854775807", # "7fffffffffffffff"
17+
asset_scale=2,
18+
)
19+
20+
response = await sign_and_reliable_submission_async(
21+
tx,
22+
WALLET,
23+
client,
24+
)
25+
26+
self.assertTrue(response.is_successful())
27+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
28+
29+
# confirm MPTokenIssuance ledger object was created
30+
account_objects_response = await client.request(
31+
AccountObjects(account=WALLET.address, type=AccountObjectType.MPT_ISSUANCE)
32+
)
33+
34+
# subsequent integration tests (sync/async + json/websocket) add one
35+
# MPTokenIssuance object to the account
36+
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
sign_and_reliable_submission_async,
4+
test_async_and_sync,
5+
)
6+
from tests.integration.reusable_values import WALLET
7+
from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType
8+
from xrpl.models.requests.tx import Tx
9+
from xrpl.models.transactions import MPTokenIssuanceCreate, MPTokenIssuanceDestroy
10+
11+
12+
class TestMPTokenIssuanceDestroy(IntegrationTestCase):
13+
@test_async_and_sync(globals())
14+
async def test_basic_functionality(self, client):
15+
tx = MPTokenIssuanceCreate(
16+
account=WALLET.classic_address,
17+
)
18+
19+
create_res = await sign_and_reliable_submission_async(
20+
tx,
21+
WALLET,
22+
client,
23+
)
24+
25+
self.assertTrue(create_res.is_successful())
26+
self.assertEqual(create_res.result["engine_result"], "tesSUCCESS")
27+
28+
tx_hash = create_res.result["tx_json"]["hash"]
29+
30+
tx_res = await client.request(Tx(transaction=tx_hash))
31+
mpt_issuance_id = tx_res.result["meta"]["mpt_issuance_id"]
32+
33+
# confirm MPTokenIssuance ledger object was created
34+
account_objects_response = await client.request(
35+
AccountObjects(
36+
account=WALLET.classic_address, type=AccountObjectType.MPT_ISSUANCE
37+
)
38+
)
39+
40+
# subsequent integration tests (sync/async + json/websocket) add one
41+
# MPTokenIssuance object to the account
42+
self.assertTrue(len(account_objects_response.result["account_objects"]) > 0)
43+
44+
destroy_res = await sign_and_reliable_submission_async(
45+
MPTokenIssuanceDestroy(
46+
account=WALLET.classic_address,
47+
mptoken_issuance_id=mpt_issuance_id,
48+
),
49+
WALLET,
50+
client,
51+
)
52+
53+
self.assertTrue(destroy_res.is_successful())
54+
self.assertEqual(destroy_res.result["engine_result"], "tesSUCCESS")
55+
56+
# confirm MPToken ledger object is removed
57+
account_objects_response3 = await client.request(
58+
AccountObjects(
59+
account=WALLET.classic_address, type=AccountObjectType.MPTOKEN
60+
)
61+
)
62+
63+
# Should no longer hold an MPToken ledger object
64+
self.assertTrue(len(account_objects_response3.result["account_objects"]) == 0)

0 commit comments

Comments
 (0)