diff --git a/.vscode/settings.json b/.vscode/settings.json index 43503b8a3..c2e7b30b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,12 +13,18 @@ "asyncio", "autofills", "binarycodec", + "Clawback", + "isnumeric", "keypair", "keypairs", "multisign", "nftoken", "rippletest", "ripplex", - "xaddress" + "xaddress", + "xchain" ], + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index f97afe897..c6278398f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added new syntax for `SetFee` pseudo transaction sent after the [XRPFees](https://xrpl.org/known-amendments.html#xrpfees) amendment. (Backwards compatible) +- Support for [XLS-38d (XChainBridge)](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-38d-XChainBridge) ### Fixed - Update request models related to AMM @@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.1.0] - 2023-07-24 ### Fixed -- Replaced alias for `classic_address` with separate property to work around this mypy issue: +- Replaced alias for `classic_address` with separate property to work around this mypy issue: https://github.com/python/mypy/issues/6700 ## [2.0.0] - 2023-07-05 diff --git a/snippets/bridge_transfer.py b/snippets/bridge_transfer.py new file mode 100644 index 000000000..4801e000a --- /dev/null +++ b/snippets/bridge_transfer.py @@ -0,0 +1,105 @@ +"""CLI command for setting up a bridge.""" + +from pprint import pprint +from time import sleep + +from xrpl.account import does_account_exist, get_balance +from xrpl.clients import JsonRpcClient +from xrpl.models import ( + AccountObjects, + AccountObjectType, + XChainAccountCreateCommit, + XChainBridge, + XChainCommit, + XChainCreateClaimID, +) +from xrpl.transaction import submit_and_wait +from xrpl.utils import get_xchain_claim_id, xrp_to_drops +from xrpl.wallet import Wallet, generate_faucet_wallet + +locking_client = JsonRpcClient("https://sidechain-net1.devnet.rippletest.net:51234") +issuing_client = JsonRpcClient("https://sidechain-net2.devnet.rippletest.net:51234") + +locking_chain_door = "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4" +bridge_data = locking_client.request( + AccountObjects(account=locking_chain_door, type=AccountObjectType.BRIDGE) +).result["account_objects"][0] +bridge = XChainBridge.from_xrpl(bridge_data["XChainBridge"]) +print(bridge) + +print("Funding a wallet via the faucet on the locking chain...") +wallet1 = generate_faucet_wallet(locking_client, debug=True) +print(wallet1) +wallet2 = Wallet.create() + +print(f"Creating {wallet2.classic_address} on the issuing chain via the bridge...") + +fund_tx = XChainAccountCreateCommit( + account=wallet1.classic_address, + xchain_bridge=bridge, + signature_reward=bridge_data["SignatureReward"], + destination=wallet2.classic_address, + amount=str(int(bridge_data["MinAccountCreateAmount"]) * 2), +) +fund_response = submit_and_wait(fund_tx, locking_client, wallet1) +pprint(fund_response.result) + + +print("Waiting for the attestation to go through on the issuing chain...") +ledgers_waited = 0 +MAX_LEDGERS_WAITED = 5 +while ledgers_waited < MAX_LEDGERS_WAITED: + sleep(4) + if does_account_exist(wallet2.classic_address, issuing_client): + print( + f"Destination account {wallet2.classic_address} has been created via the " + "bridge" + ) + initial_balance = get_balance(wallet2.classic_address, issuing_client) + break + + ledgers_waited += 1 + if ledgers_waited == MAX_LEDGERS_WAITED: + raise Exception("Destination account creation via the bridge failed.") + +print( + "Submitting XChainCreateClaimID transaction on the issuing chain to get an " + "XChainOwnedClaimID for the transfer..." +) +create_claim_id_tx = XChainCreateClaimID( + account=wallet2.classic_address, + xchain_bridge=bridge, + signature_reward=bridge_data["SignatureReward"], + other_chain_source=wallet1.classic_address, +) +create_claim_id_result = submit_and_wait(create_claim_id_tx, issuing_client, wallet2) +pprint(create_claim_id_result.result) + +# Extract new sequence number from metadata +xchain_claim_id = get_xchain_claim_id(create_claim_id_result.result["meta"]) + +# XChainCommit + +print("Sending XChainCommit tx to lock the funds for transfer...") +commit_tx = XChainCommit( + account=wallet1.classic_address, + amount=xrp_to_drops(1), + xchain_bridge=bridge, + xchain_claim_id=xchain_claim_id, + other_chain_destination=wallet2.classic_address, +) +commit_result = submit_and_wait(commit_tx, locking_client, wallet1) +pprint(commit_result.result) + +print("Waiting for the attestation to go through on the issuing chain...") +ledgers_waited = 0 +while ledgers_waited < MAX_LEDGERS_WAITED: + sleep(4) + current_balance = get_balance(wallet2.classic_address, issuing_client) + if current_balance != initial_balance: + print("Transfer is complete!") + break + + ledgers_waited += 1 + if ledgers_waited == MAX_LEDGERS_WAITED: + raise Exception("Bridge transfer failed.") diff --git a/snippets/paths.py b/snippets/paths.py index b26581d6d..4161b6bb3 100644 --- a/snippets/paths.py +++ b/snippets/paths.py @@ -36,7 +36,7 @@ paths = path_response.result["alternatives"][0]["paths_computed"] print(paths) -# # Create a Payment to send money from wallet to destination_account using path +# Create a Payment to send money from wallet to destination_account using path payment_tx = Payment( account=wallet.address, amount=destination_amount, diff --git a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json index f5b5a5976..fa30a927e 100644 --- a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json +++ b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json @@ -4450,6 +4450,189 @@ "Sequence": 62 } }, + { + "binary": "1200302200000000240000000168400000000000000A601D40000000000003E8601E400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020744630440220101BCA4B5B5A37C6F44480F9A34752C9AA8B2CDF5AD47E3CB424DEDC21C06DB702206EEB257E82A89B1F46A0A2C7F070B0BD181D980FF86FE4269E369F6FC7A270918114B5F762798A53D543A014CAF8B297CFF8F2F937E8011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000", + "json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"} + }, + "Fee": "10", + "Flags": 0, + "MinAccountCreateAmount": "10000", + "Sequence": 1, + "SignatureReward": "1000", + "SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", + "TransactionType": "XChainCreateBridge", + "TxnSignature": "30440220101BCA4B5B5A37C6F44480F9A34752C9AA8B2CDF5AD47E3CB424DEDC21C06DB702206EEB257E82A89B1F46A0A2C7F070B0BD181D980FF86FE4269E369F6FC7A27091" + } + }, + { + "binary": "12002F2200000000240000000168400000000000000A601D40000000000003E8601E400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD02074473045022100D2CABC1B0E0635A8EE2E6554F6D474C49BC292C995C5C9F83179F4A60634B04C02205D1DB569D9593136F2FBEA7140010C8F46794D653AFDBEA8D30B8750BA4805E58114B5F762798A53D543A014CAF8B297CFF8F2F937E8011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000", + "json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"} + }, + "Fee": "10", + "Flags": 0, + "MinAccountCreateAmount": "10000", + "Sequence": 1, + "SignatureReward": "1000", + "SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", + "TransactionType": "XChainModifyBridge", + "TxnSignature": "3045022100D2CABC1B0E0635A8EE2E6554F6D474C49BC292C995C5C9F83179F4A60634B04C02205D1DB569D9593136F2FBEA7140010C8F46794D653AFDBEA8D30B8750BA4805E5" + } + }, + { + "binary": "1200292280000000240000000168400000000000000A601D400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020744630440220247B20A1B9C48E21A374CB9B3E1FE2A7C528151868DF8D307E9FBE15237E531A02207C20C092DDCC525E583EF4AB7CB91E862A6DED19426997D3F0A2C84E2BE8C5DD8114B5F762798A53D543A014CAF8B297CFF8F2F937E8801214AF80285F637EE4AF3C20378F9DFB12511ACB8D27011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000", + "json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"} + }, + "Fee": "10", + "Flags": 2147483648, + "OtherChainSource": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "Sequence": 1, + "SignatureReward": "10000", + "SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", + "TransactionType": "XChainCreateClaimID", + "TxnSignature": "30440220247B20A1B9C48E21A374CB9B3E1FE2A7C528151868DF8D307E9FBE15237E531A02207C20C092DDCC525E583EF4AB7CB91E862A6DED19426997D3F0A2C84E2BE8C5DD" + } + }, + { + "binary": "12002A228000000024000000013014000000000000000161400000000000271068400000000000000A73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD02074453043021F177323F0D93612C82A4393A99B23905A7E675753FD80C52997AFAB13F5F9D002203BFFAF457E90BDA65AABE8F8762BD96162FAD98A0C030CCD69B06EE9B12BBFFE8114B5F762798A53D543A014CAF8B297CFF8F2F937E8011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000", + "json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "10000", + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"} + }, + "Fee": "10", + "Flags": 2147483648, + "Sequence": 1, + "SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", + "TransactionType": "XChainCommit", + "TxnSignature": "3043021F177323F0D93612C82A4393A99B23905A7E675753FD80C52997AFAB13F5F9D002203BFFAF457E90BDA65AABE8F8762BD96162FAD98A0C030CCD69B06EE9B12BBFFE", + "XChainClaimID": "0000000000000001" + } + }, + { + "binary": "12002B228000000024000000013014000000000000000161400000000000271068400000000000000A73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020744630440220445F7469FDA401787D9EE8A9B6E24DFF81E94F4C09FD311D2C0A58FCC02C684A022029E2EF34A5EA35F50D5BB57AC6320AD3AE12C13C8D1379B255A486D72CED142E8114B5F762798A53D543A014CAF8B297CFF8F2F937E88314550FC62003E785DC231A1058A05E56E3F09CF4E6011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000", + "json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "10000", + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"} + }, + "Destination": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "Fee": "10", + "Flags": 2147483648, + "Sequence": 1, + "SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", + "TransactionType": "XChainClaim", + "TxnSignature": "30440220445F7469FDA401787D9EE8A9B6E24DFF81E94F4C09FD311D2C0A58FCC02C684A022029E2EF34A5EA35F50D5BB57AC6320AD3AE12C13C8D1379B255A486D72CED142E", + "XChainClaimID": "0000000000000001" + } + }, + { + "binary": "12002C228000000024000000016140000000000F424068400000000000000A601D400000000000271073210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD0207446304402202984DDE7F0B566F081F7953D7212BF031ACBF8860FE114102E9512C4C8768C77022070113F4630B1DC3045E4A98DDD648CEBC31B12774F7B44A1B8123CD2C9F5CF188114B5F762798A53D543A014CAF8B297CFF8F2F937E88314AF80285F637EE4AF3C20378F9DFB12511ACB8D27011914AF80285F637EE4AF3C20378F9DFB12511ACB8D27000000000000000000000000000000000000000014550FC62003E785DC231A1058A05E56E3F09CF4E60000000000000000000000000000000000000000", + "json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"} + }, + "Amount": "1000000", + "Fee": "10", + "Flags": 2147483648, + "Destination": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "Sequence": 1, + "SignatureReward": "10000", + "SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020", + "TransactionType": "XChainAccountCreateCommit", + "TxnSignature": "304402202984DDE7F0B566F081F7953D7212BF031ACBF8860FE114102E9512C4C8768C77022070113F4630B1DC3045E4A98DDD648CEBC31B12774F7B44A1B8123CD2C9F5CF18" + } + }, + { + "binary": "12002E2400000005201B0000000D30150000000000000006614000000000989680684000000000000014601D40000000000000647121ED1F4A024ACFEBDB6C7AA88DEDE3364E060487EA31B14CC9E0D610D152B31AADC27321EDF54108BA2E0A0D3DC2AE3897F8BE0EFE776AE8D0F9FB0D0B9D64233084A8DDD1744003E74AEF1F585F156786429D2FC87A89E5C6B5A56D68BFC9A6A329F3AC67CBF2B6958283C663A4522278CA162C69B23CF75149AF022B410EA0508C16F42058007640EEFCFA3DC2AB4AB7C4D2EBBC168CB621A11B82BABD86534DFC8EFA72439A49662D744073CD848E7A587A95B35162CDF9A69BB237E72C9537A987F5B8C394F30D81145E7A3E3D7200A794FA801C66CE3775B6416EE4128314C15F113E49BCC4B9FFF43CD0366C23ACD82F75638012143FD9ED9A79DEA67CB5D585111FEF0A29203FA0408015145E7A3E3D7200A794FA801C66CE3775B6416EE4120010130101191486F0B1126CE1205E59FDFDD2661A9FB7505CA70F000000000000000000000000000000000000000014B5F762798A53D543A014CAF8B297CFF8F2F937E80000000000000000000000000000000000000000", + "json": { + "Account": "r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT", + "Amount": "10000000", + "AttestationRewardAccount": "r9cYxdjQsoXAEz3qQJc961SNLaXRkWXCvT", + "Destination": "rJdTJRJZ6GXCCRaamHJgEqVzB7Zy4557Pi", + "Fee": "20", + "LastLedgerSequence": 13, + "OtherChainSource": "raFcdz1g8LWJDJWJE2ZKLRGdmUmsTyxaym", + "PublicKey": "ED1F4A024ACFEBDB6C7AA88DEDE3364E060487EA31B14CC9E0D610D152B31AADC2", + "Sequence": 5, + "Signature": "EEFCFA3DC2AB4AB7C4D2EBBC168CB621A11B82BABD86534DFC8EFA72439A49662D744073CD848E7A587A95B35162CDF9A69BB237E72C9537A987F5B8C394F30D", + "SignatureReward": "100", + "SigningPubKey": "EDF54108BA2E0A0D3DC2AE3897F8BE0EFE776AE8D0F9FB0D0B9D64233084A8DDD1", + "TransactionType": "XChainAddAccountCreateAttestation", + "TxnSignature": "03E74AEF1F585F156786429D2FC87A89E5C6B5A56D68BFC9A6A329F3AC67CBF2B6958283C663A4522278CA162C69B23CF75149AF022B410EA0508C16F4205800", + "WasLockingChainSend": 1, + "XChainAccountCreateCount": "0000000000000006", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rDJVtEuDKr4rj1B3qtW7R5TVWdXV2DY7Qg", + "LockingChainIssue": { + "currency": "XRP" + } + } + } + }, + { + "binary": "12002D2400000009201B00000013301400000000000000016140000000009896806840000000000000147121ED7541DEC700470F54276C90C333A13CDBB5D341FD43C60CEA12170F6D6D4E11367321ED0406B134786FE0751717226657F7BF8AFE96442C05D28ACEC66FB64852BA604C7440D0423649E48A44F181262CF5FC08A68E7FA5CD9E55843E4F09014B76E602574741E8553383A4B43CABD194BB96713647FC0B885BE248E4FFA068FA3E6994CF0476407C175050B08000AD35EEB2D87E16CD3F95A0AEEBF2A049474275153D9D4DD44528FE99AA50E71660A15B0B768E1B90E609BBD5DC7AFAFD45D9705D72D40EA10C81141F30A4D728AB98B0950EC3B9815E6C8D43A7D5598314C15F113E49BCC4B9FFF43CD0366C23ACD82F75638012143FD9ED9A79DEA67CB5D585111FEF0A29203FA0408015141F30A4D728AB98B0950EC3B9815E6C8D43A7D5590010130101191486F0B1126CE1205E59FDFDD2661A9FB7505CA70F000000000000000000000000000000000000000014B5F762798A53D543A014CAF8B297CFF8F2F937E80000000000000000000000000000000000000000", + "json": { + "Account": "rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3", + "Amount": "10000000", + "AttestationRewardAccount": "rsqvD8WFFEBBv4nztpoW9YYXJ7eRzLrtc3", + "Destination": "rJdTJRJZ6GXCCRaamHJgEqVzB7Zy4557Pi", + "Fee": "20", + "LastLedgerSequence": 19, + "OtherChainSource": "raFcdz1g8LWJDJWJE2ZKLRGdmUmsTyxaym", + "PublicKey": "ED7541DEC700470F54276C90C333A13CDBB5D341FD43C60CEA12170F6D6D4E1136", + "Sequence": 9, + "Signature": "7C175050B08000AD35EEB2D87E16CD3F95A0AEEBF2A049474275153D9D4DD44528FE99AA50E71660A15B0B768E1B90E609BBD5DC7AFAFD45D9705D72D40EA10C", + "SigningPubKey": "ED0406B134786FE0751717226657F7BF8AFE96442C05D28ACEC66FB64852BA604C", + "TransactionType": "XChainAddClaimAttestation", + "TxnSignature": "D0423649E48A44F181262CF5FC08A68E7FA5CD9E55843E4F09014B76E602574741E8553383A4B43CABD194BB96713647FC0B885BE248E4FFA068FA3E6994CF04", + "WasLockingChainSend": 1, + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rDJVtEuDKr4rj1B3qtW7R5TVWdXV2DY7Qg", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "XChainClaimID": "0000000000000001" + } + }, { "binary": "12002315000A2200000000240015DAE161400000000000271068400000000000000A6BD5838D7EA4C680000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A", "json": { diff --git a/tests/unit/core/binarycodec/test_main.py b/tests/unit/core/binarycodec/test_main.py index 0e345d6b3..6c6ead083 100644 --- a/tests/unit/core/binarycodec/test_main.py +++ b/tests/unit/core/binarycodec/test_main.py @@ -331,14 +331,14 @@ def test_xaddress_xaddr_and_matching_source_tag(self): class TestMainFixtures(TestCase): - maxDiff = 1000 + maxDiff = None def _check_binary_and_json(self, test): test_binary = test["binary"] test_json = test["json"] with self.subTest(test_binary=test_binary, test_json=test_json): - self.assertEqual(encode(test_json), test_binary) self.assertEqual(decode(test_binary), test_json) + self.assertEqual(encode(test_json), test_binary) def _check_xaddress_jsons(self, test): x_json = test["xjson"] diff --git a/tests/unit/models/requests/test_ledger_entry.py b/tests/unit/models/requests/test_ledger_entry.py index 57ec98e4b..513ef9d1e 100644 --- a/tests/unit/models/requests/test_ledger_entry.py +++ b/tests/unit/models/requests/test_ledger_entry.py @@ -1,7 +1,7 @@ from unittest import TestCase +from xrpl.models import XRP, LedgerEntry, XChainBridge from xrpl.models.exceptions import XRPLModelException -from xrpl.models.requests import LedgerEntry from xrpl.models.requests.ledger_entry import RippleState @@ -69,6 +69,46 @@ def test_has_only_ticket_is_valid(self): ) self.assertTrue(req.is_valid()) + def test_has_only_xchain_claim_id_is_valid(self): + req = LedgerEntry( + xchain_claim_id=1, + ) + self.assertTrue(req.is_valid()) + + def test_has_only_xchain_create_account_claim_id_is_valid(self): + req = LedgerEntry( + xchain_create_account_claim_id=1, + ) + self.assertTrue(req.is_valid()) + + def test_has_both_bridge_fields_is_valid(self): + req = LedgerEntry( + bridge=XChainBridge( + locking_chain_door="rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + locking_chain_issue=XRP(), + issuing_chain_door="r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + issuing_chain_issue=XRP(), + ), + bridge_account="rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + ) + self.assertTrue(req.is_valid()) + + def test_missing_bridge_field_is_invalid(self): + with self.assertRaises(XRPLModelException): + LedgerEntry( + bridge=XChainBridge( + locking_chain_door="rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + locking_chain_issue=XRP(), + issuing_chain_door="r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + issuing_chain_issue=XRP(), + ), + ) + + with self.assertRaises(XRPLModelException): + LedgerEntry( + bridge_account="rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + ) + def test_has_no_query_param_is_invalid(self): with self.assertRaises(XRPLModelException): LedgerEntry() diff --git a/tests/unit/models/requests/test_sign.py b/tests/unit/models/requests/test_sign.py index 10b9a3cba..abf274967 100644 --- a/tests/unit/models/requests/test_sign.py +++ b/tests/unit/models/requests/test_sign.py @@ -3,7 +3,7 @@ from xrpl.constants import CryptoAlgorithm from xrpl.models.exceptions import XRPLModelException from xrpl.models.requests import Sign -from xrpl.models.transactions import AccountSet +from xrpl.models.transactions import AccountSet, AccountSetAsfFlag _ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" _FEE = "0.00001" @@ -12,7 +12,7 @@ _TRANSACTION = AccountSet( account=_ACCOUNT, fee=_FEE, - set_flag=3, + set_flag=AccountSetAsfFlag.ASF_DISALLOW_XRP, domain=_DOMAIN, sequence=_SEQUENCE, ) diff --git a/tests/unit/models/test_base_model.py b/tests/unit/models/test_base_model.py index ceaec1a03..7cea1d9c6 100644 --- a/tests/unit/models/test_base_model.py +++ b/tests/unit/models/test_base_model.py @@ -27,8 +27,10 @@ SignerListSet, TrustSet, TrustSetFlag, + XChainClaim, ) from xrpl.models.transactions.transaction import Transaction +from xrpl.models.xchain_bridge import XChainBridge currency = "BTC" value = "100" @@ -669,3 +671,34 @@ def test_to_xrpl_auth_accounts(self): "Flags": 0, } self.assertEqual(tx.to_xrpl(), expected) + + def test_to_from_xrpl_xchain(self): + tx_json = { + "Account": account, + "Amount": value, + "XChainBridge": { + "LockingChainDoor": "rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + "LockingChainIssue": {"currency": "XRP"}, + "IssuingChainDoor": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "IssuingChainIssue": {"currency": "XRP"}, + }, + "Destination": destination, + "TransactionType": "XChainClaim", + "Flags": 0, + "SigningPubKey": "", + "XChainClaimID": 1, + } + tx_obj = XChainClaim( + account=account, + amount=value, + xchain_bridge=XChainBridge( + locking_chain_door="rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL", + locking_chain_issue=XRP(), + issuing_chain_door="r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + issuing_chain_issue=XRP(), + ), + destination=destination, + xchain_claim_id=1, + ) + self.assertEqual(tx_obj.to_xrpl(), tx_json) + self.assertEqual(Transaction.from_xrpl(tx_json), tx_obj) diff --git a/tests/unit/models/transactions/test_xchain_account_create_commit.py b/tests/unit/models/transactions/test_xchain_account_create_commit.py new file mode 100644 index 000000000..26651610c --- /dev/null +++ b/tests/unit/models/transactions/test_xchain_account_create_commit.py @@ -0,0 +1,61 @@ +from unittest import TestCase + +from xrpl.models import ( + XRP, + IssuedCurrency, + XChainAccountCreateCommit, + XChainBridge, + XRPLModelException, +) + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ACCOUNT2 = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo" + +_ISSUER = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf" + +_GENESIS = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + +_XRP_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), +) + +_IOU_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT2, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT2), +) + + +class TestXChainAccountCreateCommit(TestCase): + def test_successful(self): + XChainAccountCreateCommit( + account=_ACCOUNT, + xchain_bridge=_XRP_BRIDGE, + signature_reward="200", + destination=_ACCOUNT2, + amount="1000000", + ) + + def test_bad_signature_reward(self): + with self.assertRaises(XRPLModelException): + XChainAccountCreateCommit( + account=_ACCOUNT, + xchain_bridge=_XRP_BRIDGE, + signature_reward="hello", + destination=_ACCOUNT2, + amount="1000000", + ) + + def test_bad_amount(self): + with self.assertRaises(XRPLModelException): + XChainAccountCreateCommit( + account=_ACCOUNT, + xchain_bridge=_XRP_BRIDGE, + signature_reward="200", + destination=_ACCOUNT2, + amount="hello", + ) diff --git a/tests/unit/models/transactions/test_xchain_claim.py b/tests/unit/models/transactions/test_xchain_claim.py new file mode 100644 index 000000000..82f9e690d --- /dev/null +++ b/tests/unit/models/transactions/test_xchain_claim.py @@ -0,0 +1,108 @@ +from unittest import TestCase + +from xrpl.models import ( + XRP, + IssuedCurrency, + IssuedCurrencyAmount, + XChainBridge, + XChainClaim, + XRPLModelException, +) + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ACCOUNT2 = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo" +_FEE = "0.00001" +_SEQUENCE = 19048 + +_ISSUER = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf" +_GENESIS = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + +_DESTINATION = "rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN" +_CLAIM_ID = 3 +_XRP_AMOUNT = "123456789" +_IOU_AMOUNT = IssuedCurrencyAmount(currency="USD", issuer=_ISSUER, value="123") + +_XRP_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), +) + +_IOU_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT2, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT2), +) + + +class TestXChainClaim(TestCase): + def test_successful_claim_xrp(self): + XChainClaim( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + xchain_claim_id=_CLAIM_ID, + destination=_DESTINATION, + amount=_XRP_AMOUNT, + ) + + def test_successful_claim_iou(self): + XChainClaim( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_IOU_BRIDGE, + xchain_claim_id=_CLAIM_ID, + destination=_DESTINATION, + amount=_IOU_AMOUNT, + ) + + def test_successful_claim_destination_tag(self): + XChainClaim( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + xchain_claim_id=_CLAIM_ID, + destination=_DESTINATION, + destination_tag="12345", + amount=_XRP_AMOUNT, + ) + + def test_successful_claim_str_claim_id(self): + XChainClaim( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + xchain_claim_id=str(_CLAIM_ID), + destination=_DESTINATION, + amount=_XRP_AMOUNT, + ) + + def test_xrp_bridge_iou_amount(self): + with self.assertRaises(XRPLModelException): + XChainClaim( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + xchain_claim_id=_CLAIM_ID, + destination=_DESTINATION, + amount=_IOU_AMOUNT, + ) + + def test_iou_bridge_xrp_amount(self): + with self.assertRaises(XRPLModelException): + XChainClaim( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_IOU_BRIDGE, + xchain_claim_id=_CLAIM_ID, + destination=_DESTINATION, + amount=_XRP_AMOUNT, + ) diff --git a/tests/unit/models/transactions/test_xchain_create_bridge.py b/tests/unit/models/transactions/test_xchain_create_bridge.py new file mode 100644 index 000000000..47e3f49c7 --- /dev/null +++ b/tests/unit/models/transactions/test_xchain_create_bridge.py @@ -0,0 +1,166 @@ +from unittest import TestCase + +from xrpl.models import ( + XRP, + IssuedCurrency, + XChainBridge, + XChainCreateBridge, + XRPLModelException, +) + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ACCOUNT2 = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo" +_FEE = "0.00001" +_SEQUENCE = 19048 + +_ISSUER = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf" + +_GENESIS = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + + +class TestXChainCreateBridge(TestCase): + def test_successful_xrp_xrp_bridge(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), + ) + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + min_account_create_amount="1000000", + ) + + def test_successful_iou_iou_bridge(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT2, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT2), + ) + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + ) + + def test_same_door_accounts(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + ) + + def test_xrp_iou_bridge(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_ACCOUNT, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + ) + + def test_iou_xrp_bridge(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT, + issuing_chain_issue=XRP(), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + ) + + def test_account_not_in_bridge(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_ACCOUNT2, + issuing_chain_issue=XRP(), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_GENESIS, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + ) + + def test_iou_iou_min_account_create_amount(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT2, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT2), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="200", + min_account_create_amount="1000000", + ) + + def test_invalid_signature_reward(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="hello", + min_account_create_amount="1000000", + ) + + def test_invalid_min_account_create_amount(self): + bridge = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), + ) + with self.assertRaises(XRPLModelException): + XChainCreateBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=bridge, + signature_reward="-200", + min_account_create_amount="hello", + ) diff --git a/tests/unit/models/transactions/test_xchain_create_claim_id.py b/tests/unit/models/transactions/test_xchain_create_claim_id.py new file mode 100644 index 000000000..589646b23 --- /dev/null +++ b/tests/unit/models/transactions/test_xchain_create_claim_id.py @@ -0,0 +1,47 @@ +from unittest import TestCase + +from xrpl.models import XRP, XChainBridge, XChainCreateClaimID, XRPLModelException + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ACCOUNT2 = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo" + +_ISSUER = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf" +_GENESIS = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + +_SOURCE = "rJrRMgiRgrU6hDF4pgu5DXQdWyPbY35ErN" +_SIGNATURE_REWARD = "200" + +_XRP_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), +) + + +class TestXChainCreateClaimID(TestCase): + def test_successful(self): + XChainCreateClaimID( + account=_ACCOUNT, + xchain_bridge=_XRP_BRIDGE, + signature_reward=_SIGNATURE_REWARD, + other_chain_source=_SOURCE, + ) + + def test_bad_signature_reward(self): + with self.assertRaises(XRPLModelException): + XChainCreateClaimID( + account=_ACCOUNT, + xchain_bridge=_XRP_BRIDGE, + signature_reward="hello", + other_chain_source=_SOURCE, + ) + + def test_bad_other_chain_source(self): + with self.assertRaises(XRPLModelException): + XChainCreateClaimID( + account=_ACCOUNT, + xchain_bridge=_XRP_BRIDGE, + signature_reward=_SIGNATURE_REWARD, + other_chain_source="hello", + ) diff --git a/tests/unit/models/transactions/test_xchain_modify_bridge.py b/tests/unit/models/transactions/test_xchain_modify_bridge.py new file mode 100644 index 000000000..63177024b --- /dev/null +++ b/tests/unit/models/transactions/test_xchain_modify_bridge.py @@ -0,0 +1,113 @@ +from unittest import TestCase + +from xrpl.models import ( + XRP, + IssuedCurrency, + XChainBridge, + XChainModifyBridge, + XRPLModelException, +) + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ACCOUNT2 = "rpZc4mVfWUif9CRoHRKKcmhu1nx2xktxBo" +_FEE = "0.00001" +_SEQUENCE = 19048 + +_ISSUER = "rGWrZyQqhTp9Xu7G5Pkayo7bXjH4k4QYpf" + +_GENESIS = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + +_XRP_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=XRP(), + issuing_chain_door=_GENESIS, + issuing_chain_issue=XRP(), +) + +_IOU_BRIDGE = XChainBridge( + locking_chain_door=_ACCOUNT, + locking_chain_issue=IssuedCurrency(currency="USD", issuer=_ISSUER), + issuing_chain_door=_ACCOUNT2, + issuing_chain_issue=IssuedCurrency(currency="USD", issuer=_ACCOUNT2), +) + + +class TestXChainModifyBridge(TestCase): + def test_successful_modify_bridge(self): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + signature_reward="200", + min_account_create_amount="1000000", + ) + + def test_successful_modify_bridge_only_signature_reward(self): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_IOU_BRIDGE, + signature_reward="200", + ) + + def test_successful_modify_bridge_only_min_account_create_amount(self): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + min_account_create_amount="1000000", + ) + + def test_modify_bridge_empty(self): + with self.assertRaises(XRPLModelException): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_IOU_BRIDGE, + ) + + def test_account_not_in_bridge(self): + with self.assertRaises(XRPLModelException): + XChainModifyBridge( + account=_ACCOUNT2, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + signature_reward="200", + ) + + def test_iou_iou_min_account_create_amount(self): + with self.assertRaises(XRPLModelException): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_IOU_BRIDGE, + min_account_create_amount="1000000", + ) + + def test_invalid_signature_reward(self): + with self.assertRaises(XRPLModelException): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + signature_reward="hello", + min_account_create_amount="1000000", + ) + + def test_invalid_min_account_create_amount(self): + with self.assertRaises(XRPLModelException): + XChainModifyBridge( + account=_ACCOUNT, + fee=_FEE, + sequence=_SEQUENCE, + xchain_bridge=_XRP_BRIDGE, + signature_reward="200", + min_account_create_amount="hello", + ) diff --git a/tests/unit/utils/test_get_xchain_claim_id.py b/tests/unit/utils/test_get_xchain_claim_id.py new file mode 100644 index 000000000..e91fb71b0 --- /dev/null +++ b/tests/unit/utils/test_get_xchain_claim_id.py @@ -0,0 +1,35 @@ +"""Test the get_xchain_claim_id util.""" +from __future__ import annotations + +import json +from unittest import TestCase + +from xrpl.utils import get_xchain_claim_id + +path_to_json = "tests/unit/utils/txn_parser/transaction_jsons/" +with open(path_to_json + "XChainCreateClaimID.json", "r") as f: + fixture = json.load(f) +with open(path_to_json + "XChainCreateClaimID2.json", "r") as f: + fixture2 = json.load(f) +with open(path_to_json + "nftokenmint_response1.json", "r") as f: + wrong_fixture = json.load(f) + + +class TestGetXChainClaimID(TestCase): + """Test get_xchain_claim_id.""" + + def test_decoding_a_valid_xchain_claim_id(self: TestGetXChainClaimID): + result = get_xchain_claim_id(fixture["meta"]) + expected_xchain_claim_id = "b0" + self.assertEqual(result, expected_xchain_claim_id) + + def test_a_different_valid_xchain_claim_id(self: TestGetXChainClaimID): + result = get_xchain_claim_id(fixture2["meta"]) + expected_xchain_claim_id = "ac" + self.assertEqual(result, expected_xchain_claim_id) + + def test_error_with_wrong_tx_metadata(self: TestGetXChainClaimID) -> None: + self.assertRaises(TypeError, lambda: get_xchain_claim_id(wrong_fixture["meta"])) + + def test_error_with_raw_instead_of_meta(self: TestGetXChainClaimID) -> None: + self.assertRaises(TypeError, lambda: get_xchain_claim_id(fixture)) diff --git a/tests/unit/utils/txn_parser/transaction_jsons/XChainCreateClaimID.json b/tests/unit/utils/txn_parser/transaction_jsons/XChainCreateClaimID.json new file mode 100644 index 000000000..533f14681 --- /dev/null +++ b/tests/unit/utils/txn_parser/transaction_jsons/XChainCreateClaimID.json @@ -0,0 +1,118 @@ +{ + "tx": { + "Account": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd", + "Fee": "20", + "Flags": 2147483648, + "NetworkID": 2552, + "OtherChainSource": "rL5Zd9m5XEoGPddMwYY5H5C8ARcR47b6oM", + "Sequence": 1007784, + "SignatureReward": "100", + "SigningPubKey": "039E925058C740A5B73E49300FC205D058520DE37F2C63C4EE3A0D1B50C4E44080", + "TransactionType": "XChainCreateClaimID", + "TxnSignature": "304402201C6F95B9997FB63DCD9854664707C58C46AA3207612FE32366B77DA084786CAF02205752C58821D7FAFAE26F77DC10AC0AFDDCBCCF4FCBED90E6B8C4523A0EB3E008", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "date": 1695324353000 + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Flags": 0, + "MinAccountCreateAmount": "10000000", + "OwnerNode": "0", + "SignatureReward": "100", + "XChainAccountClaimCount": "e3", + "XChainAccountCreateCount": "0", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "XChainClaimID": "b0" + }, + "LedgerEntryType": "Bridge", + "LedgerIndex": "114C0DC89656D1B0FB1F4A3426034C3FCE75BCE65D9574B5D96ABC2B24D6C8F1", + "PreviousFields": { + "XChainClaimID": "af" + }, + "PreviousTxnID": "3F6F3BBE584115D1A575AB24BA32B47184F2323B65DE5C8C8EE144A55115E0B9", + "PreviousTxnLgrSeq": 1027822 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Flags": 0, + "Owner": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd", + "RootIndex": "6C1EA1A93D590E831CCC0EE2CBE26C146A3A6FD36F5854DC5E5AB5CE78FAE49C" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "6C1EA1A93D590E831CCC0EE2CBE26C146A3A6FD36F5854DC5E5AB5CE78FAE49C" + } + }, + { + "CreatedNode": { + "LedgerEntryType": "XChainOwnedClaimID", + "LedgerIndex": "A00BD77AE864509D796B39041AD48E9DEFEC9AF20E5C09CEF2F5DA41D6CFEB1E", + "NewFields": { + "Account": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd", + "OtherChainSource": "rL5Zd9m5XEoGPddMwYY5H5C8ARcR47b6oM", + "SignatureReward": "100", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "XChainClaimID": "b0" + } + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rLVUz66tawieqTPAHuTyFTN6pLbHcXiTzd", + "Balance": "39999940", + "Flags": 0, + "OwnerCount": 3, + "Sequence": 1007785 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "FD919D0BAA90C759DA4C7130AEEF6AE7FA2AF074F5E867D40BCBE1ECD8D8D0EA", + "PreviousFields": { + "Balance": "39999960", + "OwnerCount": 2, + "Sequence": 1007784 + }, + "PreviousTxnID": "3F6F3BBE584115D1A575AB24BA32B47184F2323B65DE5C8C8EE144A55115E0B9", + "PreviousTxnLgrSeq": 1027822 + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "hash": "998E76B9840DA5A6009592A2674D0166A9C4862193193AA46EA6B77A64781FB4", + "ledger_index": 1027837, + "date": 1695324353000 + } diff --git a/tests/unit/utils/txn_parser/transaction_jsons/XChainCreateClaimID2.json b/tests/unit/utils/txn_parser/transaction_jsons/XChainCreateClaimID2.json new file mode 100644 index 000000000..b69cacfe9 --- /dev/null +++ b/tests/unit/utils/txn_parser/transaction_jsons/XChainCreateClaimID2.json @@ -0,0 +1,118 @@ +{ + "tx": { + "Account": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv", + "Fee": "12", + "Flags": 0, + "LastLedgerSequence": 1027798, + "NetworkID": 2552, + "OtherChainSource": "rBXdfZ7NVpdjRfYajPMpviGgq7HLDeuBdR", + "Sequence": 1027778, + "SignatureReward": "100", + "SigningPubKey": "EDDDD69DF802B8DB82D644EF92E2C1F06AC128A275CDFF86F013180D104ED39D3B", + "TransactionType": "XChainCreateClaimID", + "TxnSignature": "67BE63527EC8A0C872F23E2C4EB97C1F3E7D3FED6D10C8310B9235D3891B6B9343768A080E258F6C3687BFC4B7C5FD429ABB33654C99DE46471FD6F2A7035303", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "date": 1695324182000 + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Flags": 0, + "MinAccountCreateAmount": "10000000", + "OwnerNode": "0", + "SignatureReward": "100", + "XChainAccountClaimCount": "e2", + "XChainAccountCreateCount": "0", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "XChainClaimID": "ac" + }, + "LedgerEntryType": "Bridge", + "LedgerIndex": "114C0DC89656D1B0FB1F4A3426034C3FCE75BCE65D9574B5D96ABC2B24D6C8F1", + "PreviousFields": { + "XChainClaimID": "ab" + }, + "PreviousTxnID": "80C33D1FB349D698CFDB1A85E8368557C5B7219B74DFCB2B05E0B10E2667F902", + "PreviousTxnLgrSeq": 1027779 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv", + "Balance": "19999988", + "Flags": 0, + "OwnerCount": 1, + "Sequence": 1027779 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "33442CE111B258424548888D8999F6D064A0866B1300C44AB72E1C5A09765D9D", + "PreviousFields": { + "Balance": "20000000", + "OwnerCount": 0, + "Sequence": 1027778 + }, + "PreviousTxnID": "7C9ACA230488547B4F39EBCE332447FB90AE59B64C1B03BBF474B509B43739EC", + "PreviousTxnLgrSeq": 1027778 + } + }, + { + "CreatedNode": { + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "439684B06C22596B5B86D2F50903B6AA6F68BD07BED636FC6325704B09DE5D61", + "NewFields": { + "Owner": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv", + "RootIndex": "439684B06C22596B5B86D2F50903B6AA6F68BD07BED636FC6325704B09DE5D61" + } + } + }, + { + "CreatedNode": { + "LedgerEntryType": "XChainOwnedClaimID", + "LedgerIndex": "8097863E1200B0174006541763AA8F604782DA10C1BD37190D753C699D69C678", + "NewFields": { + "Account": "rwmUSzi5Xp31AjMTEdbvxgWqLETcVNU6Fv", + "OtherChainSource": "rBXdfZ7NVpdjRfYajPMpviGgq7HLDeuBdR", + "SignatureReward": "100", + "XChainBridge": { + "IssuingChainDoor": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "IssuingChainIssue": { + "currency": "XRP" + }, + "LockingChainDoor": "rMAXACCrp3Y8PpswXcg3bKggHX76V3F8M4", + "LockingChainIssue": { + "currency": "XRP" + } + }, + "XChainClaimID": "ac" + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "hash": "A42C4E7F5BAF8A9BEB56853114EE686D554F15F400B8DA885A344B13C32D07BC", + "ledger_index": 1027780, + "date": 1695324182000 +} diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 1ab810e44..b6b48f440 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -22,6 +22,7 @@ "UInt384": 22, "UInt512": 23, "Issue": 24, + "XChainBridge": 25, "Transaction": 10001, "LedgerEntry": 10002, "Validation": 10003, @@ -35,8 +36,11 @@ "Ticket": 84, "SignerList": 83, "Offer": 111, + "Bridge": 105, "LedgerHashes": 104, "Amendments": 102, + "XChainOwnedClaimID": 113, + "XChainOwnedCreateAccountClaimID": 116, "FeeSettings": 115, "Escrow": 117, "PayChannel": 120, @@ -233,6 +237,16 @@ "type": "UInt8" } ], + [ + "WasLockingChainSend", + { + "nth": 19, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt8" + } + ], [ "LedgerEntryType", { @@ -983,6 +997,36 @@ "type": "UInt64" } ], + [ + "XChainClaimID", + { + "nth": 20, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "XChainAccountCreateCount", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], + [ + "XChainAccountClaimCount", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt64" + } + ], [ "EmailHash", { @@ -1583,6 +1627,26 @@ "type": "Amount" } ], + [ + "SignatureReward", + { + "nth": 29, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "MinAccountCreateAmount", + { + "nth": 30, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "LPTokenBalance", { @@ -1933,6 +1997,66 @@ "type": "AccountID" } ], + [ + "OtherChainSource", + { + "nth": 18, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "OtherChainDestination", + { + "nth": 19, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "AttestationSignerAccount", + { + "nth": 20, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "AttestationRewardAccount", + { + "nth": 21, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "LockingChainDoor", + { + "nth": 22, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], + [ + "IssuingChainDoor", + { + "nth": 23, + "isVLEncoded": true, + "isSerialized": true, + "isSigningField": true, + "type": "AccountID" + } + ], [ "Indexes", { @@ -1983,6 +2107,26 @@ "type": "PathSet" } ], + [ + "LockingChainIssue", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], + [ + "IssuingChainIssue", + { + "nth": 2, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], [ "Asset", { @@ -2003,6 +2147,16 @@ "type": "Issue" } ], + [ + "XChainBridge", + { + "nth": 1, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "XChainBridge" + } + ], [ "TransactionMetaData", { @@ -2243,6 +2397,46 @@ "type": "STObject" } ], + [ + "XChainClaimProofSig", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "XChainCreateAccountProofSig", + { + "nth": 29, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "XChainClaimAttestationCollectionElement", + { + "nth": 30, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "XChainCreateAccountAttestationCollectionElement", + { + "nth": 31, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2393,6 +2587,26 @@ "type": "STArray" } ], + [ + "XChainClaimAttestations", + { + "nth": 21, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], + [ + "XChainCreateAccountAttestations", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "AuthAccounts", { @@ -2461,6 +2675,12 @@ "temSEQ_AND_TICKET": -263, "temBAD_NFTOKEN_TRANSFER_FEE": -262, "temBAD_AMM_TOKENS": -261, + "temXCHAIN_EQUAL_DOOR_ACCOUNTS": -260, + "temXCHAIN_BAD_PROOF": -259, + "temXCHAIN_BRIDGE_BAD_ISSUES": -258, + "temXCHAIN_BRIDGE_NONDOOR_OWNER": -257, + "temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT": -256, + "temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT": -255, "tefFAILURE": -199, "tefALREADY": -198, @@ -2497,6 +2717,7 @@ "terQUEUED": -89, "terPRE_TICKET": -88, "terNO_AMM": -87, + "terSUBMITTED": -86, "tesSUCCESS": 0, @@ -2538,6 +2759,7 @@ "tecKILLED": 150, "tecHAS_OBLIGATIONS": 151, "tecTOO_SOON": 152, + "tecHOOK_ERROR": 153, "tecMAX_SEQUENCE_REACHED": 154, "tecNO_SUITABLE_NFTOKEN_PAGE": 155, "tecNFTOKEN_BUY_SELL_MISMATCH": 156, @@ -2553,7 +2775,24 @@ "tecAMM_EMPTY": 166, "tecAMM_NOT_EMPTY": 167, "tecAMM_ACCOUNT": 168, - "tecINCOMPLETE": 169 + "tecINCOMPLETE": 169, + "tecXCHAIN_BAD_TRANSFER_ISSUE": 170, + "tecXCHAIN_NO_CLAIM_ID": 171, + "tecXCHAIN_BAD_CLAIM_ID": 172, + "tecXCHAIN_CLAIM_NO_QUORUM": 173, + "tecXCHAIN_PROOF_UNKNOWN_KEY": 174, + "tecXCHAIN_CREATE_ACCOUNT_NONXRP_ISSUE": 175, + "tecXCHAIN_WRONG_CHAIN": 176, + "tecXCHAIN_REWARD_MISMATCH": 177, + "tecXCHAIN_NO_SIGNERS_LIST": 178, + "tecXCHAIN_SENDING_ACCOUNT_MISMATCH": 179, + "tecXCHAIN_INSUFF_CREATE_AMOUNT": 180, + "tecXCHAIN_ACCOUNT_CREATE_PAST": 181, + "tecXCHAIN_ACCOUNT_CREATE_TOO_MANY": 182, + "tecXCHAIN_PAYMENT_FAILED": 183, + "tecXCHAIN_SELF_COMMIT": 184, + "tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR": 185, + "tecXCHAIN_CREATE_ACCOUNT_DISABLED": 186 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2592,6 +2831,14 @@ "AMMVote": 38, "AMMBid": 39, "AMMDelete": 40, + "XChainCreateClaimID": 41, + "XChainCommit": 42, + "XChainClaim": 43, + "XChainAccountCreateCommit": 44, + "XChainAddClaimAttestation": 45, + "XChainAddAccountCreateAttestation": 46, + "XChainModifyBridge": 47, + "XChainCreateBridge": 48, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/xrpl/core/binarycodec/definitions/field_header.py b/xrpl/core/binarycodec/definitions/field_header.py index 3a9b05fcb..a2ff0bc4f 100644 --- a/xrpl/core/binarycodec/definitions/field_header.py +++ b/xrpl/core/binarycodec/definitions/field_header.py @@ -48,3 +48,7 @@ def __bytes__(self: FieldHeader) -> bytes: header += [0, self.type_code, self.field_code] return bytes(header) + + def __repr__(self: FieldHeader) -> str: + """Print a string representation of a FieldHeader (for debugging).""" + return f"FieldHeader({self.type_code}, {self.field_code})" diff --git a/xrpl/core/binarycodec/types/__init__.py b/xrpl/core/binarycodec/types/__init__.py index 20ea1b4fb..e9d31a608 100644 --- a/xrpl/core/binarycodec/types/__init__.py +++ b/xrpl/core/binarycodec/types/__init__.py @@ -17,6 +17,7 @@ from xrpl.core.binarycodec.types.uint32 import UInt32 from xrpl.core.binarycodec.types.uint64 import UInt64 from xrpl.core.binarycodec.types.vector256 import Vector256 +from xrpl.core.binarycodec.types.xchain_bridge import XChainBridge __all__ = [ "AccountID", @@ -37,4 +38,5 @@ "UInt32", "UInt64", "Vector256", + "XChainBridge", ] diff --git a/xrpl/core/binarycodec/types/xchain_bridge.py b/xrpl/core/binarycodec/types/xchain_bridge.py new file mode 100644 index 000000000..b71c2bb15 --- /dev/null +++ b/xrpl/core/binarycodec/types/xchain_bridge.py @@ -0,0 +1,99 @@ +"""Codec for serializing and deserializing bridge fields.""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Tuple, Type, Union + +from xrpl.core.binarycodec.binary_wrappers.binary_parser import BinaryParser +from xrpl.core.binarycodec.exceptions import XRPLBinaryCodecException +from xrpl.core.binarycodec.types.account_id import AccountID +from xrpl.core.binarycodec.types.issue import Issue +from xrpl.core.binarycodec.types.serialized_type import SerializedType + +_TYPE_ORDER: List[Tuple[str, Type[SerializedType]]] = [ + ("LockingChainDoor", AccountID), + ("LockingChainIssue", Issue), + ("IssuingChainDoor", AccountID), + ("IssuingChainIssue", Issue), +] + +_TYPE_KEYS = {type[0] for type in _TYPE_ORDER} + + +class XChainBridge(SerializedType): + """Codec for serializing and deserializing bridge fields.""" + + def __init__(self: XChainBridge, buffer: bytes) -> None: + """Construct a XChainBridge from given bytes.""" + super().__init__(buffer) + + @classmethod + def from_value( + cls: Type[XChainBridge], value: Union[str, Dict[str, str]] + ) -> XChainBridge: + """ + Construct a XChainBridge object from a dictionary representation of a bridge. + + Args: + value: The dictionary to construct a XChainBridge object from. + + Returns: + A XChainBridge object constructed from value. + + Raises: + XRPLBinaryCodecException: If the XChainBridge representation is invalid. + """ + if isinstance(value, dict) and set(value.keys()) == _TYPE_KEYS: + buffer = b"" + for name, object_type in _TYPE_ORDER: + obj = object_type.from_value(value[name]) + if object_type == AccountID: + buffer += bytes.fromhex("14") # AccountID length + buffer += bytes(obj) + return cls(buffer) + + raise XRPLBinaryCodecException( + "Invalid type to construct a XChainBridge: expected dict," + f" received {value.__class__.__name__}." + ) + + @classmethod + def from_parser( + cls: Type[XChainBridge], parser: BinaryParser, length_hint: Optional[int] = None + ) -> XChainBridge: + """ + Construct a XChainBridge object from an existing BinaryParser. + + Args: + parser: The parser to construct the XChainBridge object from. + length_hint: The number of bytes to consume from the parser. + + Returns: + The XChainBridge object constructed from a parser. + """ + buffer = b"" + + for _, object_type in _TYPE_ORDER: + if object_type == AccountID: + # skip the `14` byte and add it by hand + parser.skip(1) + buffer += bytes.fromhex("14") + obj = object_type.from_parser(parser, length_hint) + buffer += bytes(obj) + + return cls(buffer) + + def to_json(self: XChainBridge) -> Union[str, Dict[Any, Any]]: + """ + Returns the JSON representation of a bridge. + + Returns: + The JSON representation of a XChainBridge. + """ + parser = BinaryParser(str(self)) + return_json = {} + for name, object_type in _TYPE_ORDER: + if object_type == AccountID: + parser.skip(1) + obj = object_type.from_parser(parser, None) + return_json[name] = obj.to_json() + return return_json diff --git a/xrpl/models/__init__.py b/xrpl/models/__init__.py index bed239c57..8d7d4e63a 100644 --- a/xrpl/models/__init__.py +++ b/xrpl/models/__init__.py @@ -9,6 +9,7 @@ from xrpl.models.response import Response from xrpl.models.transactions import * # noqa: F401, F403 from xrpl.models.transactions.pseudo_transactions import * # noqa: F401, F403 +from xrpl.models.xchain_bridge import XChainBridge __all__ = [ "XRPLModelException", @@ -25,4 +26,5 @@ "Path", "PathStep", "Response", + "XChainBridge", ] diff --git a/xrpl/models/base_model.py b/xrpl/models/base_model.py index 00d9f0808..a4494c2fd 100644 --- a/xrpl/models/base_model.py +++ b/xrpl/models/base_model.py @@ -9,7 +9,7 @@ from enum import Enum from typing import Any, Dict, List, Pattern, Type, TypeVar, Union, cast, get_type_hints -from typing_extensions import Final, get_args, get_origin +from typing_extensions import Final, Literal, get_args, get_origin from xrpl.models.exceptions import XRPLModelException from xrpl.models.required import REQUIRED @@ -41,6 +41,7 @@ "nftoken": "NFToken", "unl": "UNL", "uri": "URI", + "xchain": "XChain", } BM = TypeVar("BM", bound="BaseModel") # any type inherited from BaseModel @@ -176,6 +177,12 @@ def _from_dict_single_param( # expected an object, received the correct object return param_value + if get_origin(param_type) == Literal: + # param_type is Literal (has very specific values it will accept) + if param_value in get_args(param_type): + # param_value is one of the accepted values + return param_value + if ( isinstance(param_type, type) and issubclass(param_type, Enum) diff --git a/xrpl/models/currencies/xrp.py b/xrpl/models/currencies/xrp.py index c3af77328..82dd47746 100644 --- a/xrpl/models/currencies/xrp.py +++ b/xrpl/models/currencies/xrp.py @@ -77,3 +77,12 @@ def to_amount(self: XRP, value: Union[str, int, float]) -> str: if isinstance(value, str): return xrp_to_drops(float(value)) return xrp_to_drops(value) + + def __repr__(self: XRP) -> str: + """ + Generate string representation of XRP. + + Returns: + A string representation of XRP currency. + """ + return "XRP()" diff --git a/xrpl/models/requests/account_objects.py b/xrpl/models/requests/account_objects.py index 4beaba514..b6767e5e4 100644 --- a/xrpl/models/requests/account_objects.py +++ b/xrpl/models/requests/account_objects.py @@ -19,15 +19,18 @@ class AccountObjectType(str, Enum): """Represents the object types that an AccountObjectsRequest can ask for.""" AMM = "amm" + BRIDGE = "bridge" CHECK = "check" DEPOSIT_PREAUTH = "deposit_preauth" ESCROW = "escrow" + NFT_OFFER = "nft_offer" OFFER = "offer" PAYMENT_CHANNEL = "payment_channel" SIGNER_LIST = "signer_list" STATE = "state" TICKET = "ticket" - NFT_OFFER = "nft_offer" + XCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID = "xchain_owned_create_account_claim_id" + XCHAIN_OWNED_CLAIM_ID = "xchain_owned_claim_id" @require_kwargs_on_init diff --git a/xrpl/models/requests/ledger_entry.py b/xrpl/models/requests/ledger_entry.py index dfe8e2084..f696cf8a9 100644 --- a/xrpl/models/requests/ledger_entry.py +++ b/xrpl/models/requests/ledger_entry.py @@ -15,6 +15,7 @@ from xrpl.models.requests.request import LookupByLedgerRequest, Request, RequestMethod from xrpl.models.required import REQUIRED from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge class LedgerEntryType(str, Enum): @@ -133,7 +134,7 @@ class Offer(BaseModel): @require_kwargs_on_init @dataclass(frozen=True) class RippleState(BaseModel): - """Required fields for requesting a RippleState.""" + """Required fields for requesting a RippleState if not querying by object ID.""" accounts: List[str] = REQUIRED # type: ignore """ @@ -153,10 +154,7 @@ class RippleState(BaseModel): @require_kwargs_on_init @dataclass(frozen=True) class Ticket(BaseModel): - """ - Required fields for requesting a Ticket, if not querying by - object ID. - """ + """Required fields for requesting a Ticket if not querying by object ID.""" owner: str = REQUIRED # type: ignore """ @@ -173,6 +171,37 @@ class Ticket(BaseModel): """ +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainClaimID(XChainBridge): + """Required fields for requesting an XChainClaimID if not querying by object ID.""" + + xchain_claim_id: Union[int, str] = REQUIRED # type: ignore + """ + The `XChainClaimID` associated with a cross-chain transfer, which was created in an + `XChainCreateClaimID` transaction. This field is required. + + :meta hide-value: + """ + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainCreateAccountClaimID(XChainBridge): + """ + Required fields for requesting an XChainCreateAccountClaimID if not querying by + object ID. + """ + + xchain_create_account_claim_id: Union[int, str] = REQUIRED # type: ignore + """ + The `XChainCreateAccountClaimID` associated with a cross-chain account create. This + field is required. + + :meta hide-value: + """ + + @require_kwargs_on_init @dataclass(frozen=True) class LedgerEntry(Request, LookupByLedgerRequest): @@ -195,6 +224,13 @@ class LedgerEntry(Request, LookupByLedgerRequest): payment_channel: Optional[str] = None ripple_state: Optional[RippleState] = None ticket: Optional[Union[str, Ticket]] = None + bridge_account: Optional[str] = None + bridge: Optional[XChainBridge] = None + xchain_claim_id: Optional[Union[str, XChainClaimID]] = None + xchain_create_account_claim_id: Optional[ + Union[str, XChainCreateAccountClaimID] + ] = None + binary: bool = False nft_page: Optional[str] = None """Must be the object ID of the NFToken page, as hexadecimal""" @@ -206,17 +242,28 @@ def _get_errors(self: LedgerEntry) -> Dict[str, str]: for param in [ self.index, self.account_root, - self.directory, - self.offer, - self.ripple_state, self.check, + self.deposit_preauth, + self.directory, self.escrow, + self.offer, self.payment_channel, - self.deposit_preauth, + self.ripple_state, self.ticket, + self.xchain_claim_id, + self.xchain_create_account_claim_id, ] if param is not None ] - if len(query_params) != 1: + if ( + len(query_params) != 1 + and self.bridge is None + and self.bridge_account is None + ): errors["LedgerEntry"] = "Must choose exactly one data to query" + + if (self.bridge is None) != (self.bridge_account is None): + # assert that you either have both of these or neither + errors["Bridge"] = "Must include both `bridge` and `bridge_account`." + return errors diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index ef2e7835b..c13a04d6a 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -69,6 +69,24 @@ TrustSetFlag, TrustSetFlagInterface, ) +from xrpl.models.transactions.xchain_account_create_commit import ( + XChainAccountCreateCommit, +) +from xrpl.models.transactions.xchain_add_account_create_attestation import ( + XChainAddAccountCreateAttestation, +) +from xrpl.models.transactions.xchain_add_claim_attestation import ( + XChainAddClaimAttestation, +) +from xrpl.models.transactions.xchain_claim import XChainClaim +from xrpl.models.transactions.xchain_commit import XChainCommit +from xrpl.models.transactions.xchain_create_bridge import XChainCreateBridge +from xrpl.models.transactions.xchain_create_claim_id import XChainCreateClaimID +from xrpl.models.transactions.xchain_modify_bridge import ( + XChainModifyBridge, + XChainModifyBridgeFlag, + XChainModifyBridgeFlagInterface, +) __all__ = [ "AccountDelete", @@ -127,4 +145,14 @@ "TrustSet", "TrustSetFlag", "TrustSetFlagInterface", + "XChainAccountCreateCommit", + "XChainAddAccountCreateAttestation", + "XChainAddClaimAttestation", + "XChainClaim", + "XChainCommit", + "XChainCreateBridge", + "XChainCreateClaimID", + "XChainModifyBridge", + "XChainModifyBridgeFlag", + "XChainModifyBridgeFlagInterface", ] diff --git a/xrpl/models/transactions/metadata.py b/xrpl/models/transactions/metadata.py index bcb225c1d..76e2873dc 100644 --- a/xrpl/models/transactions/metadata.py +++ b/xrpl/models/transactions/metadata.py @@ -34,6 +34,7 @@ class Fields(TypedDict): BookDirectory: NotRequired[str] Expiration: NotRequired[int] NFTokens: NotRequired[List[NFTokenMetadata]] + XChainClaimID: NotRequired[str] class CreatedNodeFields(TypedDict): @@ -96,6 +97,7 @@ class TransactionMetadata(TypedDict): Node: TypeAlias = Union[CreatedNode, ModifiedNode, DeletedNode] +# TODO: make these methods use snake_case def isCreatedNode(node: Node) -> TypeGuard[CreatedNode]: """ Typeguard for CreatedNode diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 691f797ac..c8607cca9 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -37,3 +37,11 @@ class TransactionType(str, Enum): SIGNER_LIST_SET = "SignerListSet" TICKET_CREATE = "TicketCreate" TRUST_SET = "TrustSet" + XCHAIN_ACCOUNT_CREATE_COMMIT = "XChainAccountCreateCommit" + XCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION = "XChainAddAccountCreateAttestation" + XCHAIN_ADD_CLAIM_ATTESTATION = "XChainAddClaimAttestation" + XCHAIN_CLAIM = "XChainClaim" + XCHAIN_COMMIT = "XChainCommit" + XCHAIN_CREATE_BRIDGE = "XChainCreateBridge" + XCHAIN_CREATE_CLAIM_ID = "XChainCreateClaimID" + XCHAIN_MODIFY_BRIDGE = "XChainModifyBridge" diff --git a/xrpl/models/transactions/xchain_account_create_commit.py b/xrpl/models/transactions/xchain_account_create_commit.py new file mode 100644 index 000000000..53994a3a8 --- /dev/null +++ b/xrpl/models/transactions/xchain_account_create_commit.py @@ -0,0 +1,71 @@ +"""Model for a XChainAccountCreateCommit transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict + +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainAccountCreateCommit(Transaction): + """ + Represents a XChainAccountCreateCommit transaction on the XRP Ledger. + The XChainAccountCreateCommit transaction creates a new account on one of + the chains a bridge connects, which serves as the bridge entrance for that + chain. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge to create accounts for. This field is required. + + :meta hide-value: + """ + + signature_reward: str = REQUIRED # type: ignore + """ + The amount, in XRP, to be used to reward the witness servers for providing + signatures. This must match the amount on the ``Bridge`` ledger object. This + field is required. + + :meta hide-value: + """ + + destination: str = REQUIRED # type: ignore + """ + The destination account on the destination chain. This field is required. + + :meta hide-value: + """ + + amount: str = REQUIRED # type: ignore + """ + The amount, in XRP, to use for account creation. This must be greater than + or equal to the ``MinAccountCreateAmount`` specified in the ``Bridge`` + ledger object. This field is required. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_ACCOUNT_CREATE_COMMIT, + init=False, + ) + + def _get_errors(self: XChainAccountCreateCommit) -> Dict[str, str]: + errors = super()._get_errors() + + if self.signature_reward != REQUIRED and not self.signature_reward.isnumeric(): + errors["signature_reward"] = "`signature_reward` must be numeric." + + if self.amount != REQUIRED and not self.amount.isnumeric(): + errors["amount"] = "`amount` must be numeric." + + return errors diff --git a/xrpl/models/transactions/xchain_add_account_create_attestation.py b/xrpl/models/transactions/xchain_add_account_create_attestation.py new file mode 100644 index 000000000..8b9f78bc4 --- /dev/null +++ b/xrpl/models/transactions/xchain_add_account_create_attestation.py @@ -0,0 +1,118 @@ +"""Model for a XChainAddAccountCreateAttestation transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Union + +from typing_extensions import Literal + +from xrpl.models.amounts import Amount +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainAddAccountCreateAttestation(Transaction): + """ + Represents a XChainAddAccountCreateAttestation transaction. + The XChainAddAccountCreateAttestation transaction provides an attestation + from a witness server that a XChainAccountCreateCommit transaction occurred + on the other chain. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge associated with the attestation. This field is required. + + :meta hide-value: + """ + + public_key: str = REQUIRED # type: ignore + """ + The public key used to verify the signature. This field is required. + + :meta hide-value: + """ + + signature: str = REQUIRED # type: ignore + """ + The signature attesting to the event on the other chain. This field is + required. + + :meta hide-value: + """ + + other_chain_source: str = REQUIRED # type: ignore + """ + The account on the source chain that submitted the + ``XChainAccountCreateCommit`` transaction that triggered the event + associated with the attestation. This field is required. + + :meta hide-value: + """ + + amount: Amount = REQUIRED # type: ignore + """ + The amount committed by the ``XChainAccountCreateCommit`` transaction on + the source chain. This field is required. + + :meta hide-value: + """ + + attestation_reward_account: str = REQUIRED # type: ignore + """ + The account that should receive this signer's share of the + ``SignatureReward``. This field is required. + + :meta hide-value: + """ + + attestation_signer_account: str = REQUIRED # type: ignore + """ + The account on the door account's signer list that is signing the + transaction. This field is required. + + :meta hide-value: + """ + + was_locking_chain_send: Union[Literal[0], Literal[1]] = REQUIRED # type: ignore + """ + A boolean representing the chain where the event occurred. This field is + required. + + :meta hide-value: + """ + + xchain_account_create_count: Union[str, int] = REQUIRED # type: ignore + """ + The counter that represents the order that the claims must be processed in. + This field is required. + + :meta hide-value: + """ + + destination: str = REQUIRED # type: ignore + """ + The destination account for the funds on the destination chain. This field + is required. + + :meta hide-value: + """ + + signature_reward: Amount = REQUIRED # type: ignore + """ + The signature reward paid in the ``XChainAccountCreateCommit`` transaction. + This field is required. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, + init=False, + ) diff --git a/xrpl/models/transactions/xchain_add_claim_attestation.py b/xrpl/models/transactions/xchain_add_claim_attestation.py new file mode 100644 index 000000000..c54dfa18f --- /dev/null +++ b/xrpl/models/transactions/xchain_add_claim_attestation.py @@ -0,0 +1,109 @@ +"""Model for a XChainAddClaimAttestation transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional, Union + +from typing_extensions import Literal + +from xrpl.models.amounts import Amount +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainAddClaimAttestation(Transaction): + """ + Represents a XChainAddClaimAttestation transaction. + The ``XChainAddClaimAttestation`` transaction provides proof from a witness + server, attesting to an ``XChainCommit`` transaction. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge to use to transfer funds. This field is required. + + :meta hide-value: + """ + + public_key: str = REQUIRED # type: ignore + """ + The public key used to verify the signature. This field is required. + + :meta hide-value: + """ + + signature: str = REQUIRED # type: ignore + """ + The signature attesting to the event on the other chain. This field is + required. + + :meta hide-value: + """ + + other_chain_source: str = REQUIRED # type: ignore + """ + The account on the source chain that submitted the ``XChainCommit`` + transaction that triggered the event associated with the attestation. This + field is required. + + :meta hide-value: + """ + + amount: Amount = REQUIRED # type: ignore + """ + The amount committed by the ``XChainCommit`` transaction on the source + chain. This field is required. + + :meta hide-value: + """ + + attestation_reward_account: str = REQUIRED # type: ignore + """ + The account that should receive this signer's share of the + ``SignatureReward``. This field is required. + + :meta hide-value: + """ + + attestation_signer_account: str = REQUIRED # type: ignore + """ + The account on the door account's signer list that is signing the + transaction. This field is required. + + :meta hide-value: + """ + + was_locking_chain_send: Union[Literal[0], Literal[1]] = REQUIRED # type: ignore + """ + A boolean representing the chain where the event occurred. This field is + required. + + :meta hide-value: + """ + + xchain_claim_id: Union[str, int] = REQUIRED # type: ignore + """ + The ``XChainClaimID`` associated with the transfer, which was included in + the ``XChainCommit`` transaction. This field is required. + + :meta hide-value: + """ + + destination: Optional[str] = None + """ + The destination account for the funds on the destination chain (taken from + the ``XChainCommit`` transaction). + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_ADD_CLAIM_ATTESTATION, + init=False, + ) diff --git a/xrpl/models/transactions/xchain_claim.py b/xrpl/models/transactions/xchain_claim.py new file mode 100644 index 000000000..7889d35d6 --- /dev/null +++ b/xrpl/models/transactions/xchain_claim.py @@ -0,0 +1,87 @@ +"""Model for a XChainClaim transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional, Union + +from xrpl.models.amounts import Amount +from xrpl.models.currencies import XRP +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainClaim(Transaction): + """ + Represents a XChainClaim transaction. + The ``XChainClaim`` transaction completes a cross-chain transfer of value. + It allows a user to claim the value on the destination chain - the + equivalent of the value locked on the source chain. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge to use for the transfer. This field is required. + + :meta hide-value: + """ + + xchain_claim_id: Union[int, str] = REQUIRED # type: ignore + """ + The unique integer ID for the cross-chain transfer that was referenced in + the corresponding ``XChainCommit`` transaction. This field is required. + + :meta hide-value: + """ + + destination: str = REQUIRED # type: ignore + """ + The destination account on the destination chain. It must exist or the + transaction will fail. However, if the transaction fails in this case, the + sequence number and collected signatures won't be destroyed, and the + transaction can be rerun with a different destination. This field is + required. + + :meta hide-value: + """ + + destination_tag: Optional[int] = None + """ + An integer destination tag. + + :meta hide-value: + """ + + amount: Amount = REQUIRED # type: ignore + """ + The amount to claim on the destination chain. This must match the amount + attested to on the attestations associated with this ``XChainClaimID``. + This field is required. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_CLAIM, + init=False, + ) + + def _get_errors(self: XChainClaim) -> Dict[str, str]: + errors = super()._get_errors() + + bridge = self.xchain_bridge + currency = XRP() if isinstance(self.amount, str) else self.amount.to_currency() + if ( + currency != bridge.locking_chain_issue + and currency != bridge.issuing_chain_issue + ): + errors[ + "amount" + ] = "amount must match either locking chain issue or issuing chain issue." + + return errors diff --git a/xrpl/models/transactions/xchain_commit.py b/xrpl/models/transactions/xchain_commit.py new file mode 100644 index 000000000..10e7f7696 --- /dev/null +++ b/xrpl/models/transactions/xchain_commit.py @@ -0,0 +1,65 @@ +"""Model for a XChainCommit transaction type.""" + +from dataclasses import dataclass, field +from typing import Optional, Union + +from xrpl.models.amounts import Amount +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainCommit(Transaction): + """ + Represents a XChainCommit transaction. + The `XChainCommit` transaction is the second step in a cross-chain + transfer. It puts assets into trust on the locking chain so that they can + be wrapped on the issuing chain, or burns wrapped assets on the issuing + chain so that they can be returned on the locking chain. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge to use to transfer funds. This field is required. + + :meta hide-value: + """ + + xchain_claim_id: Union[int, str] = REQUIRED # type: ignore + """ + The unique integer ID for a cross-chain transfer. This must be acquired on + the destination chain (via a ``XChainCreateClaimID`` transaction) and + checked from a validated ledger before submitting this transaction. If an + incorrect sequence number is specified, the funds will be lost. This field + is required. + + :meta hide-value: + """ + + amount: Amount = REQUIRED # type: ignore + """ + The asset to commit, and the quantity. This must match the door account's + ``LockingChainIssue`` (if on the locking chain) or the door account's + ``IssuingChainIssue`` (if on the issuing chain). This field is required. + + :meta hide-value: + """ + + other_chain_destination: Optional[str] = None + """ + The destination account on the destination chain. If this is not specified, + the account that submitted the ``XChainCreateClaimID`` transaction on the + destination chain will need to submit a ``XChainClaim`` transaction to + claim the funds. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_COMMIT, + init=False, + ) diff --git a/xrpl/models/transactions/xchain_create_bridge.py b/xrpl/models/transactions/xchain_create_bridge.py new file mode 100644 index 000000000..3ebe7caeb --- /dev/null +++ b/xrpl/models/transactions/xchain_create_bridge.py @@ -0,0 +1,95 @@ +"""Model for a XChainCreateBridge transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional + +from xrpl.models.currencies import XRP +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainCreateBridge(Transaction): + """ + Represents a XChainCreateBridge transaction. + The XChainCreateBridge transaction creates a new `Bridge` ledger object and + defines a new cross-chain bridge entrance on the chain that the transaction + is submitted on. It includes information about door accounts and assets for + the bridge. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge (door accounts and assets) to create. This field is required. + + :meta hide-value: + """ + + signature_reward: str = REQUIRED # type: ignore + """ + The total amount to pay the witness servers for their signatures. This + amount will be split among the signers. This field is required. + + :meta hide-value: + """ + + min_account_create_amount: Optional[str] = None + """ + The minimum amount, in XRP, required for a ``XChainAccountCreateCommit`` + transaction. If this isn't present, the ``XChainAccountCreateCommit`` + transaction will fail. This field can only be present on XRP-XRP bridges. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_CREATE_BRIDGE, + init=False, + ) + + def _get_errors(self: XChainCreateBridge) -> Dict[str, str]: + errors = super()._get_errors() + + bridge = self.xchain_bridge + + if bridge.locking_chain_door == bridge.issuing_chain_door: + errors[ + "xchain_bridge" + ] = "Cannot have the same door accounts on the locking and issuing chain." + + if self.account not in [bridge.locking_chain_door, bridge.issuing_chain_door]: + errors[ + "account" + ] = "account must be either locking chain door or issuing chain door." + + if (bridge.locking_chain_issue == XRP()) != ( + bridge.issuing_chain_issue == XRP() + ): + errors["issue"] = "Bridge must be XRP-XRP or IOU-IOU." + + if ( + self.min_account_create_amount is not None + and bridge.locking_chain_issue != XRP() + ): + errors[ + "min_account_create_amount" + ] = "Cannot have MinAccountCreateAmount if bridge is IOU-IOU." + + if self.signature_reward != REQUIRED and not self.signature_reward.isnumeric(): + errors["signature_reward"] = "signature_reward must be numeric." + + if ( + self.min_account_create_amount is not None + and not self.min_account_create_amount.isnumeric() + ): + errors[ + "min_account_create_amount_value" + ] = "min_account_create_amount must be numeric." + + return errors diff --git a/xrpl/models/transactions/xchain_create_claim_id.py b/xrpl/models/transactions/xchain_create_claim_id.py new file mode 100644 index 000000000..02b31507a --- /dev/null +++ b/xrpl/models/transactions/xchain_create_claim_id.py @@ -0,0 +1,68 @@ +"""Model for a XChainCreateClaimID transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict + +from xrpl.core.addresscodec import is_valid_classic_address +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainCreateClaimID(Transaction): + """ + Represents a XChainCreateClaimID transaction. + The XChainCreateClaimID transaction creates a new cross-chain claim ID that + is used for a cross-chain transfer. A cross-chain claim ID represents one + cross-chain transfer of value. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge to create the claim ID for. This field is required. + + :meta hide-value: + """ + + signature_reward: str = REQUIRED # type: ignore + """ + The amount, in XRP, to reward the witness servers for providing signatures. + This must match the amount on the ``Bridge`` ledger object. This field is + required. + + :meta hide-value: + """ + + other_chain_source: str = REQUIRED # type: ignore + """ + The account that must send the corresponding ``XChainCommit`` transaction + on the source chain. This field is required. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_CREATE_CLAIM_ID, + init=False, + ) + + def _get_errors(self: XChainCreateClaimID) -> Dict[str, str]: + errors = super()._get_errors() + + if self.signature_reward != REQUIRED and not self.signature_reward.isnumeric(): + errors["signature_reward"] = "`signature_reward` must be numeric." + + if self.other_chain_source != REQUIRED and not is_valid_classic_address( + self.other_chain_source + ): + errors[ + "other_chain_source" + ] = "`other_chain_source` must be a valid XRPL address." + + return errors diff --git a/xrpl/models/transactions/xchain_modify_bridge.py b/xrpl/models/transactions/xchain_modify_bridge.py new file mode 100644 index 000000000..bae010301 --- /dev/null +++ b/xrpl/models/transactions/xchain_modify_bridge.py @@ -0,0 +1,108 @@ +"""Model for a XChainModifyBridge transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Optional + +from xrpl.models.currencies import XRP +from xrpl.models.flags import FlagInterface +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +class XChainModifyBridgeFlag(int, Enum): + """ + Transactions of the XChainModifyBridge type support additional values in the Flags + field. This enum represents those options. + """ + + TF_CLEAR_ACCOUNT_CREATE_AMOUNT = 0x00010000 + + +class XChainModifyBridgeFlagInterface(FlagInterface): + """ + Transactions of the XChainModifyBridge type support additional values in the Flags + field. This TypedDict represents those options. + """ + + TF_CLEAR_ACCOUNT_CREATE_AMOUNT: bool + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainModifyBridge(Transaction): + """ + Represents a XChainModifyBridge transaction. + The XChainModifyBridge transaction allows bridge managers to modify the + parameters of the bridge. + """ + + xchain_bridge: XChainBridge = REQUIRED # type: ignore + """ + The bridge to modify. This field is required. + + :meta hide-value: + """ + + signature_reward: Optional[str] = None + """ + The signature reward split between the witnesses for submitting + attestations. + + :meta hide-value: + """ + + min_account_create_amount: Optional[str] = None + """ + The minimum amount, in XRP, required for a ``XChainAccountCreateCommit`` + transaction. If this is not present, the ``XChainAccountCreateCommit`` + transaction will fail. This field can only be present on XRP-XRP bridges. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.XCHAIN_MODIFY_BRIDGE, + init=False, + ) + + def _get_errors(self: XChainModifyBridge) -> Dict[str, str]: + errors = super()._get_errors() + + bridge = self.xchain_bridge + + if self.signature_reward is None and self.min_account_create_amount is None: + errors[ + "xchain_modify_bridge" + ] = "Must either change signature_reward or min_account_create_amount." + + if self.account not in [bridge.locking_chain_door, bridge.issuing_chain_door]: + errors[ + "account" + ] = "account must be either locking chain door or issuing chain door." + + if self.signature_reward is not None and not self.signature_reward.isnumeric(): + errors["signature_reward"] = "`signature_reward` must be numeric." + + if ( + self.min_account_create_amount is not None + and bridge.locking_chain_issue != XRP() + ): + errors[ + "min_account_create_amount" + ] = "Cannot have MinAccountCreateAmount if bridge is IOU-IOU." + + if ( + self.min_account_create_amount is not None + and not self.min_account_create_amount.isnumeric() + ): + errors[ + "min_account_create_amount_value" + ] = "`min_account_create_amount` must be numeric." + + return errors diff --git a/xrpl/models/xchain_bridge.py b/xrpl/models/xchain_bridge.py new file mode 100644 index 000000000..2bf488ba6 --- /dev/null +++ b/xrpl/models/xchain_bridge.py @@ -0,0 +1,39 @@ +"""A XChainBridge represents a cross-chain bridge.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from xrpl.models.base_model import BaseModel +from xrpl.models.currencies import Currency +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class XChainBridge(BaseModel): + """A XChainBridge represents a cross-chain bridge.""" + + locking_chain_door: str + """ + The door account on the locking chain. + """ + + locking_chain_issue: Currency + """ + The asset that is locked and unlocked on the locking chain. + """ + + issuing_chain_door: str + """ + The door account on the issuing chain. For an XRP-XRP bridge, this must be + the genesis account (the account that is created when the network is first + started, which contains all of the XRP). + """ + + issuing_chain_issue: Currency + """ + The asset that is minted and burned on the issuing chain. For an IOU-IOU + bridge, the issuer of the asset must be the door account on the issuing + chain, to avoid supply issues. + """ diff --git a/xrpl/utils/__init__.py b/xrpl/utils/__init__.py index 5f164f1f5..789fff8f2 100644 --- a/xrpl/utils/__init__.py +++ b/xrpl/utils/__init__.py @@ -1,5 +1,6 @@ """Convenience utilities for the XRP Ledger""" +from xrpl.utils.get_xchain_claim_id import get_xchain_claim_id from xrpl.utils.str_conversions import hex_to_str, str_to_hex from xrpl.utils.time_conversions import ( XRPLTimeRangeException, @@ -29,4 +30,5 @@ "get_balance_changes", "get_final_balances", "get_order_book_changes", + "get_xchain_claim_id", ] diff --git a/xrpl/utils/get_xchain_claim_id.py b/xrpl/utils/get_xchain_claim_id.py new file mode 100644 index 000000000..8da6f8c42 --- /dev/null +++ b/xrpl/utils/get_xchain_claim_id.py @@ -0,0 +1,45 @@ +"""Utils to get an XChainClaimID from metadata.""" +from xrpl.models.transactions.metadata import TransactionMetadata, isCreatedNode + + +def get_xchain_claim_id(meta: TransactionMetadata) -> str: + """ + Gets the XChainClaimID from a recently-submitted XChainCreateClaimID transaction. + + Args: + meta: Metadata from the response to submitting an XChainCreateClaimID + transaction. + + Returns: + The newly created XChainClaimID. + + Raises: + TypeError: if given something other than metadata (e.g. the full + transaction response). + """ + if meta is None or meta.get("AffectedNodes") is None: + raise TypeError( + f"""Unable to parse the parameter given to get_xchain_claim_id. + 'meta' must be the metadata from an XChainCreateClaimID transaction. + Received {meta} instead.""" + ) + + affected_nodes = [ + node + for node in meta["AffectedNodes"] + if isCreatedNode(node) + and node["CreatedNode"]["LedgerEntryType"] == "XChainOwnedClaimID" + ] + + if len(affected_nodes) == 0: + raise TypeError("No XChainOwnedClaimID created.") + + if len(affected_nodes) > 1: + # Sanity check - should never happen + raise TypeError( + "Multiple XChainOwnedClaimIDs were somehow created. Please report this " + "error." + ) + + # Get the NFTokenID which wasn't there before this transaction completed. + return affected_nodes[0]["CreatedNode"]["NewFields"]["XChainClaimID"]