diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..04d1eb944 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Set the default code owner for everything in the repository +* @trmlabs/ci-blockchain-data + diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d2be695..11b32742c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Function to parse the final account balances from a transaction's metadata - Function to parse order book changes from a transaction's metadata - Support for Ed25519 seeds that don't use the `sEd` prefix +- Support for Automated Market Maker (AMM) transactions and requests as defined in XLS-30. - Add docs to`get_account_transactions` explaining how to allow pagination through all transaction history [#462] - Common field `ticket_sequence` to Transaction class diff --git a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json index be2e637a3..f5b5a5976 100644 --- a/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json +++ b/tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json @@ -4435,20 +4435,229 @@ } } ], - "transactions": [{ - "binary": "1200002200000000240000003E6140000002540BE40068400000000000000A7321034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E74473045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F17962646398114550FC62003E785DC231A1058A05E56E3F09CF4E68314D4CC8AB5B21D86A82C3E9E8D0ECF2404B77FECBA", - "json": { - "Account": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", - "Destination": "rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj", - "TransactionType": "Payment", - "TxnSignature": "3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639", - "SigningPubKey": "034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E", - "Amount": "10000000000", - "Fee": "10", - "Flags": 0, - "Sequence": 62 + "transactions": [ + { + "binary": "1200002200000000240000003E6140000002540BE40068400000000000000A7321034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E74473045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F17962646398114550FC62003E785DC231A1058A05E56E3F09CF4E68314D4CC8AB5B21D86A82C3E9E8D0ECF2404B77FECBA", + "json": { + "Account": "r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV", + "Destination": "rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj", + "TransactionType": "Payment", + "TxnSignature": "3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639", + "SigningPubKey": "034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E", + "Amount": "10000000000", + "Fee": "10", + "Flags": 0, + "Sequence": 62 + } + }, + { + "binary": "12002315000A2200000000240015DAE161400000000000271068400000000000000A6BD5838D7EA4C680000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMCreate", + "TxnSignature": "B3154D968314FCEB58001E1B0C3A4CFB33DF9FF6C73207E5EAEB9BD07E2747672168E1A2786D950495C38BD8DEE3391BF45F3008DD36F4B12E7C07D82CA5250E", + "Amount": "10000", + "Amount2": { + "currency": "ETH", + "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", + "value": "10000" + }, + "TradingFee": 10, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8" + } + }, + { + "binary": "1200242200010000240015DAE168400000000000000A6019D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874408073C588E7EF672DD171E414638D9AF8DBE9A1359E030DE3E1C9AA6A38A2CE9E138CB56482BB844F7228D48B1E4AD7D09BB7E9F639C115958EEEA374749CA00B8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "TxnSignature": "8073C588E7EF672DD171E414638D9AF8DBE9A1359E030DE3E1C9AA6A38A2CE9E138CB56482BB844F7228D48B1E4AD7D09BB7E9F639C115958EEEA374749CA00B", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 65536, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8" + } + }, + { + "binary": "1200242200080000240015DAE16140000000000003E868400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744096CA066F42871C55088D2758D64148921B1ACAA5C6C648D0F7D675BBF47F87DF711F17C5BD172666D5AEC257520C587A849A6E063345609D91E121A78816EB048114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "10", + "Flags": 524288, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "96CA066F42871C55088D2758D64148921B1ACAA5C6C648D0F7D675BBF47F87DF711F17C5BD172666D5AEC257520C587A849A6E063345609D91E121A78816EB04" + } + }, + { + "binary": "1200242200100000240015DAE16140000000000003E868400000000000000A6BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440FC22B16A098C236ED7EDB3EBC983026DFD218A03C8BAA848F3E1D5389D5B8B00473C1178C5BA257BFA2DCD433C414690A430A5CFD71C1C0A7F7BF725EC1759018114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "10", + "Flags": 1048576, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "FC22B16A098C236ED7EDB3EBC983026DFD218A03C8BAA848F3E1D5389D5B8B00473C1178C5BA257BFA2DCD433C414690A430A5CFD71C1C0A7F7BF725EC175901" + } + }, + { + "binary": "1200242200200000240015DAE16140000000000003E868400000000000000A6019D5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440117CF90F9B113AD3BD638B6DB63562B37C287D5180F278B3CCF58FC14A5BAEE98307EA0F6DFE19E2FBA887C92955BA5D1A04F92ADAAEB309DE89C3610D074C098114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenOut": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 2097152, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "117CF90F9B113AD3BD638B6DB63562B37C287D5180F278B3CCF58FC14A5BAEE98307EA0F6DFE19E2FBA887C92955BA5D1A04F92ADAAEB309DE89C3610D074C09" + } + }, + { + "binary": "1200242200400000240015DAE16140000000000003E868400000000000000A601B40000000000000197321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874405E51EBC6B52A7C3BA5D0AE2FC8F62E779B80182009B3108A87AB6D770D68F56053C193DB0640128E4765565970625B1E2878E116AC854E6DED412202CCDE0B0D8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMDeposit", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "10", + "Flags": 4194304, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "5E51EBC6B52A7C3BA5D0AE2FC8F62E779B80182009B3108A87AB6D770D68F56053C193DB0640128E4765565970625B1E2878E116AC854E6DED412202CCDE0B0D" + } + }, + { + "binary": "1200252200010000240015DAE168400000000000000A601AD5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B874409D4F41FC452526C0AD17191959D9B6D04A3C73B3A6C29E0F34C8459675A83A7A7D6E3021390EC8C9BE6C93E11C167E12016465E523F64F9EB3194B0A52E418028114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 65536, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "9D4F41FC452526C0AD17191959D9B6D04A3C73B3A6C29E0F34C8459675A83A7A7D6E3021390EC8C9BE6C93E11C167E12016465E523F64F9EB3194B0A52E41802" + } + }, + { + "binary": "1200252200080000240015DAE16140000000000003E868400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440E2C60D56C337D6D73E4B7D53579C93C666605494E82A89DD58CFDE79E2A4866BCF52370A2146877A2EF748E98168373710001133A51B645D89491849079035018114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Fee": "10", + "Flags": 524288, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "E2C60D56C337D6D73E4B7D53579C93C666605494E82A89DD58CFDE79E2A4866BCF52370A2146877A2EF748E98168373710001133A51B645D8949184907903501" + } + }, + { + "binary": "1200252200100000240015DAE16140000000000003E868400000000000000A6BD511C37937E080000000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440D2FCD7D03E53358BC6188BA88A7BA4FF2519B639C3B5C0EBCBDCB704426CA2837111430E92A6003D1CD0D81C63682C74839320539EC4F89B82AA5607714952028114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "Amount2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9", "value": "500"}, + "Fee": "10", + "Flags": 1048576, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "D2FCD7D03E53358BC6188BA88A7BA4FF2519B639C3B5C0EBCBDCB704426CA2837111430E92A6003D1CD0D81C63682C74839320539EC4F89B82AA560771495202" + } + }, + { + "binary": "1200252200200000240015DAE16140000000000003E868400000000000000A601AD5438D7EA4C68000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744042DA5620E924E2D2059BBB4E0C4F03244140ACED93B543136FEEDF802165F814D09F45C7E2A4618468442516F4712A23B1D3332D5DBDBAE830337F39F259C90F8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "LPTokenIn": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "1000"}, + "Fee": "10", + "Flags": 2097152, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "42DA5620E924E2D2059BBB4E0C4F03244140ACED93B543136FEEDF802165F814D09F45C7E2A4618468442516F4712A23B1D3332D5DBDBAE830337F39F259C90F" + } + }, + { + "binary": "1200252200400000240015DAE16140000000000003E868400000000000000A601B40000000000000197321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8744045BCEE5A12E5F5F1FB085A24F2F7FD962BBCB0D89A44A5319E3F7E3799E1870341880B6F684132971DDDF2E6B15356B3F407962D6D4E8DE10989F3B16E3CB90D8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMWithdraw", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "Amount": "1000", + "EPrice": "25", + "Fee": "10", + "Flags": 4194304, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "45BCEE5A12E5F5F1FB085A24F2F7FD962BBCB0D89A44A5319E3F7E3799E1870341880B6F684132971DDDF2E6B15356B3F407962D6D4E8DE10989F3B16E3CB90D" + } + }, + { + "binary": "1200272200000000240015DAE168400000000000000A6CD4C8E1BC9BF04000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0486DD4CC6F3B40B6C000B3813FCAB4EE68B3D0D735D6849465A9113EE048B3813FCAB4EE68B3D0D735D6849465A9113EE0487321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440F8EAAFB5EC1A69275167589969F0B9764BACE6BC8CC81482C2FC5ACCE691EDBD0D88D141137B1253BB1B9AC90A8A52CB37F5B6F7E1028B06DD06F91BE06F5A0F8114F92F27CC5EE2F2760278FE096D0CBE32BDD3653AF019E01B81149A91957F8F16BC57F3F200CD8C98375BF1791586E1F10318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMBid", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "AuthAccounts": [{"AuthAccount": {"Account": "rEaHTti4HZsMBpxTAF4ncWxkcdqDh1h6P7"}}], + "BidMax": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "35"}, + "BidMin": {"currency": "B3813FCAB4EE68B3D0D735D6849465A9113EE048", "issuer": "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg", "value": "25"}, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "F8EAAFB5EC1A69275167589969F0B9764BACE6BC8CC81482C2FC5ACCE691EDBD0D88D141137B1253BB1B9AC90A8A52CB37F5B6F7E1028B06DD06F91BE06F5A0F" + } + }, + { + "binary": "1200261500EA2200000000240015DAE168400000000000000A7321ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B87440BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B21078114F92F27CC5EE2F2760278FE096D0CBE32BDD3653A0318000000000000000000000000000000000000000004180000000000000000000000004554480000000000FBEF9A3A2B814E807745FA3D9C32FFD155FA2E8C", + "json": { + "Account": "rP5ZkB5RZQaECsSVR4DeSFK4fAw52BYtbw", + "TransactionType": "AMMVote", + "Asset": {"currency": "XRP"}, + "Asset2": {"currency": "ETH", "issuer": "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9"}, + "TradingFee": 234, + "Fee": "10", + "Flags": 0, + "Sequence": 1432289, + "SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8", + "TxnSignature": "BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B2107" + } } - }], + ], "ledgerData": [{ "binary": "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A00", "json": { diff --git a/tests/unit/models/requests/test_amm_info.py b/tests/unit/models/requests/test_amm_info.py new file mode 100644 index 000000000..a4d6a9e52 --- /dev/null +++ b/tests/unit/models/requests/test_amm_info.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.requests import AMMInfo + +_ASSET = XRP() +_ASSET_2 = IssuedCurrency(currency="USD", issuer="rN6zcSynkRnf8zcgTVrRL8K7r4ovE7J4Zj") + + +class TestAMMInfo(TestCase): + def test_asset_asset2(self): + request = AMMInfo( + asset=_ASSET, + asset2=_ASSET_2, + ) + self.assertTrue(request.is_valid()) diff --git a/tests/unit/models/test_base_model.py b/tests/unit/models/test_base_model.py index ae5c9bff1..ceaec1a03 100644 --- a/tests/unit/models/test_base_model.py +++ b/tests/unit/models/test_base_model.py @@ -4,6 +4,7 @@ from xrpl.models import XRPLModelException from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.currencies import XRP, IssuedCurrency from xrpl.models.requests import ( AccountChannels, BookOffers, @@ -16,6 +17,8 @@ SubmitOnly, ) from xrpl.models.transactions import ( + AMMBid, + AuthAccount, CheckCreate, Memo, Payment, @@ -597,3 +600,72 @@ def test_to_xrpl_signer(self): ], } self.assertEqual(tx.to_xrpl(), expected) + + def test_to_xrpl_auth_accounts(self): + tx = AMMBid( + account="r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ", + asset=XRP(), + asset2=IssuedCurrency( + currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW" + ), + bid_min=IssuedCurrencyAmount( + currency="5475B6C930B7BDD81CDA8FBA5CED962B11218E5A", + issuer="r3628pXjRqfw5zfwGfhSusjZTvE3BoxEBw", + value="25", + ), + bid_max=IssuedCurrencyAmount( + currency="5475B6C930B7BDD81CDA8FBA5CED962B11218E5A", + issuer="r3628pXjRqfw5zfwGfhSusjZTvE3BoxEBw", + value="35", + ), + auth_accounts=[ + AuthAccount(account="rNZdsTBP5tH1M6GHC6bTreHAp6ouP8iZSh"), + AuthAccount(account="rfpFv97Dwu89FTyUwPjtpZBbuZxTqqgTmH"), + AuthAccount(account="rzzYHPGb8Pa64oqxCzmuffm122bitq3Vb"), + AuthAccount(account="rhwxHxaHok86fe4LykBom1jSJ3RYQJs1h4"), + ], + ) + expected = { + "Account": "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ", + "Asset": {"currency": "XRP"}, + "Asset2": { + "currency": "ETH", + "issuer": "rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW", + }, + "BidMin": { + "currency": "5475B6C930B7BDD81CDA8FBA5CED962B11218E5A", + "issuer": "r3628pXjRqfw5zfwGfhSusjZTvE3BoxEBw", + "value": "25", + }, + "BidMax": { + "currency": "5475B6C930B7BDD81CDA8FBA5CED962B11218E5A", + "issuer": "r3628pXjRqfw5zfwGfhSusjZTvE3BoxEBw", + "value": "35", + }, + "AuthAccounts": [ + { + "AuthAccount": { + "Account": "rNZdsTBP5tH1M6GHC6bTreHAp6ouP8iZSh", + } + }, + { + "AuthAccount": { + "Account": "rfpFv97Dwu89FTyUwPjtpZBbuZxTqqgTmH", + } + }, + { + "AuthAccount": { + "Account": "rzzYHPGb8Pa64oqxCzmuffm122bitq3Vb", + } + }, + { + "AuthAccount": { + "Account": "rhwxHxaHok86fe4LykBom1jSJ3RYQJs1h4", + } + }, + ], + "TransactionType": "AMMBid", + "SigningPubKey": "", + "Flags": 0, + } + self.assertEqual(tx.to_xrpl(), expected) diff --git a/tests/unit/models/transactions/test_amm_bid.py b/tests/unit/models/transactions/test_amm_bid.py new file mode 100644 index 000000000..b93c243d1 --- /dev/null +++ b/tests/unit/models/transactions/test_amm_bid.py @@ -0,0 +1,66 @@ +from unittest import TestCase + +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions import AMMBid, AuthAccount + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ASSET = XRP() +_ASSET2 = IssuedCurrency(currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW") +_AUTH_ACCOUNTS = [ + AuthAccount( + account="rNZdsTBP5tH1M6GHC6bTreHAp6ouP8iZSh", + ), + AuthAccount( + account="rfpFv97Dwu89FTyUwPjtpZBbuZxTqqgTmH", + ), + AuthAccount( + account="rzzYHPGb8Pa64oqxCzmuffm122bitq3Vb", + ), + AuthAccount( + account="rhwxHxaHok86fe4LykBom1jSJ3RYQJs1h4", + ), +] +_LPTOKEN_CURRENCY = "5475B6C930B7BDD81CDA8FBA5CED962B11218E5A" +_LPTOKEN_ISSUER = "r3628pXjRqfw5zfwGfhSusjZTvE3BoxEBw" + + +class TestAMMBid(TestCase): + def test_tx_valid(self): + tx = AMMBid( + account=_ACCOUNT, + asset=_ASSET, + asset2=_ASSET2, + bid_min=IssuedCurrencyAmount( + currency=_LPTOKEN_CURRENCY, + issuer=_LPTOKEN_ISSUER, + value="25", + ), + bid_max=IssuedCurrencyAmount( + currency=_LPTOKEN_CURRENCY, + issuer=_LPTOKEN_ISSUER, + value="35", + ), + auth_accounts=_AUTH_ACCOUNTS, + ) + self.assertTrue(tx.is_valid()) + + def test_auth_accounts_length_error(self): + auth_accounts = _AUTH_ACCOUNTS.copy() + auth_accounts.append( + AuthAccount( + account="r3X6noRsvaLapAKCG78zAtWcbhB3sggS1s", + ), + ) + with self.assertRaises(XRPLModelException) as error: + AMMBid( + account=_ACCOUNT, + asset=_ASSET, + asset2=_ASSET2, + auth_accounts=auth_accounts, + ) + self.assertEqual( + error.exception.args[0], + "{'auth_accounts': 'Length must not be greater than 4'}", + ) diff --git a/tests/unit/models/transactions/test_amm_create.py b/tests/unit/models/transactions/test_amm_create.py new file mode 100644 index 000000000..04e237653 --- /dev/null +++ b/tests/unit/models/transactions/test_amm_create.py @@ -0,0 +1,52 @@ +from sys import maxsize +from unittest import TestCase + +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions import AMMCreate + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_IOU_ISSUER = "rPyfep3gcLzkosKC9XiE77Y8DZWG6iWDT9" + + +class TestAMMCreate(TestCase): + def test_tx_is_valid(self): + tx = AMMCreate( + account=_ACCOUNT, + amount="1000", + amount2=IssuedCurrencyAmount( + currency="USD", issuer=_IOU_ISSUER, value="1000" + ), + trading_fee=12, + ) + self.assertTrue(tx.is_valid()) + + def test_trading_fee_too_high(self): + with self.assertRaises(XRPLModelException) as error: + AMMCreate( + account=_ACCOUNT, + amount="1000", + amount2=IssuedCurrencyAmount( + currency="USD", issuer=_IOU_ISSUER, value="1000" + ), + trading_fee=maxsize, + ) + self.assertEqual( + error.exception.args[0], + "{'trading_fee': 'Must be between 0 and 1000'}", + ) + + def test_trading_fee_negative_number(self): + with self.assertRaises(XRPLModelException) as error: + AMMCreate( + account=_ACCOUNT, + amount="1000", + amount2=IssuedCurrencyAmount( + currency="USD", issuer=_IOU_ISSUER, value="1000" + ), + trading_fee=-1, + ) + self.assertEqual( + error.exception.args[0], + "{'trading_fee': 'Must be between 0 and 1000'}", + ) diff --git a/tests/unit/models/transactions/test_amm_delete.py b/tests/unit/models/transactions/test_amm_delete.py new file mode 100644 index 000000000..37ff6b9f6 --- /dev/null +++ b/tests/unit/models/transactions/test_amm_delete.py @@ -0,0 +1,17 @@ +from unittest import TestCase + +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.transactions import AMMDelete + + +class TestAMMDeposit(TestCase): + def test_tx_valid(self): + tx = AMMDelete( + account="r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ", + sequence=1337, + asset=XRP(), + asset2=IssuedCurrency( + currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW" + ), + ) + self.assertTrue(tx.is_valid()) diff --git a/tests/unit/models/transactions/test_amm_deposit.py b/tests/unit/models/transactions/test_amm_deposit.py new file mode 100644 index 000000000..c8a2612c7 --- /dev/null +++ b/tests/unit/models/transactions/test_amm_deposit.py @@ -0,0 +1,127 @@ +from unittest import TestCase + +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions import AMMDeposit +from xrpl.models.transactions.amm_deposit import AMMDepositFlag + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ASSET = XRP() +_ASSET2 = IssuedCurrency(currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW") +_AMOUNT = "1000" +_LPTOKEN_CURRENCY = "B3813FCAB4EE68B3D0D735D6849465A9113EE048" +_LPTOKEN_ISSUER = "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg" + + +class TestAMMDeposit(TestCase): + def test_tx_valid_xrpl_lptokenout(self): + tx = AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + lp_token_out=IssuedCurrencyAmount( + currency=_LPTOKEN_CURRENCY, + issuer=_LPTOKEN_ISSUER, + value=_AMOUNT, + ), + flags=AMMDepositFlag.TF_LP_TOKEN, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount(self): + tx = AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + flags=AMMDepositFlag.TF_SINGLE_ASSET, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount_amount2(self): + tx = AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + amount2=IssuedCurrencyAmount( + currency=_ASSET2.currency, issuer=_ASSET2.issuer, value="500" + ), + flags=AMMDepositFlag.TF_TWO_ASSET, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount_lptokenout(self): + tx = AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + lp_token_out=IssuedCurrencyAmount( + currency=_LPTOKEN_CURRENCY, + issuer=_LPTOKEN_ISSUER, + value="500", + ), + flags=AMMDepositFlag.TF_ONE_ASSET_LP_TOKEN, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount_eprice(self): + tx = AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + e_price="25", + flags=AMMDepositFlag.TF_LIMIT_LP_TOKEN, + ) + self.assertTrue(tx.is_valid()) + + def test_undefined_amount_undefined_lptokenout_invalid_combo(self): + with self.assertRaises(XRPLModelException) as error: + AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + ) + self.assertEqual( + error.exception.args[0], + "{'AMMDeposit': 'Must set at least `lp_token_out` or `amount`'}", + ) + + def test_undefined_amount_defined_amount2_invalid_combo(self): + with self.assertRaises(XRPLModelException) as error: + AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount2=IssuedCurrencyAmount( + currency=_ASSET2.currency, issuer=_ASSET2.issuer, value="500" + ), + ) + self.assertEqual( + error.exception.args[0], + "{'AMMDeposit': 'Must set `amount` with `amount2`'}", + ) + + def test_undefined_amount_defined_eprice_invalid_combo(self): + with self.assertRaises(XRPLModelException) as error: + AMMDeposit( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + e_price="25", + ) + self.assertEqual( + error.exception.args[0], + "{'AMMDeposit': 'Must set `amount` with `e_price`'}", + ) diff --git a/tests/unit/models/transactions/test_amm_vote.py b/tests/unit/models/transactions/test_amm_vote.py new file mode 100644 index 000000000..7adcd5c78 --- /dev/null +++ b/tests/unit/models/transactions/test_amm_vote.py @@ -0,0 +1,48 @@ +from sys import maxsize +from unittest import TestCase + +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions import AMMVote + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ASSET = XRP() +_ASSET2 = IssuedCurrency(currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW") +_TRADING_FEE = 234 + + +class TestAMMVote(TestCase): + def test_tx_valid(self): + tx = AMMVote( + account=_ACCOUNT, + asset=_ASSET, + asset2=_ASSET2, + trading_fee=_TRADING_FEE, + ) + self.assertTrue(tx.is_valid()) + + def test_trading_fee_too_high(self): + with self.assertRaises(XRPLModelException) as error: + AMMVote( + account=_ACCOUNT, + asset=_ASSET, + asset2=_ASSET2, + trading_fee=maxsize, + ) + self.assertEqual( + error.exception.args[0], + "{'trading_fee': 'Must be between 0 and 1000'}", + ) + + def test_trading_fee_negative_number(self): + with self.assertRaises(XRPLModelException) as error: + AMMVote( + account=_ACCOUNT, + asset=_ASSET, + asset2=_ASSET2, + trading_fee=-1, + ) + self.assertEqual( + error.exception.args[0], + "{'trading_fee': 'Must be between 0 and 1000'}", + ) diff --git a/tests/unit/models/transactions/test_amm_withdraw.py b/tests/unit/models/transactions/test_amm_withdraw.py new file mode 100644 index 000000000..6c9635dcc --- /dev/null +++ b/tests/unit/models/transactions/test_amm_withdraw.py @@ -0,0 +1,135 @@ +from unittest import TestCase + +from xrpl.models.amounts import IssuedCurrencyAmount +from xrpl.models.currencies import XRP, IssuedCurrency +from xrpl.models.exceptions import XRPLModelException +from xrpl.models.transactions import AMMWithdraw +from xrpl.models.transactions.amm_withdraw import AMMWithdrawFlag + +_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" +_ASSET = XRP() +_ASSET2 = IssuedCurrency(currency="ETH", issuer="rpGtkFRXhgVaBzC5XCR7gyE2AZN5SN3SEW") +_AMOUNT = "1000" +_LPTOKEN_CURRENCY = "B3813FCAB4EE68B3D0D735D6849465A9113EE048" +_LPTOKEN_ISSUER = "rH438jEAzTs5PYtV6CHZqpDpwCKQmPW9Cg" + + +class TestAMMWithdraw(TestCase): + def test_tx_valid_lptokenin(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + lp_token_in=IssuedCurrencyAmount( + currency=_LPTOKEN_CURRENCY, + issuer=_LPTOKEN_ISSUER, + value=_AMOUNT, + ), + flags=AMMWithdrawFlag.TF_LP_TOKEN, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + flags=AMMWithdrawFlag.TF_SINGLE_ASSET, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount_amount2(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + amount2=IssuedCurrencyAmount( + currency=_ASSET2.currency, issuer=_ASSET2.issuer, value="500" + ), + flags=AMMWithdrawFlag.TF_TWO_ASSET, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount_lptokenin(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + lp_token_in=IssuedCurrencyAmount( + currency=_LPTOKEN_CURRENCY, + issuer=_LPTOKEN_ISSUER, + value="500", + ), + flags=AMMWithdrawFlag.TF_ONE_ASSET_LP_TOKEN, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_amount_eprice(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + e_price="25", + flags=AMMWithdrawFlag.TF_LIMIT_LP_TOKEN, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_one_asset_withdraw_all(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount=_AMOUNT, + flags=AMMWithdrawFlag.TF_ONE_ASSET_WITHDRAW_ALL, + ) + self.assertTrue(tx.is_valid()) + + def test_tx_valid_withdraw_all(self): + tx = AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + flags=AMMWithdrawFlag.TF_WITHDRAW_ALL, + ) + self.assertTrue(tx.is_valid()) + + def test_undefined_amount_defined_amount2_invalid_combo(self): + with self.assertRaises(XRPLModelException) as error: + AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + amount2=IssuedCurrencyAmount( + currency=_ASSET2.currency, issuer=_ASSET2.issuer, value="500" + ), + ) + self.assertEqual( + error.exception.args[0], + "{'AMMWithdraw': 'Must set `amount` with `amount2`'}", + ) + + def test_undefined_amount_defined_eprice_invalid_combo(self): + with self.assertRaises(XRPLModelException) as error: + AMMWithdraw( + account=_ACCOUNT, + sequence=1337, + asset=_ASSET, + asset2=_ASSET2, + e_price="25", + ) + self.assertEqual( + error.exception.args[0], + "{'AMMWithdraw': 'Must set `amount` with `e_price`'}", + ) diff --git a/tests/unit/models/transactions/test_pseudo_transactions.py b/tests/unit/models/transactions/test_pseudo_transactions.py index 28c9ae4a8..487b488f0 100644 --- a/tests/unit/models/transactions/test_pseudo_transactions.py +++ b/tests/unit/models/transactions/test_pseudo_transactions.py @@ -37,7 +37,7 @@ def test_from_xrpl_enable_amendment(self): full_dict = {**amendment_dict, "Flags": 0, "TxnSignature": ""} self.assertEqual(actual.to_xrpl(), full_dict) - def test_from_xrpl_set_fee(self): + def test_from_xrpl_set_fee_pre_amendment(self): reference_fee_units = 10 reserve_base = 20000000 reserve_increment = 5000000 @@ -64,6 +64,30 @@ def test_from_xrpl_set_fee(self): full_dict = {**set_fee_dict, "Flags": 0, "TxnSignature": ""} self.assertEqual(actual.to_xrpl(), full_dict) + def test_from_xrpl_set_fee_post_amendment(self): + reserve_base_drops = "20000000" + reserve_increment_drops = "5000000" + base_fee_drops = "000000000000000A" + set_fee_dict = { + "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp", + "BaseFeeDrops": base_fee_drops, + "Fee": "0", + "ReserveBaseDrops": reserve_base_drops, + "ReserveIncrementDrops": reserve_increment_drops, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "SetFee", + } + expected = SetFee( + reserve_base_drops=reserve_base_drops, + reserve_increment_drops=reserve_increment_drops, + base_fee_drops=base_fee_drops, + ) + actual = Transaction.from_xrpl(set_fee_dict) + self.assertEqual(actual, expected) + full_dict = {**set_fee_dict, "Flags": 0, "TxnSignature": ""} + self.assertEqual(actual.to_xrpl(), full_dict) + def test_from_xrpl_unl_modify(self): ledger_sequence = 1600000 validator = "ED6629D456285AE3613B285F65BBFF168D695BA3921F309949AFCD2CA7AFEC16FE" diff --git a/xrpl/asyncio/transaction/main.py b/xrpl/asyncio/transaction/main.py index 15811fb12..91a586189 100644 --- a/xrpl/asyncio/transaction/main.py +++ b/xrpl/asyncio/transaction/main.py @@ -1,4 +1,5 @@ """High-level transaction methods with XRPL transactions.""" + import math from typing import Any, Dict, Optional, cast @@ -25,7 +26,7 @@ _LEDGER_OFFSET: Final[int] = 20 # TODO: make this dynamic based on the current ledger fee -_ACCOUNT_DELETE_FEE: Final[int] = int(xrp_to_drops(2)) +_OWNER_RESERVE_FEE: Final[int] = int(xrp_to_drops(2)) async def safe_sign_and_submit_transaction( @@ -319,11 +320,14 @@ async def _calculate_fee_per_transaction_type( base_fee = math.ceil(net_fee * (33 + (len(fulfillment_bytes) / 16))) # AccountDelete Transaction - if transaction.transaction_type == TransactionType.ACCOUNT_DELETE: + if transaction.transaction_type in ( + TransactionType.ACCOUNT_DELETE, + TransactionType.AMM_CREATE, + ): if client is None: - base_fee = _ACCOUNT_DELETE_FEE + base_fee = _OWNER_RESERVE_FEE else: - base_fee = await _fetch_account_delete_fee(client) + base_fee = await _fetch_owner_reserve_fee(client) # Multi-signed Transaction # 10 drops × (1 + Number of Signatures Provided) @@ -334,7 +338,7 @@ async def _calculate_fee_per_transaction_type( return str(math.ceil(base_fee)) -async def _fetch_account_delete_fee(client: Client) -> int: - server_state = await client.request_impl(ServerState()) +async def _fetch_owner_reserve_fee(client: Client) -> int: + server_state = await client._request_impl(ServerState()) fee = server_state.result["state"]["validated_ledger"]["reserve_inc"] return int(fee) diff --git a/xrpl/core/binarycodec/definitions/definitions.json b/xrpl/core/binarycodec/definitions/definitions.json index 317e1feb7..aaa21357f 100644 --- a/xrpl/core/binarycodec/definitions/definitions.json +++ b/xrpl/core/binarycodec/definitions/definitions.json @@ -21,6 +21,7 @@ "UInt192": 21, "UInt384": 22, "UInt512": 23, + "Issue": 24, "Transaction": 10001, "LedgerEntry": 10002, "Validation": 10003, @@ -44,6 +45,7 @@ "NegativeUNL": 78, "NFTokenPage": 80, "NFTokenOffer": 55, + "AMM": 121, "Any": -3, "Child": -2, "Nickname": 110, @@ -271,6 +273,26 @@ "type": "UInt16" } ], + [ + "TradingFee", + { + "nth": 5, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], + [ + "DiscountedFee", + { + "nth": 6, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt16" + } + ], [ "Version", { @@ -761,6 +783,26 @@ "type": "UInt32" } ], + [ + "VoteWeight", + { + "nth": 48, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], + [ + "FirstNFTokenSequence", + { + "nth": 50, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "UInt32" + } + ], [ "IndexNext", { @@ -1111,6 +1153,16 @@ "type": "Hash256" } ], + [ + "AMMID", + { + "nth": 14, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Hash256" + } + ], [ "BookDirectory", { @@ -1381,6 +1433,36 @@ "type": "Amount" } ], + [ + "Amount2", + { + "nth": 11, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "BidMin", + { + "nth": 12, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "BidMax", + { + "nth": 13, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "MinimumOffer", { @@ -1421,6 +1503,86 @@ "type": "Amount" } ], + [ + "BaseFeeDrops", + { + "nth": 22, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "ReserveBaseDrops", + { + "nth": 23, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "ReserveIncrementDrops", + { + "nth": 24, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenOut", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenIn", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "EPrice", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "Price", + { + "nth": 28, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], + [ + "LPTokenBalance", + { + "nth": 31, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Amount" + } + ], [ "PublicKey", { @@ -1811,6 +1973,26 @@ "type": "PathSet" } ], + [ + "Asset", + { + "nth": 3, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], + [ + "Asset2", + { + "nth": 4, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "Issue" + } + ], [ "TransactionMetaData", { @@ -2021,6 +2203,36 @@ "type": "STObject" } ], + [ + "VoteEntry", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "AuctionSlot", + { + "nth": 26, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], + [ + "AuthAccount", + { + "nth": 27, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STObject" + } + ], [ "Signers", { @@ -2111,6 +2323,16 @@ "type": "STArray" } ], + [ + "VoteSlots", + { + "nth": 12, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } + ], [ "Majorities", { @@ -2160,6 +2382,16 @@ "isSigningField": true, "type": "STArray" } + ], + [ + "AuthAccounts", + { + "nth": 25, + "isVLEncoded": false, + "isSerialized": true, + "isSigningField": true, + "type": "STArray" + } ] ], "TRANSACTION_RESULTS": { @@ -2215,6 +2447,7 @@ "temUNKNOWN": -264, "temSEQ_AND_TICKET": -263, "temBAD_NFTOKEN_TRANSFER_FEE": -262, + "temBAD_AMM_TOKENS": -261, "tefFAILURE": -199, "tefALREADY": -198, @@ -2250,6 +2483,7 @@ "terNO_RIPPLE": -90, "terQUEUED": -89, "terPRE_TICKET": -88, + "terNO_AMM": -87, "tesSUCCESS": 0, @@ -2298,7 +2532,15 @@ "tecCANT_ACCEPT_OWN_NFTOKEN_OFFER": 158, "tecINSUFFICIENT_FUNDS": 159, "tecOBJECT_NOT_FOUND": 160, - "tecINSUFFICIENT_PAYMENT": 161 + "tecINSUFFICIENT_PAYMENT": 161, + "tecUNFUNDED_AMM": 162, + "tecAMM_BALANCE": 163, + "tecAMM_FAILED": 164, + "tecAMM_INVALID_TOKENS": 165, + "tecAMM_EMPTY": 166, + "tecAMM_NOT_EMPTY": 167, + "tecAMM_ACCOUNT": 168, + "tecINCOMPLETE": 169 }, "TRANSACTION_TYPES": { "Invalid": -1, @@ -2321,15 +2563,24 @@ "CheckCreate": 16, "CheckCash": 17, "CheckCancel": 18, + "CredentialCreate": 70, "DepositPreauth": 19, "TrustSet": 20, "AccountDelete": 21, "SetHook": 22, "NFTokenMint": 25, + "NFTokenModify": 61, "NFTokenBurn": 26, "NFTokenCreateOffer": 27, "NFTokenCancelOffer": 28, "NFTokenAcceptOffer": 29, + "Clawback": 30, + "AMMCreate": 35, + "AMMDeposit": 36, + "AMMWithdraw": 37, + "AMMVote": 38, + "AMMBid": 39, + "AMMDelete": 40, "EnableAmendment": 100, "SetFee": 101, "UNLModify": 102 diff --git a/xrpl/core/binarycodec/types/__init__.py b/xrpl/core/binarycodec/types/__init__.py index 9511fc29b..20ea1b4fb 100644 --- a/xrpl/core/binarycodec/types/__init__.py +++ b/xrpl/core/binarycodec/types/__init__.py @@ -7,6 +7,7 @@ from xrpl.core.binarycodec.types.hash128 import Hash128 from xrpl.core.binarycodec.types.hash160 import Hash160 from xrpl.core.binarycodec.types.hash256 import Hash256 +from xrpl.core.binarycodec.types.issue import Issue from xrpl.core.binarycodec.types.path_set import PathSet from xrpl.core.binarycodec.types.st_array import STArray from xrpl.core.binarycodec.types.st_object import STObject @@ -26,6 +27,7 @@ "Hash128", "Hash160", "Hash256", + "Issue", "PathSet", "STObject", "STArray", diff --git a/xrpl/core/binarycodec/types/issue.py b/xrpl/core/binarycodec/types/issue.py new file mode 100644 index 000000000..1c5b5029a --- /dev/null +++ b/xrpl/core/binarycodec/types/issue.py @@ -0,0 +1,94 @@ +"""Codec for serializing and deserializing issued currency fields.""" + +from __future__ import annotations + +from typing import Any, Dict, Optional, 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.currency import Currency +from xrpl.core.binarycodec.types.serialized_type import SerializedType +from xrpl.models.currencies import XRP as XRPModel +from xrpl.models.currencies import IssuedCurrency as IssuedCurrencyModel + + +class Issue(SerializedType): + """Codec for serializing and deserializing issued currency fields.""" + + def __init__(self: Issue, buffer: bytes) -> None: + """ + Construct an Issue from given bytes. + + Args: + buffer: The byte buffer that will be used to store the serialized + encoding of this field. + """ + super().__init__(buffer) + + @classmethod + def from_value(cls: Type[Issue], value: Dict[str, str]) -> Issue: + """ + Construct an Issue object from a string or dictionary representation + of an issued currency. + + Args: + value: The dictionary to construct an Issue object from. + + Returns: + An Issue object constructed from value. + + Raises: + XRPLBinaryCodecException: If the Issue representation is invalid. + """ + if XRPModel.is_dict_of_model(value): + currency_bytes = bytes(Currency.from_value(value["currency"])) + return cls(currency_bytes) + + if IssuedCurrencyModel.is_dict_of_model(value): + currency_bytes = bytes(Currency.from_value(value["currency"])) + issuer_bytes = bytes(AccountID.from_value(value["issuer"])) + return cls(currency_bytes + issuer_bytes) + + raise XRPLBinaryCodecException( + "Invalid type to construct an Issue: expected str or dict," + f" received {value.__class__.__name__}." + ) + + @classmethod + def from_parser( + cls: Type[Issue], + parser: BinaryParser, + length_hint: Optional[int] = None, + ) -> Issue: + """ + Construct an Issue object from an existing BinaryParser. + + Args: + parser: The parser to construct the Issue object from. + length_hint: The number of bytes to consume from the parser. + + Returns: + The Issue object constructed from a parser. + """ + currency = Currency.from_parser(parser) + if currency.to_json() == "XRP": + return cls(bytes(currency)) + + issuer = parser.read(20) # the length in bytes of an account ID + return cls(bytes(currency) + issuer) + + def to_json(self: Issue) -> Union[str, Dict[Any, Any]]: + """ + Returns the JSON representation of an issued currency. + + Returns: + The JSON representation of an Issue. + """ + parser = BinaryParser(str(self)) + currency: Union[str, Dict[Any, Any]] = Currency.from_parser(parser).to_json() + if currency == "XRP": + return {"currency": currency} + + issuer = AccountID.from_parser(parser) + return {"currency": currency, "issuer": issuer.to_json()} diff --git a/xrpl/models/__init__.py b/xrpl/models/__init__.py index 059e5f355..bed239c57 100644 --- a/xrpl/models/__init__.py +++ b/xrpl/models/__init__.py @@ -1,6 +1,7 @@ """Top-level exports for the models package.""" from xrpl.models import amounts, currencies, requests, transactions from xrpl.models.amounts import * # noqa: F401, F403 +from xrpl.models.auth_account import AuthAccount from xrpl.models.currencies import * # noqa: F401, F403 from xrpl.models.exceptions import XRPLModelException from xrpl.models.path import Path, PathStep @@ -13,6 +14,7 @@ "XRPLModelException", "amounts", *amounts.__all__, + "AuthAccount", "currencies", *currencies.__all__, "requests", diff --git a/xrpl/models/auth_account.py b/xrpl/models/auth_account.py new file mode 100644 index 000000000..96cebabbb --- /dev/null +++ b/xrpl/models/auth_account.py @@ -0,0 +1,21 @@ +"""Model used in AMMBid transaction.""" +from __future__ import annotations + +from dataclasses import dataclass + +from xrpl.models.nested_model import NestedModel +from xrpl.models.required import REQUIRED +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AuthAccount(NestedModel): + """Represents one entry in a list of AuthAccounts used in AMMBid transaction.""" + + account: str = REQUIRED # type: ignore + """ + This field is required. + + :meta hide-value: + """ diff --git a/xrpl/models/base_model.py b/xrpl/models/base_model.py index 829113f6f..154344dff 100644 --- a/xrpl/models/base_model.py +++ b/xrpl/models/base_model.py @@ -3,13 +3,14 @@ from __future__ import annotations import json +import logging import re from abc import ABC -from dataclasses import fields +from dataclasses import dataclass, fields 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 @@ -32,8 +33,18 @@ _CAMEL_TO_SNAKE_CASE_REGEX: Final[Pattern[str]] = re.compile( f"(?:{_CAMEL_CASE_LEADING_LOWER}|{_CAMEL_CASE_ABBREVIATION}|{_CAMEL_CASE_TYPICAL})" ) -# used for converting special substrings inside CamelCase fields -SPECIAL_CAMELCASE_STRINGS = ["NFToken"] +# This is used to make exceptions when converting dictionary keys to xrpl JSON +# keys. We snake case keys, but some keys are abbreviations. +ABBREVIATIONS: Final[Dict[str, str]] = { + "amm": "AMM", + "did": "DID", + "id": "ID", + "lp": "LP", + "nftoken": "NFToken", + "unl": "UNL", + "uri": "URI", + "xchain": "XChain", +} BM = TypeVar("BM", bound="BaseModel") # any type inherited from BaseModel @@ -46,7 +57,7 @@ def _key_to_json(field: str) -> str: 3. 'URI' becomes 'uri' """ # convert all special CamelCase substrings to capitalized strings - for spec_str in SPECIAL_CAMELCASE_STRINGS: + for spec_str in ABBREVIATIONS.values(): if spec_str in field: field = field.replace(spec_str, spec_str.capitalize()) @@ -63,6 +74,7 @@ def _value_to_json(value: XRPL_VALUE_TYPE) -> XRPL_VALUE_TYPE: return value +@dataclass(frozen=True) class BaseModel(ABC): """The base class for all model types.""" @@ -115,9 +127,11 @@ def from_dict(cls: Type[BM], value: Dict[str, XRPL_VALUE_TYPE]) -> BM: args = {} for param in value: if param not in class_types: - raise XRPLModelException( + # Do not fail parsing if we encounter an unknown arg + logging.debug( f"{param} not a valid parameter for {cls.__name__}" ) + continue args[param] = cls._from_dict_single_param( param, class_types[param], value[param] @@ -153,7 +167,11 @@ def _from_dict_single_param( return cls._from_dict_single_param( param, param_type_option, param_value ) - except XRPLModelException: + except XRPLModelException as e: + # Uncomment this if you are getting parsing errors with a Union or other collection type + # and want to uncover the parsing errors with each param type option + # print(f"{param_type_option} failed to parse: {e}") + # this Union-ed type did not work, move onto the next one pass @@ -167,6 +185,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) @@ -196,6 +220,30 @@ def _from_dict_single_param( ) raise XRPLModelException(error_message) + @classmethod + def _process_xrpl_json( + cls: Type[BM], value: Union[str, Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Creates a dictionary object based on a JSON or dictionary in the standard XRPL + format. + + Args: + value: The dictionary or JSON string to be processed. + + Returns: + A formatted dictionary instantiated from the input. + """ + if isinstance(value, str): + value = json.loads(value) + + formatted_dict = { + _key_to_json(k): _value_to_json(v) + for (k, v) in cast(Dict[str, XRPL_VALUE_TYPE], value).items() + } + + return formatted_dict + @classmethod def _get_only_init_args(cls: Type[BM], args: Dict[str, Any]) -> Dict[str, Any]: init_keys = {field.name for field in fields(cls) if field.init is True} @@ -227,7 +275,11 @@ def from_xrpl(cls: Type[BM], value: Union[str, Dict[str, Any]]) -> BM: def __post_init__(self: BaseModel) -> None: """Called by dataclasses immediately after __init__.""" - self.validate() + # NOTE: The model validation in this library is intended for writing txs. These + # are not invariants for data obtained from the blockchain, so we disable + # validation by default and require calling validate() directly if it is + # desired. + # self.validate() def validate(self: BaseModel) -> None: """ @@ -292,6 +344,35 @@ def _to_dict_elem(self: BaseModel, elem: Any) -> Any: ] return elem + # NOTE: The upstream library exports nested models like so: + # {"nested_model_name": { "key": "value", ... }} + # This is so that when a nested model is encountered at parse time, it is possible + # to identify what model to use to parse the dict data. For our uses however, the + # schema is well-defined and this technique actively interferes with our ability + # to load JSON data into BigQuery. Therefore, we provide a backdoor to export + # without the nested model behavior by preventing the original method from being + # overriden. + def to_dict_no_nesting(self: BaseModel) -> Dict[str, Any]: + # mypy doesn't realize that BaseModel has a field called __dataclass_fields__ + dataclass_fields = self.__dataclass_fields__.keys() # type: ignore + return { + key: self._to_dict_elem_no_nesting(getattr(self, key)) + for key in dataclass_fields + } + + def _to_dict_elem_no_nesting(self: BaseModel, elem: Any) -> Any: + if isinstance(elem, BaseModel): + return elem.to_dict_no_nesting() + if isinstance(elem, Enum): + return elem.value + if isinstance(elem, list): + return [ + self._to_dict_elem_no_nesting(sub_elem) + for sub_elem in elem + if sub_elem is not None + ] + return elem + def __eq__(self: BaseModel, other: object) -> bool: """Compares a BaseModel to another object to determine if they are equal.""" return isinstance(other, BaseModel) and self.to_dict() == other.to_dict() @@ -299,4 +380,4 @@ def __eq__(self: BaseModel, other: object) -> bool: def __repr__(self: BaseModel) -> str: """Returns a string representation of a BaseModel object""" repr_items = [f"{key}={repr(value)}" for key, value in self.to_dict().items()] - return f"{type(self).__name__}({repr_items})" + return f"{type(self).__name__}({repr_items})" \ No newline at end of file diff --git a/xrpl/models/currencies/currency.py b/xrpl/models/currencies/currency.py index 1c665ff72..4f740a9a3 100644 --- a/xrpl/models/currencies/currency.py +++ b/xrpl/models/currencies/currency.py @@ -8,4 +8,4 @@ from xrpl.models.currencies.issued_currency import IssuedCurrency from xrpl.models.currencies.xrp import XRP -Currency = Union[IssuedCurrency, XRP] +Currency = Union[XRP, IssuedCurrency] diff --git a/xrpl/models/flags.py b/xrpl/models/flags.py index 78f4de22d..8cf3c86a3 100644 --- a/xrpl/models/flags.py +++ b/xrpl/models/flags.py @@ -21,6 +21,22 @@ "asf_require_auth": 0x00000002, "asf_require_dest": 0x00000001, }, + "AMMDeposit": { + "tf_lp_token": 0x00010000, + "tf_single_asset": 0x00080000, + "tf_two_asset": 0x00100000, + "tf_one_asset_lp_token": 0x00200000, + "tf_limit_lp_token": 0x00400000, + }, + "AMMWithdraw": { + "tf_lp_token": 0x00010000, + "tf_withdraw_all": 0x00020000, + "tf_one_asset_withdraw_all": 0x00040000, + "tf_single_asset": 0x00080000, + "tf_two_asset": 0x00100000, + "tf_one_asset_lp_token": 0x00200000, + "tf_limit_lp_token": 0x00400000, + }, "NFTokenCreateOffer": { "tf_sell_token": 0x00000001, }, diff --git a/xrpl/models/requests/__init__.py b/xrpl/models/requests/__init__.py index ae575b383..adde2698f 100644 --- a/xrpl/models/requests/__init__.py +++ b/xrpl/models/requests/__init__.py @@ -1,4 +1,5 @@ """Request models.""" +from xrpl.models.auth_account import AuthAccount from xrpl.models.path import PathStep from xrpl.models.requests.account_channels import AccountChannels from xrpl.models.requests.account_currencies import AccountCurrencies @@ -8,6 +9,7 @@ from xrpl.models.requests.account_objects import AccountObjects, AccountObjectType from xrpl.models.requests.account_offers import AccountOffers from xrpl.models.requests.account_tx import AccountTx +from xrpl.models.requests.amm_info import AMMInfo from xrpl.models.requests.book_offers import BookOffers from xrpl.models.requests.channel_authorize import ChannelAuthorize from xrpl.models.requests.channel_verify import ChannelVerify @@ -52,6 +54,8 @@ "AccountObjectType", "AccountOffers", "AccountTx", + "AMMInfo", + "AuthAccount", "BookOffers", "ChannelAuthorize", "ChannelVerify", diff --git a/xrpl/models/requests/amm_info.py b/xrpl/models/requests/amm_info.py new file mode 100644 index 000000000..cf0c7dd10 --- /dev/null +++ b/xrpl/models/requests/amm_info.py @@ -0,0 +1,30 @@ +"""This request gets information about an Automated Market Maker (AMM) instance.""" +from __future__ import annotations + +from dataclasses import dataclass, field + +from xrpl.models.currencies import Currency +from xrpl.models.requests.request import Request, RequestMethod +from xrpl.models.required import REQUIRED +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMInfo(Request): + """ + The `amm_info` method gets information about an Automated Market Maker + (AMM) instance. + """ + + asset: Currency = REQUIRED # type: ignore + """ + One of the assets of the AMM pool to look up. This field is required. + """ + + asset2: Currency = REQUIRED # type: ignore + """ + The other asset of the AMM pool. This field is required. + """ + + method: RequestMethod = field(default=RequestMethod.AMM_INFO, init=False) diff --git a/xrpl/models/requests/request.py b/xrpl/models/requests/request.py index 325ddd682..cb25d03ea 100644 --- a/xrpl/models/requests/request.py +++ b/xrpl/models/requests/request.py @@ -2,6 +2,7 @@ The base class for all network request types. Represents fields common to all request types. """ + from __future__ import annotations from dataclasses import dataclass @@ -76,6 +77,9 @@ class RequestMethod(str, Enum): # sidechain methods FEDERATOR_INFO = "federator_info" + # amm methods + AMM_INFO = "amm_info" + # generic unknown/unsupported request # (there is no XRPL analog, this model is specific to xrpl-py) GENERIC_REQUEST = "zzgeneric_request" diff --git a/xrpl/models/transactions/__init__.py b/xrpl/models/transactions/__init__.py index cd040e508..c8d969d47 100644 --- a/xrpl/models/transactions/__init__.py +++ b/xrpl/models/transactions/__init__.py @@ -6,13 +6,32 @@ from xrpl.models.transactions.account_delete import AccountDelete from xrpl.models.transactions.account_set import ( AccountSet, + AccountSetAsfFlag, AccountSetFlag, AccountSetFlagInterface, ) +from xrpl.models.transactions.amm_bid import AMMBid, AuthAccount +from xrpl.models.transactions.amm_create import AMMCreate +from xrpl.models.transactions.amm_delete import AMMDelete +from xrpl.models.transactions.amm_deposit import ( + AMMDeposit, + AMMDepositFlag, + AMMDepositFlagInterface, +) +from xrpl.models.transactions.amm_vote import AMMVote +from xrpl.models.transactions.amm_withdraw import ( + AMMWithdraw, + AMMWithdrawFlag, + AMMWithdrawFlagInterface, +) from xrpl.models.transactions.check_cancel import CheckCancel from xrpl.models.transactions.check_cash import CheckCash from xrpl.models.transactions.check_create import CheckCreate +from xrpl.models.transactions.credential_create import CredentialCreate +from xrpl.models.transactions.clawback import Clawback from xrpl.models.transactions.deposit_preauth import DepositPreauth +from xrpl.models.transactions.did_delete import DIDDelete +from xrpl.models.transactions.did_set import DIDSet from xrpl.models.transactions.escrow_cancel import EscrowCancel from xrpl.models.transactions.escrow_create import EscrowCreate from xrpl.models.transactions.escrow_finish import EscrowFinish @@ -30,12 +49,15 @@ NFTokenMintFlag, NFTokenMintFlagInterface, ) +from xrpl.models.transactions.nftoken_modify import NFTokenModify from xrpl.models.transactions.offer_cancel import OfferCancel from xrpl.models.transactions.offer_create import ( OfferCreate, OfferCreateFlag, OfferCreateFlagInterface, ) +from xrpl.models.transactions.oracle_delete import OracleDelete +from xrpl.models.transactions.oracle_set import OracleSet from xrpl.models.transactions.payment import Payment, PaymentFlag, PaymentFlagInterface from xrpl.models.transactions.payment_channel_claim import ( PaymentChannelClaim, @@ -53,16 +75,50 @@ 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", "AccountSet", + "AccountSetAsfFlag", "AccountSetFlag", "AccountSetFlagInterface", + "AMMBid", + "AMMCreate", + "AMMDelete", + "AMMDeposit", + "AMMDepositFlag", + "AMMDepositFlagInterface", + "AMMVote", + "AMMWithdraw", + "AMMWithdrawFlag", + "AMMWithdrawFlagInterface", + "AuthAccount", "CheckCancel", "CheckCash", "CheckCreate", + "CredentialCreate", + "Clawback", "DepositPreauth", + "DIDDelete", + "DIDSet", "EscrowCancel", "EscrowCreate", "EscrowFinish", @@ -76,18 +132,21 @@ "NFTokenMint", "NFTokenMintFlag", "NFTokenMintFlagInterface", + "NFTokenModify", "OfferCancel", "OfferCreate", "OfferCreateFlag", "OfferCreateFlagInterface", + "OracleDelete", + "OracleSet", "Payment", - "PaymentFlag", - "PaymentFlagInterface", "PaymentChannelClaim", "PaymentChannelClaimFlag", "PaymentChannelClaimFlagInterface", "PaymentChannelCreate", "PaymentChannelFund", + "PaymentFlag", + "PaymentFlagInterface", "SetRegularKey", "Signer", "SignerEntry", @@ -98,4 +157,14 @@ "TrustSet", "TrustSetFlag", "TrustSetFlagInterface", + "XChainAccountCreateCommit", + "XChainAddAccountCreateAttestation", + "XChainAddClaimAttestation", + "XChainClaim", + "XChainCommit", + "XChainCreateBridge", + "XChainCreateClaimID", + "XChainModifyBridge", + "XChainModifyBridgeFlag", + "XChainModifyBridgeFlagInterface", ] diff --git a/xrpl/models/transactions/account_set.py b/xrpl/models/transactions/account_set.py index d3ced19f6..3ae891641 100644 --- a/xrpl/models/transactions/account_set.py +++ b/xrpl/models/transactions/account_set.py @@ -1,16 +1,17 @@ """Model for AccountSet transaction type.""" + from __future__ import annotations # Requires Python 3.7+ from dataclasses import dataclass, field from enum import Enum from typing import Dict, Optional -from typing_extensions import Final +from typing_extensions import Final, Self from xrpl.models.flags import FlagInterface 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.utils import KW_ONLY_DATACLASS, require_kwargs_on_init _MAX_TRANSFER_RATE: Final[int] = 2000000000 _MIN_TRANSFER_RATE: Final[int] = 1000000000 @@ -23,8 +24,10 @@ _MAX_DOMAIN_LENGTH: Final[int] = 256 -class AccountSetFlag(int, Enum): +class AccountSetAsfFlag(int, Enum): """ + Enum for AccountSet Flags. + There are several options which can be either enabled or disabled for an account. Account options are represented by different types of flags depending on the situation. The AccountSet transaction type has several "AccountSet Flags" (prefixed @@ -32,7 +35,7 @@ class AccountSetFlag(int, Enum): an option when passed as the ClearFlag parameter. This enum represents those options. - `See AccountSet Flags `_ + `See AccountSet asf Flags `_ """ ASF_ACCOUNT_TXN_ID = 5 @@ -89,40 +92,88 @@ class AccountSetFlag(int, Enum): ASF_AUTHORIZED_NFTOKEN_MINTER = 10 """Allow another account to mint and burn tokens on behalf of this account.""" + ASF_DISABLE_INCOMING_NFTOKEN_OFFER = 12 + """Disallow other accounts from creating NFTokenOffers directed at this account.""" + + ASF_DISABLE_INCOMING_CHECK = 13 + """Disallow other accounts from creating Checks directed at this account.""" + + ASF_DISABLE_INCOMING_PAYCHAN = 14 + """Disallow other accounts from creating PayChannels directed at this account.""" + + ASF_DISABLE_INCOMING_TRUSTLINE = 15 + """Disallow other accounts from creating Trustlines directed at this account.""" + + ASF_ALLOW_TRUSTLINE_CLAWBACK = 16 + """Allow trustline clawback feature""" + + +class AccountSetFlag(int, Enum): + """ + Enum for AccountSet Transaction Flags. + + Transactions of the AccountSet type support additional values in the Flags field. + This enum represents those options. + + `See AccountSet tf Flags `_ + """ + + TF_REQUIRE_DEST_TAG = 0x00010000 + """ + The same as SetFlag: asfRequireDest. + """ + + TF_OPTIONAL_DEST_TAG = 0x00020000 + """ + The same as ClearFlag: asfRequireDest. + """ + + TF_REQUIRE_AUTH = 0x00040000 + """ + The same as SetFlag: asfRequireAuth. + """ + + TF_OPTIONAL_AUTH = 0x00080000 + """ + The same as ClearFlag: asfRequireAuth. + """ + + TF_DISALLOW_XRP = 0x00100000 + """ + The same as SetFlag: asfDisallowXRP. + """ + + TF_ALLOW_XRP = 0x00200000 + """ + The same as ClearFlag: asfDisallowXRP. + """ + class AccountSetFlagInterface(FlagInterface): """ - There are several options which can be either enabled or disabled for an account. - Account options are represented by different types of flags depending on the - situation. The AccountSet transaction type has several "AccountSet Flags" (prefixed - `asf`) that can enable an option when passed as the SetFlag parameter, or disable - an option when passed as the ClearFlag parameter. This TypedDict represents those - options. + Transactions of the AccountSet type support additional values in the Flags field. + This TypedDict represents those options. - `See AccountSet Flags `_ + `See AccountSet tf Flags `_ """ - ASF_ACCOUNT_TXN_ID: bool - ASF_DEFAULT_RIPPLE: bool - ASF_DEPOSIT_AUTH: bool - ASF_DISABLE_MASTER: bool - ASF_DISALLOW_XRP: bool - ASF_GLOBAL_FREEZE: bool - ASF_NO_FREEZE: bool - ASF_REQUIRE_AUTH: bool - ASF_REQUIRE_DEST: bool - ASF_AUTHORIZED_NFTOKEN_MINTER: bool + TF_REQUIRE_DEST_TAG: bool + TF_OPTIONAL_DEST_TAG: bool + TF_REQUIRE_AUTH: bool + TF_OPTIONAL_AUTH: bool + TF_DISALLOW_XRP: bool + TF_ALLOW_XRP: bool @require_kwargs_on_init -@dataclass(frozen=True) +@dataclass(frozen=True, **KW_ONLY_DATACLASS) class AccountSet(Transaction): """ Represents an `AccountSet transaction `_, which modifies the properties of an account in the XRP Ledger. """ - clear_flag: Optional[int] = None + clear_flag: Optional[AccountSetAsfFlag] = None """ Disable a specific `AccountSet Flag `_ @@ -143,7 +194,7 @@ class AccountSet(Transaction): message_key: Optional[str] = None """Set a public key for sending encrypted messages to this account.""" - set_flag: Optional[int] = None + set_flag: Optional[AccountSetAsfFlag] = None """ Enable a specific `AccountSet Flag `_ @@ -167,7 +218,7 @@ class AccountSet(Transaction): """ Sets an alternate account that is allowed to mint NFTokens on this account's behalf using NFTokenMint's `Issuer` field. If set, you must - also set the AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER flag. + also set the AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER flag. """ transaction_type: TransactionType = field( @@ -175,7 +226,7 @@ class AccountSet(Transaction): init=False, ) - def _get_errors(self: AccountSet) -> Dict[str, str]: + def _get_errors(self: Self) -> Dict[str, str]: return { key: value for key, value in { @@ -189,7 +240,7 @@ def _get_errors(self: AccountSet) -> Dict[str, str]: if value is not None } - def _get_tick_size_error(self: AccountSet) -> Optional[str]: + def _get_tick_size_error(self: Self) -> Optional[str]: if self.tick_size is None: return None if self.tick_size > _MAX_TICK_SIZE: @@ -198,7 +249,7 @@ def _get_tick_size_error(self: AccountSet) -> Optional[str]: return f"`tick_size` is below {_MIN_TICK_SIZE}." return None - def _get_transfer_rate_error(self: AccountSet) -> Optional[str]: + def _get_transfer_rate_error(self: Self) -> Optional[str]: if self.transfer_rate is None: return None if self.transfer_rate > _MAX_TRANSFER_RATE: @@ -210,39 +261,42 @@ def _get_transfer_rate_error(self: AccountSet) -> Optional[str]: return f"`transfer_rate` is below {_MIN_TRANSFER_RATE}." return None - def _get_domain_error(self: AccountSet) -> Optional[str]: + def _get_domain_error(self: Self) -> Optional[str]: if self.domain is not None and self.domain.lower() != self.domain: return f"Domain {self.domain} is not lowercase" if self.domain is not None and len(self.domain) > _MAX_DOMAIN_LENGTH: return f"Must not be longer than {_MAX_DOMAIN_LENGTH} characters" return None - def _get_clear_flag_error(self: AccountSet) -> Optional[str]: + def _get_clear_flag_error(self: Self) -> Optional[str]: if self.clear_flag is not None and self.clear_flag == self.set_flag: return "Must not be equal to the set_flag" return None - def _get_nftoken_minter_error(self: AccountSet) -> Optional[str]: + def _get_nftoken_minter_error(self: Self) -> Optional[str]: if ( - self.set_flag != AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER + self.set_flag != AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER and self.nftoken_minter is not None ): return ( "Will not set the minter unless " - "AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER is set" + "AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER is set" ) if ( - self.set_flag == AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER + self.set_flag == AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER and self.nftoken_minter is None ): - return "\ - Must be present if AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER is set" + return ( + "Must be present if AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER " + "is set" + ) if ( - self.clear_flag == AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER + self.clear_flag == AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER and self.nftoken_minter is not None ): return ( - "Must not be present if AccountSetFlag.ASF_AUTHORIZED_NFTOKEN_MINTER " - "is unset using clear_flag" + "Must not be present if " + "AccountSetAsfFlag.ASF_AUTHORIZED_NFTOKEN_MINTER is unset " + "using clear_flag" ) return None diff --git a/xrpl/models/transactions/amm_bid.py b/xrpl/models/transactions/amm_bid.py new file mode 100644 index 000000000..edf21f1bd --- /dev/null +++ b/xrpl/models/transactions/amm_bid.py @@ -0,0 +1,85 @@ +"""Model for AMMBid transaction type.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Optional + +from typing_extensions import Final + +from xrpl.models.amounts import Amount +from xrpl.models.auth_account import AuthAccount +from xrpl.models.currencies import Currency +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 + +_MAX_AUTH_ACCOUNTS: Final[int] = 4 + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMBid(Transaction): + """ + Bid on an Automated Market Maker's (AMM's) auction slot. + + If you win, you can trade against the AMM at a discounted fee until you are outbid + or 24 hours have passed. + If you are outbid before 24 hours have passed, you are refunded part of the cost + of your bid based on how much time remains. + You bid using the AMM's LP Tokens; the amount of a winning bid is returned + to the AMM, decreasing the outstanding balance of LP Tokens. + """ + + asset: Currency = REQUIRED # type: ignore + """ + The definition for one of the assets in the AMM's pool. This field is required. + """ + + asset2: Currency = REQUIRED # type: ignore + """ + The definition for the other asset in the AMM's pool. This field is required. + """ + + bid_min: Optional[Amount] = None + """ + Pay at least this amount for the slot. + Setting this value higher makes it harder for others to outbid you. + If omitted, pay the minimum necessary to win the bid. + """ + + bid_max: Optional[Amount] = None + """ + Pay at most this amount for the slot. + If the cost to win the bid is higher than this amount, the transaction fails. + If omitted, pay as much as necessary to win the bid. + """ + + auth_accounts: Optional[List[AuthAccount]] = None + """ + A list of up to 4 additional accounts that you allow to trade at the discounted fee. + This cannot include the address of the transaction sender. + """ + + transaction_type: TransactionType = field( + default=TransactionType.AMM_BID, + init=False, + ) + + def _get_errors(self: AMMBid) -> Dict[str, str]: + return { + key: value + for key, value in { + **super()._get_errors(), + "auth_accounts": self._get_auth_accounts_error(), + }.items() + if value is not None + } + + def _get_auth_accounts_error(self: AMMBid) -> Optional[str]: + if ( + self.auth_accounts is not None + and len(self.auth_accounts) > _MAX_AUTH_ACCOUNTS + ): + return f"Length must not be greater than {_MAX_AUTH_ACCOUNTS}" + return None diff --git a/xrpl/models/transactions/amm_create.py b/xrpl/models/transactions/amm_create.py new file mode 100644 index 000000000..1fed0bcf5 --- /dev/null +++ b/xrpl/models/transactions/amm_create.py @@ -0,0 +1,78 @@ +"""Model for AMMCreate transaction type.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional + +from typing_extensions import Final + +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 + +AMM_MAX_TRADING_FEE: Final[int] = 1000 + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMCreate(Transaction): + """ + Create a new Automated Market Maker (AMM) instance for trading a pair of + assets (fungible tokens or XRP). + + Creates both an AMM object and a special AccountRoot object to represent the AMM. + Also transfers ownership of the starting balance of both assets from the sender to + the created AccountRoot and issues an initial balance of liquidity provider + tokens (LP Tokens) from the AMM account to the sender. + + Caution: When you create the AMM, you should fund it with (approximately) + equal-value amounts of each asset. + Otherwise, other users can profit at your expense by trading with + this AMM (performing arbitrage). + The currency risk that liquidity providers take on increases with the + volatility (potential for imbalance) of the asset pair. + The higher the trading fee, the more it offsets this risk, + so it's best to set the trading fee based on the volatility of the asset pair. + """ + + amount: Amount = REQUIRED # type: ignore + """ + The first of the two assets to fund this AMM with. This must be a positive amount. + This field is required. + """ + + amount2: Amount = REQUIRED # type: ignore + """ + The second of the two assets to fund this AMM with. This must be a positive amount. + This field is required. + """ + + trading_fee: int = REQUIRED # type: ignore + """ + The fee to charge for trades against this AMM instance, in units of 1/100,000; + a value of 1 is equivalent to 0.001%. + The maximum value is 1000, indicating a 1% fee. + The minimum value is 0. + """ + + transaction_type: TransactionType = field( + default=TransactionType.AMM_CREATE, + init=False, + ) + + def _get_errors(self: AMMCreate) -> Dict[str, str]: + return { + key: value + for key, value in { + **super()._get_errors(), + "trading_fee": self._get_trading_fee_error(), + }.items() + if value is not None + } + + def _get_trading_fee_error(self: AMMCreate) -> Optional[str]: + if self.trading_fee < 0 or self.trading_fee > AMM_MAX_TRADING_FEE: + return f"Must be between 0 and {AMM_MAX_TRADING_FEE}" + return None diff --git a/xrpl/models/transactions/amm_delete.py b/xrpl/models/transactions/amm_delete.py new file mode 100644 index 000000000..c13e65ca8 --- /dev/null +++ b/xrpl/models/transactions/amm_delete.py @@ -0,0 +1,43 @@ +"""Model for AMMDelete transaction type.""" +from __future__ import annotations + +from dataclasses import dataclass, field + +from xrpl.models.currencies import Currency +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 + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMDelete(Transaction): + """ + Delete an empty Automated Market Maker (AMM) instance that could not be fully + deleted automatically. + + Tip: The AMMWithdraw transaction automatically tries to delete an AMM, along with + associated ledger entries such as empty trust lines, if it withdrew all the assets + from the AMM's pool. However, if there are too many trust lines to the AMM account + to remove in one transaction, it may stop before fully removing the AMM. Similarly, + an AMMDelete transaction removes up to a maximum number of trust lines; in extreme + cases, it may take several AMMDelete transactions to fully delete the trust lines + and the associated AMM. In all cases, the AMM ledger entry and AMM account are + deleted by the last such transaction. + """ + + asset: Currency = REQUIRED # type: ignore + """ + The definition for one of the assets in the AMM's pool. This field is required. + """ + + asset2: Currency = REQUIRED # type: ignore + """ + The definition for the other asset in the AMM's pool. This field is required. + """ + + transaction_type: TransactionType = field( + default=TransactionType.AMM_DELETE, + init=False, + ) diff --git a/xrpl/models/transactions/amm_deposit.py b/xrpl/models/transactions/amm_deposit.py new file mode 100644 index 000000000..6a8b273f8 --- /dev/null +++ b/xrpl/models/transactions/amm_deposit.py @@ -0,0 +1,103 @@ +"""Model for AMMDeposit transaction type.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Optional + +from xrpl.models.amounts import Amount, IssuedCurrencyAmount +from xrpl.models.currencies import Currency +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 + + +class AMMDepositFlag(int, Enum): + """ + Transactions of the AMMDeposit type support additional values in the Flags field. + This enum represents those options. + """ + + TF_LP_TOKEN = 0x00010000 + TF_SINGLE_ASSET = 0x00080000 + TF_TWO_ASSET = 0x00100000 + TF_ONE_ASSET_LP_TOKEN = 0x00200000 + TF_LIMIT_LP_TOKEN = 0x00400000 + + +class AMMDepositFlagInterface(FlagInterface): + """ + Transactions of the AMMDeposit type support additional values in the Flags field. + This TypedDict represents those options. + """ + + TF_LP_TOKEN: bool + TF_SINGLE_ASSET: bool + TF_TWO_ASSET: bool + TF_ONE_ASSET_LP_TOKEN: bool + TF_LIMIT_LP_TOKEN: bool + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMDeposit(Transaction): + """ + Deposit funds into an Automated Market Maker (AMM) instance + and receive the AMM's liquidity provider tokens (LP Tokens) in exchange. + + You can deposit one or both of the assets in the AMM's pool. + If successful, this transaction creates a trust line to the AMM Account (limit 0) + to hold the LP Tokens. + """ + + asset: Currency = REQUIRED # type: ignore + """ + The definition for one of the assets in the AMM's pool. This field is required. + """ + + asset2: Currency = REQUIRED # type: ignore + """ + The definition for the other asset in the AMM's pool. This field is required. + """ + + amount: Optional[Amount] = None + """ + The amount of one asset to deposit to the AMM. + If present, this must match the type of one of the assets (tokens or XRP) + in the AMM's pool. + """ + + amount2: Optional[Amount] = None + """ + The amount of another asset to add to the AMM. + If present, this must match the type of the other asset in the AMM's pool + and cannot be the same asset as Amount. + """ + + e_price: Optional[Amount] = None + """ + The maximum effective price, in the deposit asset, to pay + for each LP Token received. + """ + + lp_token_out: Optional[IssuedCurrencyAmount] = None + """ + How many of the AMM's LP Tokens to buy. + """ + + transaction_type: TransactionType = field( + default=TransactionType.AMM_DEPOSIT, + init=False, + ) + + def _get_errors(self: AMMDeposit) -> Dict[str, str]: + errors = super()._get_errors() + if self.amount2 is not None and self.amount is None: + errors["AMMDeposit"] = "Must set `amount` with `amount2`" + elif self.e_price is not None and self.amount is None: + errors["AMMDeposit"] = "Must set `amount` with `e_price`" + elif self.lp_token_out is None and self.amount is None: + errors["AMMDeposit"] = "Must set at least `lp_token_out` or `amount`" + return errors diff --git a/xrpl/models/transactions/amm_vote.py b/xrpl/models/transactions/amm_vote.py new file mode 100644 index 000000000..e955aefaa --- /dev/null +++ b/xrpl/models/transactions/amm_vote.py @@ -0,0 +1,62 @@ +"""Model for AMMVote transaction type.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional + +from xrpl.models.currencies import Currency +from xrpl.models.required import REQUIRED +from xrpl.models.transactions.amm_create import AMM_MAX_TRADING_FEE +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMVote(Transaction): + """ + Vote on the trading fee for an Automated Market Maker (AMM) instance. + + Up to 8 accounts can vote in proportion to the amount of the AMM's LP Tokens + they hold. + Each new vote re-calculates the AMM's trading fee based on a weighted average + of the votes. + """ + + asset: Currency = REQUIRED # type: ignore + """ + The definition for one of the assets in the AMM's pool. This field is required. + """ + + asset2: Currency = REQUIRED # type: ignore + """ + The definition for the other asset in the AMM's pool. This field is required. + """ + + trading_fee: int = REQUIRED # type: ignore + """ + The proposed fee to vote for, in units of 1/100,000; a value of 1 is equivalent + to 0.001%. + The maximum value is 1000, indicating a 1% fee. This field is required. + """ + + transaction_type: TransactionType = field( + default=TransactionType.AMM_VOTE, + init=False, + ) + + def _get_errors(self: AMMVote) -> Dict[str, str]: + return { + key: value + for key, value in { + **super()._get_errors(), + "trading_fee": self._get_trading_fee_error(), + }.items() + if value is not None + } + + def _get_trading_fee_error(self: AMMVote) -> Optional[str]: + if self.trading_fee < 0 or self.trading_fee > AMM_MAX_TRADING_FEE: + return f"Must be between 0 and {AMM_MAX_TRADING_FEE}" + return None diff --git a/xrpl/models/transactions/amm_withdraw.py b/xrpl/models/transactions/amm_withdraw.py new file mode 100644 index 000000000..2c09bb386 --- /dev/null +++ b/xrpl/models/transactions/amm_withdraw.py @@ -0,0 +1,100 @@ +"""Model for AMMWithdraw transaction type.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, Optional + +from xrpl.models.amounts import Amount, IssuedCurrencyAmount +from xrpl.models.currencies import Currency +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 + + +class AMMWithdrawFlag(int, Enum): + """ + Transactions of the AMMWithdraw type support additional values in the Flags field. + This enum represents those options. + """ + + TF_LP_TOKEN = 0x00010000 + TF_WITHDRAW_ALL = 0x00020000 + TF_ONE_ASSET_WITHDRAW_ALL = 0x00040000 + TF_SINGLE_ASSET = 0x00080000 + TF_TWO_ASSET = 0x00100000 + TF_ONE_ASSET_LP_TOKEN = 0x00200000 + TF_LIMIT_LP_TOKEN = 0x00400000 + + +class AMMWithdrawFlagInterface(FlagInterface): + """ + Transactions of the AMMWithdraw type support additional values in the Flags field. + This TypedDict represents those options. + """ + + TF_LP_TOKEN: bool + TF_WITHDRAW_ALL: bool + TF_ONE_ASSET_WITHDRAW_ALL: bool + TF_SINGLE_ASSET: bool + TF_TWO_ASSET: bool + TF_ONE_ASSET_LP_TOKEN: bool + TF_LIMIT_LP_TOKEN: bool + + +@require_kwargs_on_init +@dataclass(frozen=True) +class AMMWithdraw(Transaction): + """ + Withdraw assets from an Automated Market Maker (AMM) instance by returning the + AMM's liquidity provider tokens (LP Tokens). + """ + + asset: Currency = REQUIRED # type: ignore + """ + The definition for one of the assets in the AMM's pool. This field is required. + """ + + asset2: Currency = REQUIRED # type: ignore + """ + The definition for the other asset in the AMM's pool. This field is required. + """ + + amount: Optional[Amount] = None + """ + The amount of one asset to withdraw from the AMM. + This must match the type of one of the assets (tokens or XRP) in the AMM's pool. + """ + + amount2: Optional[Amount] = None + """ + The amount of another asset to withdraw from the AMM. + If present, this must match the type of the other asset in the AMM's pool + and cannot be the same type as Amount. + """ + + e_price: Optional[Amount] = None + """ + The minimum effective price, in LP Token returned, to pay per unit of the asset + to withdraw. + """ + + lp_token_in: Optional[IssuedCurrencyAmount] = None + """ + How many of the AMM's LP Tokens to redeem. + """ + + transaction_type: TransactionType = field( + default=TransactionType.AMM_WITHDRAW, + init=False, + ) + + def _get_errors(self: AMMWithdraw) -> Dict[str, str]: + errors = super()._get_errors() + if self.amount2 is not None and self.amount is None: + errors["AMMWithdraw"] = "Must set `amount` with `amount2`" + elif self.e_price is not None and self.amount is None: + errors["AMMWithdraw"] = "Must set `amount` with `e_price`" + return errors diff --git a/xrpl/models/transactions/clawback.py b/xrpl/models/transactions/clawback.py new file mode 100644 index 000000000..74bb37e9a --- /dev/null +++ b/xrpl/models/transactions/clawback.py @@ -0,0 +1,43 @@ +"""Model for Clawback transaction type and related flags.""" +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict + +from xrpl.models.amounts import IssuedCurrencyAmount, is_issued_currency, is_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 + + +@require_kwargs_on_init +@dataclass(frozen=True) +class Clawback(Transaction): + """The clawback transaction claws back issued funds from token holders.""" + + amount: IssuedCurrencyAmount = REQUIRED # type: ignore + """ + The amount of currency to claw back. The issuer field is used for the token holder's + address, from whom the tokens will be clawed back. + + :meta hide-value: + """ + + transaction_type: TransactionType = field( + default=TransactionType.CLAWBACK, + init=False, + ) + + def _get_errors(self: Clawback) -> Dict[str, str]: + errors = super()._get_errors() + + # Amount transaction errors + if is_xrp(self.amount): + errors["amount"] = "``amount`` cannot be XRP." + + if is_issued_currency(self.amount): + if self.account == self.amount.issuer: + errors["amount"] = "Holder's address is wrong." + + return errors diff --git a/xrpl/models/transactions/credential_create.py b/xrpl/models/transactions/credential_create.py new file mode 100644 index 000000000..711784afa --- /dev/null +++ b/xrpl/models/transactions/credential_create.py @@ -0,0 +1,60 @@ +"""Model for CheckCreate transaction type.""" + +from dataclasses import dataclass, field +from typing import Optional + +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 + + +@require_kwargs_on_init +@dataclass(frozen=True) +class CredentialCreate(Transaction): + """ + Represents a `CredentialCreate `_ transaction, + which creates a Credential object. A Credential object is a credential that can be used to verify the identity of a subject. + """ + + subject: str = REQUIRED # type: ignore + """ + The address of the `account + `_ that can cash the Check. This field is + required. + :meta hide-value: + """ + + send_max: Amount = REQUIRED # type: ignore + """ + Maximum amount of source token the Check is allowed to debit the + sender, including transfer fees on non-XRP tokens. The Check can only + credit the destination with the same token (from the same issuer, for + non-XRP tokens). This field is required. + :meta hide-value: + """ + + destination_tag: Optional[int] = None + """ + An arbitrary `destination tag + `_ that + identifies the reason for the Check, or a hosted recipient to pay. + """ + + expiration: Optional[int] = None + """ + Time after which the Check is no longer valid, in seconds since the + Ripple Epoch. + """ + + invoice_id: Optional[str] = None + """ + Arbitrary 256-bit hash representing a specific reason or identifier for + this Check. + """ + + transaction_type: TransactionType = field( + default=TransactionType.CREDENTIAL_CREATE, + init=False, + ) diff --git a/xrpl/models/transactions/did_delete.py b/xrpl/models/transactions/did_delete.py new file mode 100644 index 000000000..48532198e --- /dev/null +++ b/xrpl/models/transactions/did_delete.py @@ -0,0 +1,20 @@ +"""Model for DIDDelete transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class DIDDelete(Transaction): + """Represents a DIDDelete transaction.""" + + transaction_type: TransactionType = field( + default=TransactionType.DID_DELETE, + init=False, + ) diff --git a/xrpl/models/transactions/did_set.py b/xrpl/models/transactions/did_set.py new file mode 100644 index 000000000..1fdfc2041 --- /dev/null +++ b/xrpl/models/transactions/did_set.py @@ -0,0 +1,80 @@ +"""Model for DIDSet transaction type.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Dict, Optional, Pattern + +from typing_extensions import Final, Self + +from xrpl.models.transactions.transaction import Transaction +from xrpl.models.transactions.types import TransactionType +from xrpl.models.utils import KW_ONLY_DATACLASS, require_kwargs_on_init + +HEX_REGEX: Final[Pattern[str]] = re.compile("[a-fA-F0-9]*") + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class DIDSet(Transaction): + """Represents a DIDSet transaction.""" + + did_document: Optional[str] = None + """ + The DID document associated with the DID. + + To delete the Data, DIDDocument, or URI field from an existing DID ledger + entry, add the field as an empty string. + """ + + data: Optional[str] = None + """ + The public attestations of identity credentials associated with the DID. + To delete the Data, DIDDocument, or URI field from an existing DID ledger + entry, add the field as an empty string. + """ + + uri: Optional[str] = None + """ + The Universal Resource Identifier associated with the DID. + To delete the Data, DIDDocument, or URI field from an existing DID ledger + entry, add the field as an empty string. + """ + + transaction_type: TransactionType = field( + default=TransactionType.DID_SET, + init=False, + ) + + def _get_errors(self: Self) -> Dict[str, str]: + errors = super()._get_errors() + + if self.did_document is None and self.data is None and self.uri is None: + errors["did_set"] = "Must have one of `did_document`, `data`, and `uri`." + # Can return here because there are no fields to process + return errors + + if self.did_document == "" and self.data == "" and self.uri == "": + errors["did_set"] = ( + "At least one of the fields `did_document`, `data`, and `uri` " + + "must have a length greater than zero" + ) + + return errors + + def _process_field(name: str, value: Optional[str]) -> None: + if value is not None: + error_strs = [] + if not bool(HEX_REGEX.fullmatch(value)): + error_strs.append("must be hex") + if len(value) > 256: + error_strs.append("must be <= 256 characters") + if len(error_strs) > 0: + errors[name] = (" and ".join(error_strs) + ".").capitalize() + + _process_field("did_document", self.did_document) + _process_field("data", self.data) + _process_field("uri", self.uri) + + return errors \ No newline at end of file diff --git a/xrpl/models/transactions/nftoken_modify.py b/xrpl/models/transactions/nftoken_modify.py new file mode 100644 index 000000000..930edbd3c --- /dev/null +++ b/xrpl/models/transactions/nftoken_modify.py @@ -0,0 +1,72 @@ +"""Model for NFTokenModify transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional + +from typing_extensions import Final, Self + +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 HEX_REGEX, KW_ONLY_DATACLASS, require_kwargs_on_init + +_MAX_URI_LENGTH: Final[int] = 512 + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +class NFTokenModify(Transaction): + """ + The NFTokenModify transaction modifies an NFToken's URI + if its tfMutable is set to true. + """ + + nftoken_id: str = REQUIRED # type: ignore + """ + Identifies the TokenID of the NFToken object that the + offer references. This field is required. + """ + + owner: Optional[str] = None + """ + Indicates the AccountID of the account that owns the + corresponding NFToken. + """ + + uri: Optional[str] = None + """ + URI that points to the data and/or metadata associated with the NFT. + This field need not be an HTTP or HTTPS URL; it could be an IPFS URI, a + magnet link, immediate data encoded as an RFC2379 "data" URL, or even an + opaque issuer-specific encoding. The URI is not checked for validity. + + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. + """ + + transaction_type: TransactionType = field( + default=TransactionType.NFTOKEN_MODIFY, + init=False, + ) + + def _get_errors(self: Self) -> Dict[str, str]: + return { + key: value + for key, value in { + **super()._get_errors(), + "uri": self._get_uri_error(), + }.items() + if value is not None + } + + def _get_uri_error(self: Self) -> Optional[str]: + if not self.uri: + return "URI must not be empty string" + elif len(self.uri) > _MAX_URI_LENGTH: + return f"URI must not be longer than {_MAX_URI_LENGTH} characters" + + if not HEX_REGEX.fullmatch(self.uri): + return "URI must be encoded in hex" + return None \ No newline at end of file diff --git a/xrpl/models/transactions/oracle_delete.py b/xrpl/models/transactions/oracle_delete.py new file mode 100644 index 000000000..1e8def6c0 --- /dev/null +++ b/xrpl/models/transactions/oracle_delete.py @@ -0,0 +1,27 @@ +"""Model for OracleDelete transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +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 + + +@require_kwargs_on_init +@dataclass(frozen=True) +class OracleDelete(Transaction): + """Delete an Oracle ledger entry.""" + + account: str = REQUIRED # type: ignore + """This account must match the account in the Owner field of the Oracle object.""" + + oracle_document_id: int = REQUIRED # type: ignore + """A unique identifier of the price oracle for the Account.""" + + transaction_type: TransactionType = field( + default=TransactionType.ORACLE_DELETE, + init=False, + ) diff --git a/xrpl/models/transactions/oracle_set.py b/xrpl/models/transactions/oracle_set.py new file mode 100644 index 000000000..4d3652eb3 --- /dev/null +++ b/xrpl/models/transactions/oracle_set.py @@ -0,0 +1,178 @@ +"""Model for OracleSet transaction type.""" + +from __future__ import annotations + +import datetime +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Any + +from typing_extensions import Self + +from xrpl.models.nested_model import NestedModel +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 + +MAX_ORACLE_DATA_SERIES = 10 +MAX_ORACLE_PROVIDER = 256 +MAX_ORACLE_URI = 256 +MAX_ORACLE_SYMBOL_CLASS = 16 + +# epoch offset must equal 946684800 seconds. It represents the diff between the +# genesis of Unix time and Ripple-Epoch time +EPOCH_OFFSET = ( + datetime.datetime(2000, 1, 1) - datetime.datetime(1970, 1, 1) +).total_seconds() + + +@require_kwargs_on_init +@dataclass(frozen=True) +class OracleSet(Transaction): + """Creates a new Oracle ledger entry or updates the fields of an existing one, + using the Oracle ID. + + The oracle provider must complete these steps before submitting this transaction: + + Create or own the XRPL account in the Owner field and have enough XRP to meet the + reserve and transaction fee requirements. + Publish the XRPL account public key, so it can be used for verification by dApps. + Publish a registry of available price oracles with their unique OracleDocumentID . + """ + + account: str = REQUIRED # type: ignore + """This account must match the account in the Owner field of the Oracle object.""" + + oracle_document_id: int = REQUIRED # type: ignore + """A unique identifier of the price oracle for the Account.""" + + provider: Optional[str] = None + """ + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. + + An arbitrary value that identifies an oracle provider, such as Chainlink, Band, or + DIA. This field is a string, up to 256 ASCII hex encoded characters (0x20-0x7E). + This field is required when creating a new Oracle ledger entry, but is optional for + updates. + """ + + uri: Optional[str] = None + """ + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. + + An optional Universal Resource Identifier to reference price data off-chain. This + field is limited to 256 bytes. + """ + + asset_class: Optional[str] = None + """ + This field must be hex-encoded. You can use `xrpl.utils.str_to_hex` to + convert a UTF-8 string to hex. + + Describes the type of asset, such as "currency", "commodity", or "index". This + field is a string, up to 16 ASCII hex encoded characters (0x20-0x7E). This field is + required when creating a new Oracle ledger entry, but is optional for updates. + """ + + last_update_time: int = REQUIRED # type: ignore + """LastUpdateTime is the specific point in time when the data was last updated. + The LastUpdateTime is represented as Unix Time - the number of seconds since + January 1, 1970 (00:00 UTC).""" + + price_data_series: List[PriceData] = REQUIRED # type: ignore + """An array of up to 10 PriceData objects, each representing the price information + for a token pair. More than five PriceData objects require two owner reserves.""" + + transaction_type: TransactionType = field( + default=TransactionType.ORACLE_SET, + init=False, + ) + + def _get_errors(self: Self) -> Dict[str, str]: + errors = super()._get_errors() + + # If price_data_series is not set, do not perform further validation + if "price_data_series" not in errors: + if len(self.price_data_series) == 0: + errors["price_data_series"] = "Field must have a length greater than 0." + + if len(self.price_data_series) > MAX_ORACLE_DATA_SERIES: + errors["price_data_series"] = ( + "Field must have a length less than" + f" or equal to {MAX_ORACLE_DATA_SERIES}" + ) + + # either asset_price and scale are both present or both excluded + for price_data in self.price_data_series: + if (price_data.asset_price is not None) != ( + price_data.scale is not None + ): + errors["price_data_series"] = ( + "Field must have both " + "`AssetPrice` and `Scale` if any are present" + ) + + if self.asset_class is not None and len(self.asset_class) == 0: + errors["asset_class"] = "Field must have a length greater than 0." + + if ( + self.asset_class is not None + and len(self.asset_class) > MAX_ORACLE_SYMBOL_CLASS + ): + errors["asset_class"] = ( + "Field must have a length less than" + f" or equal to {MAX_ORACLE_SYMBOL_CLASS}" + ) + + if self.provider is not None and len(self.provider) == 0: + errors["provider"] = "Field must have a length greater than 0." + + if self.provider is not None and len(self.provider) > MAX_ORACLE_PROVIDER: + errors[ + "provider" + ] = f"Field must have a length less than or equal to {MAX_ORACLE_PROVIDER}." + + if self.uri is not None and len(self.uri) == 0: + errors["uri"] = "Field must have a length greater than 0." + + if self.uri is not None and len(self.uri) > MAX_ORACLE_URI: + errors[ + "uri" + ] = f"Field must have a length less than or equal to {MAX_ORACLE_URI}." + + # check on the last_update_time + if self.last_update_time < EPOCH_OFFSET: + errors["last_update_time"] = ( + "LastUpdateTime must be greater than or equal" + f" to Ripple-Epoch {EPOCH_OFFSET} seconds" + ) + + return errors + + +@require_kwargs_on_init +@dataclass(frozen=True) +class PriceData(NestedModel): + """Represents one PriceData element. It is used in OracleSet transaction""" + + base_asset: str = REQUIRED # type: ignore + """The primary asset in a trading pair. Any valid identifier, such as a stock + symbol, bond CUSIP, or currency code is allowed. For example, in the BTC/USD pair, + BTC is the base asset; in 912810RR9/BTC, 912810RR9 is the base asset.""" + + quote_asset: str = REQUIRED # type: ignore + """The quote asset in a trading pair. The quote asset denotes the price of one unit + of the base asset. For example, in the BTC/USD pair, BTC is the base asset; in + 912810RR9/BTC, 912810RR9 is the base asset.""" + + asset_price: Optional[Any] = None + """The asset price after applying the Scale precision level. It's not included if + the last update transaction didn't include the BaseAsset/QuoteAsset pair.""" + + scale: Optional[int] = None + """The scaling factor to apply to an asset price. For example, if Scale is 6 and + original price is 0.155, then the scaled price is 155000. Valid scale ranges are + 0-10. It's not included if the last update transaction didn't include the + BaseAsset/QuoteAsset pair.""" diff --git a/xrpl/models/transactions/payment.py b/xrpl/models/transactions/payment.py index 9b1267946..af723c602 100644 --- a/xrpl/models/transactions/payment.py +++ b/xrpl/models/transactions/payment.py @@ -138,8 +138,6 @@ def _get_errors(self: Payment) -> Dict[str, str]: "An XRP payment transaction cannot have the same sender and " "destination." ) - - # partial payment errors elif self.has_flag(PaymentFlag.TF_PARTIAL_PAYMENT) and self.send_max is None: errors["send_max"] = "A partial payment must have a `send_max` value." elif self.deliver_min is not None and not self.has_flag( diff --git a/xrpl/models/transactions/pseudo_transactions/set_fee.py b/xrpl/models/transactions/pseudo_transactions/set_fee.py index b78701c61..8d12d86ce 100644 --- a/xrpl/models/transactions/pseudo_transactions/set_fee.py +++ b/xrpl/models/transactions/pseudo_transactions/set_fee.py @@ -1,9 +1,10 @@ """Model for SetFee pseudo-transaction type.""" +from __future__ import annotations + from dataclasses import dataclass, field from typing import Optional -from xrpl.models.required import REQUIRED from xrpl.models.transactions.pseudo_transactions.pseudo_transaction import ( PseudoTransaction, ) @@ -19,9 +20,24 @@ class SetFee(PseudoTransaction): `_ or `reserve requirements `_ as a result of `Fee Voting `_. + + The parameters are different depending on if this is before or after the + `XRPFees Amendment`_ + + Before the XRPFees Amendment which was proposed in rippled 1.10.0 + base_fee, reference_fee_units, reserve_base, and reserve_increment + were required fields. + + After the XRPFees Amendment, base_fee_drops, reserve_base_drops, + and reserve_increment_drops are required fields. + + No SetFee Pseudo Transaction should contain fields from BOTH before + and after the XRPFees amendment. """ - base_fee: str = REQUIRED # type: ignore + # Required BEFORE the XRPFees Amendment + + base_fee: Optional[str] = None """ The charge, in drops of XRP, for the reference transaction, as hex. (This is the transaction cost before scaling for load.) This field is required. @@ -29,32 +45,49 @@ class SetFee(PseudoTransaction): :meta hide-value: """ - reference_fee_units: int = REQUIRED # type: ignore + reference_fee_units: Optional[int] = None """ The cost, in fee units, of the reference transaction. This field is required. :meta hide-value: """ - reserve_base: int = REQUIRED # type: ignore + reserve_base: Optional[int] = None """ The base reserve, in drops. This field is required. :meta hide-value: """ - reserve_increment: int = REQUIRED # type: ignore + reserve_increment: Optional[int] = None """ The incremental reserve, in drops. This field is required. :meta hide-value: """ - ledger_sequence: Optional[int] = None + # Required AFTER the XRPFees Amendment + + base_fee_drops: Optional[str] = None + """ + The charge, in drops of XRP, for the reference transaction, as hex. (This is the + transaction cost before scaling for load.) This field is required. + + :meta hide-value: """ - The index of the ledger version where this pseudo-transaction appears. This - distinguishes the pseudo-transaction from other occurrences of the same change. - This field is omitted for some historical SetFee pseudo-transactions. + + reserve_base_drops: Optional[str] = None + """ + The base reserve, in drops. This field is required. + + :meta hide-value: + """ + + reserve_increment_drops: Optional[str] = None + """ + The incremental reserve, in drops. This field is required. + + :meta hide-value: """ transaction_type: PseudoTransactionType = field( diff --git a/xrpl/models/transactions/transaction.py b/xrpl/models/transactions/transaction.py index 746240f05..49444ac49 100644 --- a/xrpl/models/transactions/transaction.py +++ b/xrpl/models/transactions/transaction.py @@ -7,9 +7,9 @@ from typing_extensions import Final -from xrpl.core.binarycodec import encode +from xrpl.core.binarycodec import decode, encode from xrpl.models.amounts import IssuedCurrencyAmount -from xrpl.models.base_model import BaseModel +from xrpl.models.base_model import ABBREVIATIONS, BaseModel from xrpl.models.exceptions import XRPLModelException from xrpl.models.flags import check_false_flag_definition, interface_to_flag_list from xrpl.models.nested_model import NestedModel @@ -20,14 +20,6 @@ from xrpl.models.utils import require_kwargs_on_init _TRANSACTION_HASH_PREFIX: Final[int] = 0x54584E00 -# This is used to make exceptions when converting dictionary keys to xrpl JSON -# keys. We snake case keys, but some keys are abbreviations. -_ABBREVIATIONS: Final[Dict[str, str]] = { - "unl": "UNL", - "id": "ID", - "uri": "URI", - "nftoken": "NFToken", -} def transaction_json_to_binary_codec_form( @@ -56,11 +48,11 @@ def _key_to_tx_json(key: str) -> str: 1. 'transaction_type' becomes 'TransactionType' 2. 'URI' becomes 'uri' - Known abbreviations (example 2 above) need to be enumerated in _ABBREVIATIONS. + Known abbreviations (example 2 above) need to be enumerated in ABBREVIATIONS. """ return "".join( [ - _ABBREVIATIONS[word] if word in _ABBREVIATIONS else word.capitalize() + ABBREVIATIONS[word] if word in ABBREVIATIONS else word.capitalize() for word in key.split("_") ] ) @@ -69,7 +61,9 @@ def _key_to_tx_json(key: str) -> str: def _value_to_tx_json(value: XRPL_VALUE_TYPE) -> XRPL_VALUE_TYPE: # IssuedCurrencyAmount and PathStep are special cases and should not be snake cased # and only contain primitive members - if IssuedCurrencyAmount.is_dict_of_model(value) or PathStep.is_dict_of_model(value): + if isinstance(value, list) and all(PathStep.is_dict_of_model(v) for v in value): + return value + if IssuedCurrencyAmount.is_dict_of_model(value): return value if isinstance(value, dict): return transaction_json_to_binary_codec_form(value) @@ -255,7 +249,13 @@ class Transaction(BaseModel): transaction. Automatically added during signing. """ + network_id: Optional[int] = None + """The network id of the transaction.""" + def _get_errors(self: Transaction) -> Dict[str, str]: + # import must be here to avoid circular dependencies + from xrpl.wallet.main import Wallet + errors = super()._get_errors() if self.ticket_sequence is not None and ( (self.sequence is not None and self.sequence != 0) @@ -265,6 +265,10 @@ def _get_errors(self: Transaction) -> Dict[str, str]: "Transaction" ] = """If ticket_sequence is provided, account_txn_id must be None and sequence must be None or 0""" + + if isinstance(self.account, Wallet): + errors["account"] = "Must pass in `wallet.address`, not `wallet`." + return errors def to_dict(self: Transaction) -> Dict[str, Any]: @@ -316,6 +320,15 @@ def to_xrpl(self: Transaction) -> Dict[str, Any]: """ return transaction_json_to_binary_codec_form(self.to_dict()) + def blob(self: Transaction) -> str: + """ + Creates the canonical binary format of the Transaction object. + + Returns: + The binary-encoded object, as a hexadecimal string. + """ + return encode(self.to_xrpl()) + @classmethod def from_dict(cls: Type[T], value: Dict[str, Any]) -> T: """ @@ -371,6 +384,24 @@ def has_flag(self: Transaction, flag: int) -> bool: else: # is List[int] return flag in self.flags + def is_signed(self: Transaction) -> bool: + """ + Checks if a transaction has been signed. + + Returns: + Whether the transaction has been signed + """ + if self.signers: + for signer in self.signers: + if ( + signer.signing_pub_key is None or len(signer.signing_pub_key) <= 0 + ) or (signer.txn_signature is None or len(signer.txn_signature) <= 0): + return False + return True + return ( + self.signing_pub_key is not None and len(self.signing_pub_key) > 0 + ) and (self.txn_signature is not None and len(self.txn_signature) > 0) + def get_hash(self: Transaction) -> str: """ Hashes the Transaction object as the ledger does. Only valid for signed @@ -382,7 +413,7 @@ def get_hash(self: Transaction) -> str: Raises: XRPLModelException: if the Transaction is unsigned. """ - if self.txn_signature is None: + if self.txn_signature is None and self.signers is None: raise XRPLModelException( "Cannot get the hash from an unsigned Transaction." ) @@ -424,3 +455,61 @@ def get_transaction_type( return pseudo_transaction_types[transaction_type] raise XRPLModelException(f"{transaction_type} is not a valid Transaction type") + + class Config: + smart_union = True + + @staticmethod + def from_blob(tx_blob: str) -> Transaction: + """ + Decodes a transaction blob. + + Args: + tx_blob: the tx blob to decode. + + Returns: + The formatted transaction. + """ + return Transaction.from_xrpl(decode(tx_blob)) + + @classmethod + def from_xrpl(cls: Type[T], value: Union[str, Dict[str, Any]]) -> T: + """ + Creates a Transaction object based on a JSON or JSON-string representation of + data + + In Payment transactions, the DeliverMax field is renamed to the Amount field. + + Args: + value: The dictionary or JSON string to be instantiated. + + Returns: + A Transaction object instantiated from the input. + + Raises: + XRPLModelException: If Payment transactions have different values for + amount and deliver_max fields + """ + processed_value = cls._process_xrpl_json(value) + + # handle the deliver_max alias in Payment transactions + if ( + "transaction_type" in processed_value + and processed_value["transaction_type"] == "Payment" + ) and "deliver_max" in processed_value: + if ( + "amount" in processed_value + and processed_value["amount"] != processed_value["deliver_max"] + ): + raise XRPLModelException( + "Error: amount and deliver_max fields must be equal if both are " + + "provided" + ) + else: + processed_value["amount"] = processed_value["deliver_max"] + + # deliver_max field is not recognised in the Payment Request format, + # nor is it supported in the serialization operations. + del processed_value["deliver_max"] + + return cls.from_dict(processed_value) \ No newline at end of file diff --git a/xrpl/models/transactions/types/transaction_type.py b/xrpl/models/transactions/types/transaction_type.py index 3b8a0c809..822fbd4ec 100644 --- a/xrpl/models/transactions/types/transaction_type.py +++ b/xrpl/models/transactions/types/transaction_type.py @@ -8,10 +8,20 @@ class TransactionType(str, Enum): ACCOUNT_DELETE = "AccountDelete" ACCOUNT_SET = "AccountSet" + AMM_BID = "AMMBid" + AMM_CREATE = "AMMCreate" + AMM_DELETE = "AMMDelete" + AMM_DEPOSIT = "AMMDeposit" + AMM_VOTE = "AMMVote" + AMM_WITHDRAW = "AMMWithdraw" CHECK_CANCEL = "CheckCancel" CHECK_CASH = "CheckCash" CHECK_CREATE = "CheckCreate" + CREDENTIAL_CREATE = "CredentialCreate" + CLAWBACK = "Clawback" DEPOSIT_PREAUTH = "DepositPreauth" + DID_DELETE = "DIDDelete" + DID_SET = "DIDSet" ESCROW_CANCEL = "EscrowCancel" ESCROW_CREATE = "EscrowCreate" ESCROW_FINISH = "EscrowFinish" @@ -22,11 +32,22 @@ class TransactionType(str, Enum): NFTOKEN_MINT = "NFTokenMint" OFFER_CANCEL = "OfferCancel" OFFER_CREATE = "OfferCreate" + ORACLE_SET = "OracleSet" + ORACLE_DELETE = "OracleDelete" PAYMENT = "Payment" PAYMENT_CHANNEL_CLAIM = "PaymentChannelClaim" PAYMENT_CHANNEL_CREATE = "PaymentChannelCreate" PAYMENT_CHANNEL_FUND = "PaymentChannelFund" + NFTOKEN_MODIFY = "NFTokenModify" SET_REGULAR_KEY = "SetRegularKey" 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..5c8410334 --- /dev/null +++ b/xrpl/models/transactions/xchain_account_create_commit.py @@ -0,0 +1,73 @@ +"""Model for a XChainAccountCreateCommit transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict + +from typing_extensions import Self + +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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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: Self) -> 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..f4b1a15ce --- /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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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..0fe6d2f69 --- /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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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..d7dadb5ff --- /dev/null +++ b/xrpl/models/transactions/xchain_claim.py @@ -0,0 +1,89 @@ +"""Model for a XChainClaim transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional, Union + +from typing_extensions import Self + +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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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: Self) -> 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..44ff48ccb --- /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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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..29097864d --- /dev/null +++ b/xrpl/models/transactions/xchain_create_bridge.py @@ -0,0 +1,97 @@ +"""Model for a XChainCreateBridge transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Optional + +from typing_extensions import Self + +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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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: Self) -> 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..48906d587 --- /dev/null +++ b/xrpl/models/transactions/xchain_create_claim_id.py @@ -0,0 +1,70 @@ +"""Model for a XChainCreateClaimID transaction type.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict + +from typing_extensions import Self + +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 KW_ONLY_DATACLASS, require_kwargs_on_init +from xrpl.models.xchain_bridge import XChainBridge + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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: Self) -> 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..ec0be90c4 --- /dev/null +++ b/xrpl/models/transactions/xchain_modify_bridge.py @@ -0,0 +1,115 @@ +"""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 typing_extensions import Self + +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 KW_ONLY_DATACLASS, 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, **KW_ONLY_DATACLASS) +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: Self) -> 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 + and not self.has_flag(XChainModifyBridgeFlag.TF_CLEAR_ACCOUNT_CREATE_AMOUNT) + ): + errors["xchain_modify_bridge"] = ( + "Must either change signature_reward, change " + + "min_account_create_amount, or clear 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/utils.py b/xrpl/models/utils.py index 87a3c5c24..99b030cfd 100644 --- a/xrpl/models/utils.py +++ b/xrpl/models/utils.py @@ -1,10 +1,15 @@ """Helper util functions for the models module.""" +import re +from dataclasses import dataclass, is_dataclass +from typing import Any, Dict, List, Optional, Pattern, Type, TypeVar, cast -from dataclasses import is_dataclass -from typing import Any, Dict, List, Type, TypeVar +from typing_extensions import Final from xrpl.models.exceptions import XRPLModelException +HEX_REGEX: Final[Pattern[str]] = re.compile("[a-fA-F0-9]*") + + # Code source for requiring kwargs: # https://gist.github.com/mikeholler/4be180627d3f8fceb55704b729464adb @@ -12,6 +17,38 @@ _Self = TypeVar("_Self") +def _is_kw_only_attr_defined_in_dataclass() -> bool: + """ + Returns: + Utility function to determine if the Python interpreter's version is older + than 3.10. This information is used to check the presence of KW_ONLY attribute + in the dataclass + + For ease of understanding, the output of this function should be equivalent to the + below code, unless the `kw_only` attribute is backported to older versions of + Python interpreter + + Returns: + if sys.version_info.major < 3: + return True + return sys.version_info.minor < 10 + """ + return "kw_only" in dataclass.__kwdefaults__ + + +# Python 3.10 and higer versions of Python enable a new KW_ONLY parameter in dataclass +# This dictionary is used to ensure that Ledger Objects constructors reject +# positional arguments. It obviates the need to maintain decorators for the same +# functionality and enbles IDEs to auto-complete the constructor arguments. +# KW_ONLY Docs: https://docs.python.org/3/library/dataclasses.html#dataclasses.KW_ONLY + +# Unit tests that validate this behavior can be found at test_channel_authorize.py +# and test_sign.py files. +KW_ONLY_DATACLASS = ( + dict(kw_only=True) if _is_kw_only_attr_defined_in_dataclass() else {} +) + + def require_kwargs_on_init(cls: Type[_T]) -> Type[_T]: """ Force a dataclass's init function to only work if called with keyword arguments. @@ -59,7 +96,10 @@ def new_init(self: _Self, *args: List[Any], **kwargs: Dict[str, Any]) -> None: ) original_init(self, **kwargs) - # noinspection PyTypeHints - cls.__init__ = new_init # type: ignore - - return cls + # For Python v3.10 and above, the KW_ONLY attribute in data_class + # performs the functionality of require_kwargs_on_init class. + # When support for older versions of Python (earlier than v3.10) is removed, the + # usage of require_kwargs_on_init decorator on model classes can also be removed. + if not _is_kw_only_attr_defined_in_dataclass(): + cls.__init__ = new_init # type: ignore + return cast(Type[_T], cls) \ No newline at end of file diff --git a/xrpl/models/xchain_bridge.py b/xrpl/models/xchain_bridge.py new file mode 100644 index 000000000..f5db939dd --- /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 KW_ONLY_DATACLASS, require_kwargs_on_init + + +@require_kwargs_on_init +@dataclass(frozen=True, **KW_ONLY_DATACLASS) +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. + """