From b7dfe759bd2174339542a87f40b1784456061eeb Mon Sep 17 00:00:00 2001 From: LimpidCrypto <97235361+LimpidCrypto@users.noreply.github.com> Date: Thu, 28 Jul 2022 22:06:25 +0200 Subject: [PATCH] Get order book changes (#407) * add get_order_book_changes * return real delta from _calculate_delta --- CHANGELOG.md | 3 +- .../txn_parser/test_get_order_book_changes.py | 137 ++++++ .../transaction_jsons/offer_cancelled.json | 90 ++++ .../transaction_jsons/offer_created.json | 93 +++++ .../offer_partially_filled_and_filled.json | 391 ++++++++++++++++++ .../offer_with_expiration.json | 127 ++++++ xrpl/models/transactions/metadata.py | 6 + xrpl/utils/__init__.py | 7 +- xrpl/utils/txn_parser/__init__.py | 3 +- .../txn_parser/get_order_book_changes.py | 20 + xrpl/utils/txn_parser/utils/__init__.py | 11 +- xrpl/utils/txn_parser/utils/balance_parser.py | 51 +-- .../txn_parser/utils/order_book_parser.py | 188 +++++++++ xrpl/utils/txn_parser/utils/parser.py | 47 +++ xrpl/utils/txn_parser/utils/types.py | 44 +- 15 files changed, 1164 insertions(+), 54 deletions(-) create mode 100644 tests/unit/utils/txn_parser/test_get_order_book_changes.py create mode 100644 tests/unit/utils/txn_parser/transaction_jsons/offer_cancelled.json create mode 100644 tests/unit/utils/txn_parser/transaction_jsons/offer_created.json create mode 100644 tests/unit/utils/txn_parser/transaction_jsons/offer_partially_filled_and_filled.json create mode 100644 tests/unit/utils/txn_parser/transaction_jsons/offer_with_expiration.json create mode 100644 xrpl/utils/txn_parser/get_order_book_changes.py create mode 100644 xrpl/utils/txn_parser/utils/order_book_parser.py create mode 100644 xrpl/utils/txn_parser/utils/parser.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 964a0dbf8..cba259dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [[Unreleased]] ### Added: - 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 ### Fixed: - Typing for factory classmethods on models - -### Fixed: - Use properly encoded transactions in `Sign`, `SignFor`, and `SignAndSubmit` ## [1.6.0] - 2022-06-02 diff --git a/tests/unit/utils/txn_parser/test_get_order_book_changes.py b/tests/unit/utils/txn_parser/test_get_order_book_changes.py new file mode 100644 index 000000000..c9f1dacce --- /dev/null +++ b/tests/unit/utils/txn_parser/test_get_order_book_changes.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import json +from unittest import TestCase + +from xrpl.utils import get_order_book_changes + +path_to_json = "tests/unit/utils/txn_parser/transaction_jsons/" +with open(path_to_json + "offer_created.json", "r") as infile: + offer_created = json.load(infile) +with open(path_to_json + "offer_partially_filled_and_filled.json", "r") as infile: + offer_partially_filled_and_filled = json.load(infile) +with open(path_to_json + "offer_cancelled.json", "r") as infile: + offer_cancelled = json.load(infile) +with open(path_to_json + "offer_with_expiration.json", "r") as infile: + offer_with_expiration = json.load(infile) + + +class TestGetOrderBookChanges(TestCase): + def test_offer_created(self: TestGetOrderBookChanges): + actual = get_order_book_changes(offer_created["meta"]) + expected = [ + { + "maker_account": "rJHbqhp9Sea4f43RoUanrDE1gW9MymTLp9", + "offer_changes": [ + { + "flags": 131072, + "taker_gets": {"currency": "XRP", "value": "44.930000"}, + "taker_pays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "14.524821", + }, + "sequence": 71307620, + "status": "created", + "maker_exchange_rate": "0.3232766748275094591586912976", + "expiration_time": 740218424, + } + ], + } + ] + self.assertEqual(actual, expected) + + def test_offer_partially_filled_and_filled(self: TestGetOrderBookChanges): + actual = get_order_book_changes(offer_partially_filled_and_filled["meta"]) + expected = [ + { + "maker_account": "rNzgS71DyJPMnWMA8aS7NqvXP7bNuwyaZo", + "offer_changes": [ + { + "flags": 131072, + "taker_gets": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "-63.7479881398749", + }, + "taker_pays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "-62.4730283770749", + }, + "sequence": 5931, + "status": "filled", + "maker_exchange_rate": "0.9799999999999607517025555356", + } + ], + }, + { + "maker_account": "rPu2feBaViWGmWJhvaF5yLocTVD8FUxd2A", + "offer_changes": [ + { + "flags": 131072, + "taker_gets": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "-117.3895136925395", + }, + "taker_pays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "-115.0877585220975", + }, + "sequence": 67701941, + "status": "partially-filled", + "maker_exchange_rate": "0.980392156862744680458408839", + } + ], + }, + ] + self.assertEqual(actual, expected) + + def test_offer_cancelled(self: TestGetOrderBookChanges): + actual = get_order_book_changes(offer_cancelled["meta"]) + expected = [ + { + "maker_account": "rEUt5Wy44vDKBDaGkUWG6oSTvxmqgnKWCg", + "offer_changes": [ + { + "flags": 0, + "taker_gets": { + "currency": "XDX", + "issuer": "rMJAXYsbNzhwp7FfYnAsYP5ty3R9XnurPo", + "value": "-82335.52909", + }, + "taker_pays": {"currency": "XRP", "value": "-47.504858"}, + "sequence": 70922543, + "status": "cancelled", + "maker_exchange_rate": "0.0005769666937838341642215588998", + } + ], + } + ] + self.assertEqual(actual, expected) + + def test_offer_with_expiration(self: TestGetOrderBookChanges): + actual = get_order_book_changes(offer_with_expiration["meta"]) + expected = [ + { + "maker_account": "rJHHRtt6qmiz71tyGFMZUoxMGakdgqEou5", + "offer_changes": [ + { + "flags": 0, + "taker_gets": {"currency": "XRP", "value": "-50.000000"}, + "taker_pays": { + "currency": "457175696C69627269756D000000000000000000", + "issuer": "rpakCr61Q92abPXJnVboKENmpKssWyHpwu", + "value": "-230.8404670389911", + }, + "sequence": 67782876, + "status": "cancelled", + "maker_exchange_rate": "4.616809340779822", + "expiration_time": 708682031, + } + ], + } + ] + self.assertEqual(actual, expected) diff --git a/tests/unit/utils/txn_parser/transaction_jsons/offer_cancelled.json b/tests/unit/utils/txn_parser/transaction_jsons/offer_cancelled.json new file mode 100644 index 000000000..c2b9ddc76 --- /dev/null +++ b/tests/unit/utils/txn_parser/transaction_jsons/offer_cancelled.json @@ -0,0 +1,90 @@ +{ + "Account": "rEUt5Wy44vDKBDaGkUWG6oSTvxmqgnKWCg", + "Fee": "15", + "Flags": 2147483648, + "LastLedgerSequence": 72374248, + "OfferSequence": 70922543, + "Sequence": 70922544, + "SigningPubKey": "ED024DA5B0A65470D8582D43BDD54D2BD861C9485CFB13EFA2928650CE6686A86D", + "TransactionType": "OfferCancel", + "TxnSignature": "37EB64C659AE77A70FB93A2DFA0AE5282E3B6C0C8E89A3FE08E004D216CB5C8C62219761E3764AFCBA82692DF6204516FD63C0265CC894B360EA06F18401E90E", + "date": 708682141, + "hash": "40FDBE9F71213696FE67CC80B2847E7DA1D3E03A4446D34DCC7DE347137FF436", + "inLedger": 72374247, + "ledger_index": 72374247, + "meta": { + "AffectedNodes": [ + { + "DeletedNode": { + "FinalFields": { + "ExchangeRate": "57147f7b444d5b06", + "Flags": 0, + "RootIndex": "2004607367326F467796C4080157BE0997B306F1CA6AE0C657147F7B444D5B06", + "TakerGetsCurrency": "0000000000000000000000005844580000000000", + "TakerGetsIssuer": "DEC71A7A7168ED930218B15994FD007CE80A4913", + "TakerPaysCurrency": "0000000000000000000000000000000000000000", + "TakerPaysIssuer": "0000000000000000000000000000000000000000" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "2004607367326F467796C4080157BE0997B306F1CA6AE0C657147F7B444D5B06" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rEUt5Wy44vDKBDaGkUWG6oSTvxmqgnKWCg", + "Balance": "1283353963", + "Flags": 0, + "OwnerCount": 2, + "Sequence": 70922545 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "2CAC78E1EEC7340A666AED1DE99DB8B6C8235CB6675903CAB26EA5C62D4F0BB4", + "PreviousFields": { + "Balance": "1283353978", + "OwnerCount": 3, + "Sequence": 70922544 + }, + "PreviousTxnID": "3C6A9B3C72B79DB877AC205ADC1700281C20D08CCD30A6779C3E1B550B558B95", + "PreviousTxnLgrSeq": 72373937 + } + }, + { + "DeletedNode": { + "FinalFields": { + "Account": "rEUt5Wy44vDKBDaGkUWG6oSTvxmqgnKWCg", + "BookDirectory": "2004607367326F467796C4080157BE0997B306F1CA6AE0C657147F7B444D5B06", + "BookNode": "0", + "Flags": 0, + "OwnerNode": "0", + "PreviousTxnID": "3C6A9B3C72B79DB877AC205ADC1700281C20D08CCD30A6779C3E1B550B558B95", + "PreviousTxnLgrSeq": 72373937, + "Sequence": 70922543, + "TakerGets": { + "currency": "XDX", + "issuer": "rMJAXYsbNzhwp7FfYnAsYP5ty3R9XnurPo", + "value": "82335.52909" + }, + "TakerPays": "47504858" + }, + "LedgerEntryType": "Offer", + "LedgerIndex": "A867E960F0D9F88D7BE2885AA3E69C5F2C7F76499D6CC483E353D6616EEBD352" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Flags": 0, + "Owner": "rEUt5Wy44vDKBDaGkUWG6oSTvxmqgnKWCg", + "RootIndex": "FEB025593E20EE2B66382D9F9079719A40AD0DE257CBBD6421092037E39C9A53" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "FEB025593E20EE2B66382D9F9079719A40AD0DE257CBBD6421092037E39C9A53" + } + } + ], + "TransactionIndex": 16, + "TransactionResult": "tesSUCCESS" + }, + "validated": true +} diff --git a/tests/unit/utils/txn_parser/transaction_jsons/offer_created.json b/tests/unit/utils/txn_parser/transaction_jsons/offer_created.json new file mode 100644 index 000000000..09bb70c71 --- /dev/null +++ b/tests/unit/utils/txn_parser/transaction_jsons/offer_created.json @@ -0,0 +1,93 @@ +{ + "Account": "rJHbqhp9Sea4f43RoUanrDE1gW9MymTLp9", + "Expiration": 740218424, + "Fee": "25", + "Flags": 2148007936, + "LastLedgerSequence": 72374322, + "Sequence": 71307620, + "SigningPubKey": "020AF1B1419DB3C80FBB3B2621315F78F03806118094140B6FE535ED809561AC23", + "TakerGets": "44930000", + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "14.524821" + }, + "TransactionType": "OfferCreate", + "TxnSignature": "3045022100C1CA7C960C1BD0B2EA9ACCA13BB429C1C5B558AB2B8AFE9FE3815DB075E07806022030152DC46694A3BF8BA1509944F9388723CB30C678E659613D4B3BF8166CCCA5", + "date": 708682430, + "hash": "463E28521ACA7FB5F4081E7E368FA5AEB76FB641FCB3C92CA9E8971A990CE84A", + "inLedger": 72374321, + "ledger_index": 72374321, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Flags": 0, + "Owner": "rJHbqhp9Sea4f43RoUanrDE1gW9MymTLp9", + "RootIndex": "3418F55643869450792F7047DC92DD661D38E68AC827C378D7C12FE8E017DD2B" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "3418F55643869450792F7047DC92DD661D38E68AC827C378D7C12FE8E017DD2B" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rJHbqhp9Sea4f43RoUanrDE1gW9MymTLp9", + "Balance": "69932774", + "Flags": 0, + "OwnerCount": 3, + "Sequence": 71307621 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "72D49D4E2ECA5153A90413AA4BCEFBFBE748A2B66A96F5E5611089C095BD666D", + "PreviousFields": { + "Balance": "69932799", + "OwnerCount": 2, + "Sequence": 71307620 + }, + "PreviousTxnID": "70C2D1F863FBF18CA7E9B17D7B35A19BD5ED22C8B703D447A21C746E1F66F311", + "PreviousTxnLgrSeq": 72374313 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "ExchangeRate": "4e0b7c2f29ac3197", + "Flags": 0, + "RootIndex": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E0B7C2F29AC3197", + "TakerGetsCurrency": "0000000000000000000000000000000000000000", + "TakerGetsIssuer": "0000000000000000000000000000000000000000", + "TakerPaysCurrency": "0000000000000000000000005553440000000000", + "TakerPaysIssuer": "0A20B3C85F482532A9578DBB3950B85CA06594D1" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E0B7C2F29AC3197" + } + }, + { + "CreatedNode": { + "LedgerEntryType": "Offer", + "LedgerIndex": "DFA50F8A0710C8483D5DEF31E87BFA5DBC617F16045EC187EB21A07B7A2B23DA", + "NewFields": { + "Account": "rJHbqhp9Sea4f43RoUanrDE1gW9MymTLp9", + "BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E0B7C2F29AC3197", + "Expiration": 740218424, + "Flags": 131072, + "Sequence": 71307620, + "TakerGets": "44930000", + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "14.524821" + } + } + } + } + ], + "TransactionIndex": 37, + "TransactionResult": "tesSUCCESS" + }, + "validated": true +} diff --git a/tests/unit/utils/txn_parser/transaction_jsons/offer_partially_filled_and_filled.json b/tests/unit/utils/txn_parser/transaction_jsons/offer_partially_filled_and_filled.json new file mode 100644 index 000000000..82efc8b03 --- /dev/null +++ b/tests/unit/utils/txn_parser/transaction_jsons/offer_partially_filled_and_filled.json @@ -0,0 +1,391 @@ +{ + "Account": "rogue5HnPRSszD9CWGSUz8UGHMVwSSKF6", + "Fee": "10", + "Flags": 655360, + "LastLedgerSequence": 69465967, + "Memos": [ + { + "Memo": { + "MemoData": "FB440A33756EB27101D52DC01A097AABBC8852C104090CB53FF051EB851EB852406E000000000000406E99999999999A3FF0083126E978D5" + } + } + ], + "Sequence": 2978465, + "SigningPubKey": "ED3DC1A8262390DBA0E9926050A7BE377DFCC7937CC94C5F5F24E6BD97D677BA6C", + "TakerGets": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "194.0900821202701" + }, + "TakerPays": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "179.9744397842505" + }, + "TransactionType": "OfferCreate", + "TxnSignature": "45B6A5D968AC63BAF052BFCB68AEBB1C3AE0E2CAA30D8CB753D78CDD2E601748235374B8BF21C1ADF98095BCAA7206C584F6F42BB27D1EF0BDA3F2FB0FD58207", + "date": 697291341, + "hash": "CC7E314E86F40CA8342E991D1F20444B2889110988EB6E5674E219031B07A9D4", + "inLedger": 69465967, + "ledger_index": 69465967, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Flags": 0, + "IndexPrevious": "4", + "Owner": "rNzgS71DyJPMnWMA8aS7NqvXP7bNuwyaZo", + "RootIndex": "E5EDA666FF7FA049136BE952984ABDE9F59BD97C0EEFD1ADB93094E27047573A" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "091BE53F6AFE8089FCD573C96D0E3B7E9145FEA4057924EA298D950F7F667352" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-143.1629304840639" + }, + "Flags": 2228224, + "HighLimit": { + "currency": "USD", + "issuer": "rPu2feBaViWGmWJhvaF5yLocTVD8FUxd2A", + "value": "1000000000" + }, + "HighNode": "0", + "LowLimit": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "0" + }, + "LowNode": "1fce" + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "14BE4365C33BE6BF6AF293C0CF48F8556037541C016DDF37A8AC71C028803206", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-260.7872232039884" + } + }, + "PreviousTxnID": "32DE54D6ADA7060BDED9407327C4401ADC2441A4D80A138654E808B5167C2466", + "PreviousTxnLgrSeq": 69429668 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "181.1375018324144" + }, + "Flags": 1114112, + "HighLimit": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "0" + }, + "HighNode": "26", + "LowLimit": { + "currency": "USD", + "issuer": "rogue5HnPRSszD9CWGSUz8UGHMVwSSKF6", + "value": "100000" + }, + "LowNode": "0" + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "197E542413061266DC6995ADD18728149974484D717A415649AE12E89E297DA5", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "0" + } + }, + "PreviousTxnID": "20D7FEEABCA8072B9F2662F3A23BF87483FEB8055EFBE95D857AE4BBB792D0B8", + "PreviousTxnLgrSeq": 69465965 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-120.4655520405203" + }, + "Flags": 2228224, + "HighLimit": { + "currency": "USD", + "issuer": "rPu2feBaViWGmWJhvaF5yLocTVD8FUxd2A", + "value": "1000000000" + }, + "HighNode": "0", + "LowLimit": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "0" + }, + "LowNode": "991" + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "1B0CE75F44E3498B444ACB80AA40E3212954BBD55D237205182612EA8998877F", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-5.377793518422876" + } + }, + "PreviousTxnID": "32DE54D6ADA7060BDED9407327C4401ADC2441A4D80A138654E808B5167C2466", + "PreviousTxnLgrSeq": 69429668 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-32143.92279120974" + }, + "Flags": 2228224, + "HighLimit": { + "currency": "USD", + "issuer": "rNzgS71DyJPMnWMA8aS7NqvXP7bNuwyaZo", + "value": "1000000000" + }, + "HighNode": "0", + "LowLimit": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "0" + }, + "LowNode": "29d" + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "3924BC09BEECCE4B411D17789803B66074F248365D70A9757A21478352D75383", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-32207.79827532589" + } + }, + "PreviousTxnID": "3778786185BB8335BB3B14258CAF060630EE91ACFC6B7362D5ED6333DC5B3109", + "PreviousTxnLgrSeq": 69465965 + } + }, + { + "DeletedNode": { + "FinalFields": { + "Account": "rNzgS71DyJPMnWMA8aS7NqvXP7bNuwyaZo", + "BookDirectory": "E8900917D0B37ED23351E4A7276E10B5BE987CCD4EA2A08A5422D10C4ECC8000", + "BookNode": "0", + "Flags": 131072, + "OwnerNode": "5", + "PreviousTxnID": "3778786185BB8335BB3B14258CAF060630EE91ACFC6B7362D5ED6333DC5B3109", + "PreviousTxnLgrSeq": 69465965, + "Sequence": 5931, + "TakerGets": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "0" + }, + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "0" + } + }, + "LedgerEntryType": "Offer", + "LedgerIndex": "80353A895BD8033894C868B6BCC01B8CA23FC12E82769122ECDAF9880BF31274", + "PreviousFields": { + "TakerGets": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "63.7479881398749" + }, + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "62.4730283770749" + } + } + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-8427.912727359367" + }, + "Flags": 2228224, + "HighLimit": { + "currency": "USD", + "issuer": "rNzgS71DyJPMnWMA8aS7NqvXP7bNuwyaZo", + "value": "1000000000" + }, + "HighNode": "0", + "LowLimit": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "0" + }, + "LowNode": "4af" + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "9723C56F7296CD00954B197C97C2EEE149CA42542F025F067A0037B0D4734CE8", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "-8365.439698982293" + } + }, + "PreviousTxnID": "3778786185BB8335BB3B14258CAF060630EE91ACFC6B7362D5ED6333DC5B3109", + "PreviousTxnLgrSeq": 69465965 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "0" + }, + "Flags": 1114112, + "HighLimit": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "0" + }, + "HighNode": "326", + "LowLimit": { + "currency": "USD", + "issuer": "rogue5HnPRSszD9CWGSUz8UGHMVwSSKF6", + "value": "10000000" + }, + "LowNode": "0" + }, + "LedgerEntryType": "RippleState", + "LedgerIndex": "CE8BC6520C7A30953A4AC6733394E6924DAA4C3CB16CFB9EAC02EA7494417D59", + "PreviousFields": { + "Balance": { + "currency": "USD", + "issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji", + "value": "177.9159084729707" + } + }, + "PreviousTxnID": "F90D4CB0BBB7F1AAE3F221A062E616595ADBB636C0B0E60891A6EF880E083EB7", + "PreviousTxnLgrSeq": 69465967 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rNzgS71DyJPMnWMA8aS7NqvXP7bNuwyaZo", + "Balance": "24135271925", + "Flags": 0, + "MessageKey": "02000000000000000000000000C03555B48C4398613CE55C8B728FFF2C91265101", + "OwnerCount": 34, + "Sequence": 5936 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "E3795FE99A17BDE69DC0234FF295C811302E451B773A8D96EAAF3846628E8A43", + "PreviousFields": { + "OwnerCount": 35 + }, + "PreviousTxnID": "496EA410ACF2B767ADDB3781D55F2E514CD8B34A2B64ED732EE83236E072412F", + "PreviousTxnLgrSeq": 69460853 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rogue5HnPRSszD9CWGSUz8UGHMVwSSKF6", + "AccountTxnID": "CC7E314E86F40CA8342E991D1F20444B2889110988EB6E5674E219031B07A9D4", + "Balance": "2487581399", + "Flags": 0, + "OwnerCount": 216, + "Sequence": 2978466 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "E7C799A822859C2DC1CA293CB3136B6590628B19F81D5C7BA8752B49BB422E84", + "PreviousFields": { + "AccountTxnID": "F90D4CB0BBB7F1AAE3F221A062E616595ADBB636C0B0E60891A6EF880E083EB7", + "Balance": "2487581409", + "Sequence": 2978465 + }, + "PreviousTxnID": "F90D4CB0BBB7F1AAE3F221A062E616595ADBB636C0B0E60891A6EF880E083EB7", + "PreviousTxnLgrSeq": 69465967 + } + }, + { + "DeletedNode": { + "FinalFields": { + "ExchangeRate": "5422d10c4ecc8000", + "Flags": 0, + "RootIndex": "E8900917D0B37ED23351E4A7276E10B5BE987CCD4EA2A08A5422D10C4ECC8000", + "TakerGetsCurrency": "0000000000000000000000005553440000000000", + "TakerGetsIssuer": "2ADB0B3959D60A6E6991F729E1918B7163925230", + "TakerPaysCurrency": "0000000000000000000000005553440000000000", + "TakerPaysIssuer": "0A20B3C85F482532A9578DBB3950B85CA06594D1" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "E8900917D0B37ED23351E4A7276E10B5BE987CCD4EA2A08A5422D10C4ECC8000" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rPu2feBaViWGmWJhvaF5yLocTVD8FUxd2A", + "BookDirectory": "E8900917D0B37ED23351E4A7276E10B5BE987CCD4EA2A08A5422D49D5E80FAFB", + "BookNode": "0", + "Flags": 131072, + "OwnerNode": "0", + "Sequence": 67701941, + "TakerGets": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "127.4104863074605" + }, + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "124.9122414779025" + } + }, + "LedgerEntryType": "Offer", + "LedgerIndex": "FBF23A35CFD5CED0C995D78063D497F707A681737CB384BC90CD40B466D21F96", + "PreviousFields": { + "TakerGets": { + "currency": "USD", + "issuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "value": "244.8" + }, + "TakerPays": { + "currency": "USD", + "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", + "value": "240" + } + }, + "PreviousTxnID": "898C5F3A8060A3BFAF99D8F0762714D8224DCB2B7FC4F79A95A6A24E259E6FAF", + "PreviousTxnLgrSeq": 69429769 + } + } + ], + "TransactionIndex": 15, + "TransactionResult": "tesSUCCESS" + }, + "validated": true +} diff --git a/tests/unit/utils/txn_parser/transaction_jsons/offer_with_expiration.json b/tests/unit/utils/txn_parser/transaction_jsons/offer_with_expiration.json new file mode 100644 index 000000000..9f4ca0e41 --- /dev/null +++ b/tests/unit/utils/txn_parser/transaction_jsons/offer_with_expiration.json @@ -0,0 +1,127 @@ +{ + "Account": "rJHHRtt6qmiz71tyGFMZUoxMGakdgqEou5", + "Expiration": 708682061, + "Fee": "20", + "Flags": 0, + "LastLedgerSequence": 72374175, + "OfferSequence": 67782876, + "Sequence": 67782878, + "SigningPubKey": "026C5ACDB50D55179AF872ACAB51576DEBB309302FCF1E72889777EC77031F6ADC", + "TakerGets": "50000000", + "TakerPays": { + "currency": "457175696C69627269756D000000000000000000", + "issuer": "rpakCr61Q92abPXJnVboKENmpKssWyHpwu", + "value": "230.7776699646076" + }, + "TransactionType": "OfferCreate", + "TxnSignature": "3044022054DFB5CF41AEA6E224FF5B9C91ACB80ED9544CE06BC3CA71F79BC63DEA9E7D8F0220669706DB65CDED48D4F93BFBD3013C3E03BBB13FE1211C90C6785BA72CADBBBA", + "date": 708681822, + "hash": "6BF848D3996143219272E868F47DD726240CBF4D9FA0F0FA2801CB9AACC79776", + "inLedger": 72374166, + "ledger_index": 72374166, + "meta": { + "AffectedNodes": [ + { + "CreatedNode": { + "LedgerEntryType": "Offer", + "LedgerIndex": "02BAC5DA6C1E7476B6469A9AD560AB618ABC63C7AA1C2CCC55979555DAFFC91C", + "NewFields": { + "Account": "rJHHRtt6qmiz71tyGFMZUoxMGakdgqEou5", + "BookDirectory": "8DFA6E78CAB715332FBF45CB8FCE3BF4FD55FF70592FD0A04F1065D244CE80F8", + "Expiration": 708682061, + "Sequence": 67782878, + "TakerGets": "50000000", + "TakerPays": { + "currency": "457175696C69627269756D000000000000000000", + "issuer": "rpakCr61Q92abPXJnVboKENmpKssWyHpwu", + "value": "230.7776699646076" + } + } + } + }, + { + "DeletedNode": { + "FinalFields": { + "Account": "rJHHRtt6qmiz71tyGFMZUoxMGakdgqEou5", + "BookDirectory": "8DFA6E78CAB715332FBF45CB8FCE3BF4FD55FF70592FD0A04F1066F6B0C0BD2E", + "BookNode": "0", + "Expiration": 708682031, + "Flags": 0, + "OwnerNode": "0", + "PreviousTxnID": "15F78285E2236EF8E7268D9BE0A2BD2341F702D979C4013D0FC5354CF5A70120", + "PreviousTxnLgrSeq": 72374158, + "Sequence": 67782876, + "TakerGets": "50000000", + "TakerPays": { + "currency": "457175696C69627269756D000000000000000000", + "issuer": "rpakCr61Q92abPXJnVboKENmpKssWyHpwu", + "value": "230.8404670389911" + } + }, + "LedgerEntryType": "Offer", + "LedgerIndex": "45C5FA82C61E7F01B191551E532A61187600FA63006A6C7727A450671B350AD4" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Flags": 0, + "Owner": "rJHHRtt6qmiz71tyGFMZUoxMGakdgqEou5", + "RootIndex": "559D3DEC473D3BC0A503E38A8CFB632E17190F78763B96BC6A6D8A3B7360F1D0" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "559D3DEC473D3BC0A503E38A8CFB632E17190F78763B96BC6A6D8A3B7360F1D0" + } + }, + { + "CreatedNode": { + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "8DFA6E78CAB715332FBF45CB8FCE3BF4FD55FF70592FD0A04F1065D244CE80F8", + "NewFields": { + "ExchangeRate": "4f1065d244ce80f8", + "RootIndex": "8DFA6E78CAB715332FBF45CB8FCE3BF4FD55FF70592FD0A04F1065D244CE80F8", + "TakerPaysCurrency": "457175696C69627269756D000000000000000000", + "TakerPaysIssuer": "0C0EA5B382100A2405ADD5A93ECE617FAEEE156D" + } + } + }, + { + "DeletedNode": { + "FinalFields": { + "ExchangeRate": "4f1066f6b0c0bd2e", + "Flags": 0, + "RootIndex": "8DFA6E78CAB715332FBF45CB8FCE3BF4FD55FF70592FD0A04F1066F6B0C0BD2E", + "TakerGetsCurrency": "0000000000000000000000000000000000000000", + "TakerGetsIssuer": "0000000000000000000000000000000000000000", + "TakerPaysCurrency": "457175696C69627269756D000000000000000000", + "TakerPaysIssuer": "0C0EA5B382100A2405ADD5A93ECE617FAEEE156D" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "8DFA6E78CAB715332FBF45CB8FCE3BF4FD55FF70592FD0A04F1066F6B0C0BD2E" + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rJHHRtt6qmiz71tyGFMZUoxMGakdgqEou5", + "Balance": "207351731", + "Flags": 0, + "OwnerCount": 5, + "Sequence": 67782879 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "CD5FFCEA88A52BBD0559E3EFE0B5D385D447B088477B36948A3DF019874E76E6", + "PreviousFields": { + "Balance": "207351751", + "Sequence": 67782878 + }, + "PreviousTxnID": "AFB25623792838FFF9D59E5E24E812E335D467CD75602DE81F2875CD4E869922", + "PreviousTxnLgrSeq": 72374158 + } + } + ], + "TransactionIndex": 33, + "TransactionResult": "tesSUCCESS" + }, + "validated": true +} diff --git a/xrpl/models/transactions/metadata.py b/xrpl/models/transactions/metadata.py index f82664cf5..449fb06f3 100644 --- a/xrpl/models/transactions/metadata.py +++ b/xrpl/models/transactions/metadata.py @@ -14,6 +14,12 @@ class Fields(TypedDict, total=False): LowLimit: Optional[Dict[str, str]] HighLimit: Optional[Dict[str, str]] Balance: Optional[Union[Dict[str, str], str]] + TakerGets: Optional[Union[Dict[str, str], str]] + TakerPays: Optional[Union[Dict[str, str], str]] + Flags: int + Sequence: int + BookDirectory: Optional[str] + Expiration: Optional[int] class CreatedNodeFields(TypedDict): diff --git a/xrpl/utils/__init__.py b/xrpl/utils/__init__.py index fe292f4e2..8ef3cd12f 100644 --- a/xrpl/utils/__init__.py +++ b/xrpl/utils/__init__.py @@ -9,7 +9,11 @@ ripple_time_to_datetime, ripple_time_to_posix, ) -from xrpl.utils.txn_parser import get_balance_changes, get_final_balances +from xrpl.utils.txn_parser import ( + get_balance_changes, + get_final_balances, + get_order_book_changes, +) from xrpl.utils.xrp_conversions import XRPRangeException, drops_to_xrp, xrp_to_drops __all__ = [ @@ -26,4 +30,5 @@ "create_cross_chain_payment", "get_balance_changes", "get_final_balances", + "get_order_book_changes", ] diff --git a/xrpl/utils/txn_parser/__init__.py b/xrpl/utils/txn_parser/__init__.py index e8a4cfc14..56a4300d1 100644 --- a/xrpl/utils/txn_parser/__init__.py +++ b/xrpl/utils/txn_parser/__init__.py @@ -2,5 +2,6 @@ from xrpl.utils.txn_parser.get_balance_changes import get_balance_changes from xrpl.utils.txn_parser.get_final_balances import get_final_balances +from xrpl.utils.txn_parser.get_order_book_changes import get_order_book_changes -__all__ = ["get_balance_changes", "get_final_balances"] +__all__ = ["get_balance_changes", "get_final_balances", "get_order_book_changes"] diff --git a/xrpl/utils/txn_parser/get_order_book_changes.py b/xrpl/utils/txn_parser/get_order_book_changes.py new file mode 100644 index 000000000..f66c69fe1 --- /dev/null +++ b/xrpl/utils/txn_parser/get_order_book_changes.py @@ -0,0 +1,20 @@ +"""Parse offer changes of every offer object involved in the given transaction.""" + +from typing import List + +from xrpl.models import TransactionMetadata +from xrpl.utils.txn_parser.utils import AccountOfferChanges, compute_order_book_changes + + +def get_order_book_changes(metadata: TransactionMetadata) -> List[AccountOfferChanges]: + """ + Parse all order book changes from a transaction's metadata. + + Args: + metadata: Transactions metadata. + + Returns: + All offer changes caused by the transaction. + The offer changes are grouped by their owner accounts. + """ + return compute_order_book_changes(metadata) diff --git a/xrpl/utils/txn_parser/utils/__init__.py b/xrpl/utils/txn_parser/utils/__init__.py index f730c658b..8d9798d6f 100644 --- a/xrpl/utils/txn_parser/utils/__init__.py +++ b/xrpl/utils/txn_parser/utils/__init__.py @@ -1,11 +1,10 @@ """Utility functions for the transaction parser.""" -from xrpl.utils.txn_parser.utils.balance_parser import ( - derive_account_balances, - get_value, -) +from xrpl.utils.txn_parser.utils.balance_parser import derive_account_balances from xrpl.utils.txn_parser.utils.nodes import NormalizedNode, normalize_nodes -from xrpl.utils.txn_parser.utils.types import AccountBalances +from xrpl.utils.txn_parser.utils.order_book_parser import compute_order_book_changes +from xrpl.utils.txn_parser.utils.parser import get_value +from xrpl.utils.txn_parser.utils.types import AccountBalances, AccountOfferChanges __all__ = [ "get_value", @@ -13,4 +12,6 @@ "NormalizedNode", "normalize_nodes", "AccountBalances", + "AccountOfferChanges", + "compute_order_book_changes", ] diff --git a/xrpl/utils/txn_parser/utils/balance_parser.py b/xrpl/utils/txn_parser/utils/balance_parser.py index a649c551a..b1ab1c26f 100644 --- a/xrpl/utils/txn_parser/utils/balance_parser.py +++ b/xrpl/utils/txn_parser/utils/balance_parser.py @@ -1,10 +1,11 @@ """Helper functions for balance parser.""" from decimal import Decimal -from typing import Callable, Dict, List, Optional, Union +from typing import Callable, List, Optional from xrpl.models.transactions.metadata import TransactionMetadata from xrpl.utils.txn_parser.utils.nodes import NormalizedNode, normalize_nodes +from xrpl.utils.txn_parser.utils.parser import group_by_account from xrpl.utils.txn_parser.utils.types import AccountBalance, AccountBalances, Balance from xrpl.utils.xrp_conversions import drops_to_xrp @@ -105,33 +106,6 @@ def _get_trustline_quantity( return [] -def _group_balances( - account_balances: List[AccountBalance], -) -> Dict[str, List[AccountBalance]]: - grouped_balances: Dict[str, List[AccountBalance]] = {} - for balance in account_balances: - account = balance["account"] - if account not in grouped_balances: - grouped_balances[account] = [] - grouped_balances[account].append(balance) - return grouped_balances - - -def get_value(balance: Union[Dict[str, str], str]) -> Decimal: - """ - Get a currency amount's value. - - Args: - balance: Account's balance. - - Returns: - The currency amount's value. - """ - if isinstance(balance, str): - return Decimal(balance) - return Decimal(balance["value"]) - - def _get_node_balances( node: NormalizedNode, value: Optional[Decimal], @@ -157,24 +131,13 @@ def _get_node_balances( return [] -def _group_by_account( +def _group_balances_by_account( account_balances: List[AccountBalance], ) -> List[AccountBalances]: - """ - Groups the account balances in one list for each account. - - Args: - account_balance: All computed balances cause by a transaction. - - Returns: - The grouped computed balances. - """ - grouped = _group_balances(account_balances) + grouped_balances = group_by_account(account_balances) result = [] - for account, account_balances in grouped.items(): - balances: List[Balance] = [] - for balance in account_balances: - balances.append(balance["balance"]) + for account, account_obj in grouped_balances.items(): + balances: List[Balance] = [balance["balance"] for balance in account_obj] result.append( AccountBalances( account=account, @@ -204,4 +167,4 @@ def derive_account_balances( for node in normalize_nodes(metadata) for quantity in _get_node_balances(node, parser(node)) ] - return _group_by_account(quantities) + return _group_balances_by_account(quantities) diff --git a/xrpl/utils/txn_parser/utils/order_book_parser.py b/xrpl/utils/txn_parser/utils/order_book_parser.py new file mode 100644 index 000000000..ea433c8cb --- /dev/null +++ b/xrpl/utils/txn_parser/utils/order_book_parser.py @@ -0,0 +1,188 @@ +"""Helper functions for order book parser.""" + +from decimal import Decimal +from typing import Any, Dict, List, Optional, Union + +from typing_extensions import Literal + +from xrpl.models import TransactionMetadata +from xrpl.utils.txn_parser.utils import NormalizedNode, normalize_nodes +from xrpl.utils.txn_parser.utils.parser import get_value, group_by_account +from xrpl.utils.txn_parser.utils.types import ( + AccountOfferChange, + AccountOfferChanges, + CurrencyAmount, + OfferChange, +) +from xrpl.utils.xrp_conversions import drops_to_xrp + +LSF_SELL = 0x00020000 + + +def _get_offer_status( + node: NormalizedNode, +) -> Literal["created", "partially-filled", "filled", "cancelled"]: + node_type = node["NodeType"] + if node_type == "CreatedNode": + return "created" + elif node_type == "ModifiedNode": + return "partially-filled" + else: # node_type == "DeletedNode" + previous_fields = node.get("PreviousFields") + # a filled offer has previous fields + if previous_fields is not None: + return "filled" + # a cancelled offer has no previous fields + return "cancelled" + + +def _derive_currency_amount( + currency_amount: Union[str, Dict[str, str]] +) -> CurrencyAmount: + if isinstance(currency_amount, str): + return CurrencyAmount(currency="XRP", value=str(drops_to_xrp(currency_amount))) + else: + return CurrencyAmount( + currency=currency_amount["currency"], + issuer=currency_amount["issuer"], + value=currency_amount["value"], + ) + + +def _calculate_delta( + final_amount: CurrencyAmount, + previous_amount: CurrencyAmount, +) -> str: + final_value = get_value(final_amount) + previous_value = get_value(previous_amount) + delta = final_value - previous_value + return str(delta) + + +def _get_change_amount( + node: NormalizedNode, + side: Literal["TakerGets", "TakerPays"], +) -> Optional[CurrencyAmount]: + new_fields = node.get("NewFields") + if new_fields is not None: + new_fields_amount = new_fields.get(side) + if new_fields_amount is not None: + return _derive_currency_amount(new_fields_amount) + final_fields = node.get("FinalFields") + previous_fields = node.get("PreviousFields") + if final_fields is not None: + final_fields_amount = final_fields.get(side) + if final_fields_amount is not None: + final_amount = _derive_currency_amount(final_fields_amount) + if previous_fields is not None: + previous_fields_amount = previous_fields.get(side) + if previous_fields_amount is not None: + previous_amount = _derive_currency_amount(previous_fields_amount) + value = _calculate_delta(final_amount, previous_amount) + changed_amount = final_amount + changed_amount["value"] = value + return changed_amount + return None + changed_amount = final_amount + changed_amount["value"] = str(0 - Decimal(changed_amount["value"])) + return changed_amount + return None + + +def _get_quality( + taker_gets: CurrencyAmount, + taker_pays: CurrencyAmount, +) -> str: + taker_gets_value = Decimal(taker_gets["value"]) + taker_pays_value = Decimal(taker_pays["value"]) + quality = taker_pays_value / taker_gets_value + normalized_quality = str(quality.normalize()) + return normalized_quality + + +def _get_fields( + node: NormalizedNode, + field_name: str, +) -> Optional[Any]: + new_fields = node.get("NewFields") + final_fields = node.get("FinalFields") + if new_fields is not None: + return new_fields.get(field_name) + if final_fields is not None: + return final_fields.get(field_name) + return None + + +def _get_offer_change(node: NormalizedNode) -> Optional[AccountOfferChange]: + status = _get_offer_status(node) + taker_gets = _get_change_amount(node, "TakerGets") + taker_pays = _get_change_amount(node, "TakerPays") + account = _get_fields(node, "Account") + sequence = _get_fields(node, "Sequence") + flags = _get_fields(node, "Flags") + # if required fields are None: return None + if ( + taker_gets is None + or taker_pays is None + or account is None + or sequence is None + or flags is None + ): + return None + + expiration_time = _get_fields(node, "Expiration") + quality = _get_quality(taker_gets, taker_pays) + offer_change = OfferChange( + flags=flags, + taker_gets=taker_gets, + taker_pays=taker_pays, + sequence=sequence, + status=status, + maker_exchange_rate=quality, + ) + if expiration_time is not None: + offer_change["expiration_time"] = expiration_time + return AccountOfferChange(maker_account=account, offer_change=offer_change) + + +def _group_offer_changes_by_account( + account_offer_changes: List[AccountOfferChange], +) -> List[AccountOfferChanges]: + grouped_offer_changes = group_by_account(account_offer_changes) + result = [] + for account, account_obj in grouped_offer_changes.items(): + offer_changes: List[OfferChange] = [ + offer_change["offer_change"] for offer_change in account_obj + ] + result.append( + AccountOfferChanges( + maker_account=account, + offer_changes=offer_changes, + ) + ) + return result + + +def compute_order_book_changes( + metadata: TransactionMetadata, +) -> List[AccountOfferChanges]: + """ + Compute the offer changes from offer objects affected by the transaction. + + Args: + metadata: Transactions metadata. + + Returns: + All offer changes caused by the transaction. + The offer changes are grouped by their owner accounts. + """ + normalized_nodes = normalize_nodes(metadata) + offer_nodes = [ + node for node in normalized_nodes if node["LedgerEntryType"] == "Offer" + ] + offer_changes = [] + for node in offer_nodes: + change = _get_offer_change(node) + if change is not None: + offer_changes.append(change) + return _group_offer_changes_by_account(offer_changes) diff --git a/xrpl/utils/txn_parser/utils/parser.py b/xrpl/utils/txn_parser/utils/parser.py new file mode 100644 index 000000000..6888c2fb4 --- /dev/null +++ b/xrpl/utils/txn_parser/utils/parser.py @@ -0,0 +1,47 @@ +"""Helper functions for parsers.""" + +from decimal import Decimal +from typing import Any, Dict, List, Union + +from xrpl.utils.txn_parser.utils.types import ( + AccountBalance, + AccountOfferChange, + CurrencyAmount, +) + + +def get_value(balance: Union[CurrencyAmount, Dict[str, str], str]) -> Decimal: + """ + Get a currency amount's value. + + Args: + balance: Account's balance. + + Returns: + The currency amount's value. + """ + if isinstance(balance, str): + return Decimal(balance) + return Decimal(balance["value"]) + + +def group_by_account( + account_objects: Union[List[AccountBalance], List[AccountOfferChange]], +) -> Dict[str, Any]: + """ + Groups the account objects in one list for each account. + + Args: + account_objects: All computed objects. + + Returns: + The grouped computed objects. + """ + grouped_objects: Dict[str, Any] = {} + for object in account_objects: + if object.get("account") is not None: + account = str(object.get("account")) + else: + account = str(object.get("maker_account")) + grouped_objects.setdefault(account, []).append(object) + return grouped_objects diff --git a/xrpl/utils/txn_parser/utils/types.py b/xrpl/utils/txn_parser/utils/types.py index 169895dfc..5acf47145 100644 --- a/xrpl/utils/txn_parser/utils/types.py +++ b/xrpl/utils/txn_parser/utils/types.py @@ -2,7 +2,7 @@ from typing import List -from typing_extensions import TypedDict +from typing_extensions import Literal, TypedDict class OptionalIssuer(TypedDict, total=False): @@ -39,3 +39,45 @@ class AccountBalances(TypedDict): account: str balances: List[Balance] + + +class CurrencyAmount(Balance): + """A currency amount model. Has the same fields as `Balance`""" + + pass + + +class OptionalExpiration(TypedDict, total=False): + """The optional expiration field for an offer.""" + + """ + The `Expiration` field is separated from `OfferChange` to make it + optional, while keeping all other fields required. + """ + + expiration_time: int + + +class OfferChange(OptionalExpiration): + """A single offer change.""" + + flags: int + taker_gets: CurrencyAmount + taker_pays: CurrencyAmount + sequence: int + status: Literal["created", "partially-filled", "filled", "cancelled"] + maker_exchange_rate: str + + +class AccountOfferChange(TypedDict): + """A model representing an account's offer change.""" + + maker_account: str + offer_change: OfferChange + + +class AccountOfferChanges(TypedDict): + """A model representing an account's offer changes.""" + + maker_account: str + offer_changes: List[OfferChange]