def test_initializes_if_all_required_fields_present(self): tx = Transaction( account=_ACCOUNT, fee=_FEE, sequence=_SEQUENCE, transaction_type=TransactionType.ACCOUNT_DELETE, ) self.assertTrue(tx.is_valid())
def test_to_dict_includes_type_as_string(self): tx = Transaction( account=_ACCOUNT, fee=_FEE, sequence=_SEQUENCE, transaction_type=TransactionType.ACCOUNT_DELETE, ) value = tx.to_dict()["transaction_type"] self.assertEqual(type(value), str)
def test_to_dict_flag_list(self): tx = Transaction( account=_ACCOUNT, fee=_FEE, sequence=_SEQUENCE, transaction_type=TransactionType.ACCOUNT_DELETE, flags=[0b1, 0b10, 0b100], ) expected_flags = 0b111 value = tx.to_dict()["flags"] self.assertEqual(value, expected_flags)
def _autofill_transaction(transaction: Transaction, client: Client) -> Transaction: transaction_json = transaction.to_dict() if "sequence" not in transaction_json: sequence = get_next_valid_seq_number(transaction_json["account"], client) transaction_json["sequence"] = sequence if "fee" not in transaction_json: transaction_json["fee"] = _calculate_fee_per_transaction_type( transaction, client ) if "last_ledger_sequence" not in transaction_json: ledger_sequence = get_latest_validated_ledger_sequence(client) transaction_json["last_ledger_sequence"] = ledger_sequence + _LEDGER_OFFSET return Transaction.from_dict(transaction_json)
def submit_transaction( transaction: Transaction, client: Client, ) -> Response: """ Submits a transaction to the ledger. Args: transaction: the Transaction to be submitted. client: the network client with which to submit the transaction. Returns: The response from the ledger. Raises: XRPLRequestFailureException: if the rippled API call fails. """ transaction_json = transaction.to_xrpl() transaction_blob = encode(transaction_json) response = client.request(SubmitOnly(tx_blob=transaction_blob)) if response.is_successful(): return response result = cast(Dict[str, Any], response.result) raise XRPLRequestFailureException(result)
def _from_dict_special_cases( cls: Type[BaseModel], param: str, param_type: Type[Any], param_value: Dict[str, Any], ) -> Union[str, Enum, BaseModel, Dict[str, Any]]: """Handles all the recursive/more complex cases for `from_dict`.""" from xrpl.models.amounts import Amount, IssuedCurrencyAmount from xrpl.models.currencies import XRP, Currency, IssuedCurrency from xrpl.models.transactions.transaction import Transaction # TODO: figure out how to make Unions work generically (if possible) if param_type == Amount: # special case, Union if isinstance(param_value, str): return param_value if not isinstance(param_value, dict): raise XRPLModelException( f"{param_type} requires a dictionary of params") return IssuedCurrencyAmount.from_dict(param_value) if param_type == Currency: # special case, Union if not isinstance(param_value, dict): raise XRPLModelException( f"{param_type} requires a dictionary of params") if "currency" in param_value and "issuer" in param_value: return IssuedCurrency.from_dict(param_value) if "currency" in param_value: param_value_copy = {**param_value} del param_value_copy["currency"] return XRP.from_dict(param_value_copy) raise XRPLModelException(f"No valid type for {param}") if param_type == Transaction: # special case, multiple options (could be any Transaction type) if "transaction_type" not in param_value: raise XRPLModelException( f"{param} not a valid parameter for {cls.__name__}") type_str = param_value["transaction_type"] # safely convert type string into the actual type transaction_type = Transaction.get_transaction_type(type_str) param_value_copy = {**param_value} del param_value_copy["transaction_type"] return transaction_type.from_dict(param_value_copy) if param_type in BaseModel.__subclasses__(): # any other BaseModel if not isinstance(param_value, dict): raise XRPLModelException( f"{param_type} requires a dictionary of params") # mypy doesn't know that the If checks that it's a subclass of BaseModel return param_type.from_dict(param_value) # type: ignore if param_type in Enum.__subclasses__(): # mypy doesn't know that the If checks that it's a subclass of Enum return param_type(param_value) # type: ignore return param_value
def send_reliable_submission(transaction: Transaction, client: Client) -> Response: """ Submits a transaction and verifies that it has been included in a validated ledger (or has errored/will not be included for some reason). `See Reliable Transaction Submission <https://xrpl.org/reliable-transaction-submission.html>`_ Args: transaction: the signed transaction to submit to the ledger. Requires a `last_ledger_sequence` param. client: the network client used to submit the transaction to a rippled node. Returns: The response from a validated ledger. Raises: XRPLReliableSubmissionException: if the transaction fails or is missing a `last_ledger_sequence` param. """ transaction_hash = transaction.get_hash() submit_response = submit_transaction(transaction, client) result = cast(Dict[str, Any], submit_response.result) if result["engine_result"] != "tesSUCCESS": result_code = result["engine_result"] result_message = result["engine_result_message"] raise XRPLReliableSubmissionException( f"Transaction failed, {result_code}: {result_message}") return _wait_for_final_transaction_outcome(transaction_hash, client)
def test_from_xrpl_set_fee(self): reference_fee_units = 10 reserve_base = 20000000 reserve_increment = 5000000 base_fee = "000000000000000A" set_fee_dict = { "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp", "BaseFee": base_fee, "Fee": "0", "ReferenceFeeUnits": reference_fee_units, "ReserveBase": reserve_base, "ReserveIncrement": reserve_increment, "Sequence": 0, "SigningPubKey": "", "TransactionType": "SetFee", } expected = SetFee( reference_fee_units=reference_fee_units, reserve_base=reserve_base, reserve_increment=reserve_increment, base_fee=base_fee, ) 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 safe_sign_transaction( transaction: Transaction, wallet: Wallet, check_fee: bool = True, ) -> Transaction: """ Signs a transaction locally, without trusting external rippled nodes. Args: transaction: the transaction to be signed. wallet: the wallet with which to sign the transaction. check_fee: whether to check if the fee is higher than the expected transaction type fee. Defaults to True. Returns: The signed transaction blob. """ if check_fee: _check_fee(transaction) transaction_json = _prepare_transaction(transaction, wallet) serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = sign(serialized_bytes, wallet.private_key) transaction_json["TxnSignature"] = signature return cast(Transaction, Transaction.from_xrpl(transaction_json))
def test_missing_required_field(self): with self.assertRaises(XRPLModelException): # missing account Transaction( fee=_FEE, sequence=_SEQUENCE, transaction_type=TransactionType.ACCOUNT_DELETE, )
def test_from_xrpl(self): dirname = os.path.dirname(__file__) full_filename = "x-codec-fixtures.json" absolute_path = os.path.join(dirname, full_filename) with open(absolute_path) as fixtures_file: fixtures_json = json.load(fixtures_file) for test in fixtures_json["transactions"]: x_json = test["xjson"] r_json = test["rjson"] with self.subTest(json=x_json): tx = Transaction.from_xrpl(x_json) translated_tx = tx.to_xrpl() self.assertEqual(x_json, translated_tx) with self.subTest(json=r_json): tx = Transaction.from_xrpl(r_json) translated_tx = tx.to_xrpl() self.assertEqual(r_json, translated_tx)
def from_dict(cls: Type[SubmitMultisigned], value: Dict[str, Any]) -> SubmitMultisigned: """ Construct a new SubmitMultisigned object from a dictionary of parameters. Args: value: The value to construct the SubmitMultisigned from. Returns: A new SubmitMultisigned object, constructed using the given parameters. """ fixed_value = {**value} if "TransactionType" in fixed_value["tx_json"]: # xrpl format fixed_value["tx_json"] = Transaction.from_xrpl( fixed_value["tx_json"]) return cast(SubmitMultisigned, super(SubmitMultisigned, cls).from_dict(fixed_value))
def safe_sign_transaction( transaction: Transaction, wallet: Wallet, ) -> Transaction: """ Signs a transaction locally, without trusting external rippled nodes. Args: transaction: the transaction to be signed. wallet: the wallet with which to sign the transaction. Returns: The signed transaction blob. """ transaction_json = _prepare_transaction(transaction, wallet) serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = sign(serialized_bytes, wallet.private_key) transaction_json["TxnSignature"] = signature return cast(Transaction, Transaction.from_xrpl(transaction_json))
def safe_sign_transaction(transaction: Transaction, wallet: Wallet) -> str: """ Signs a transaction locally, without trusting external rippled nodes. Args: transaction: the transaction to be signed. wallet: the wallet with which to sign the transaction. Returns: The signed transaction blob. """ # Increment the wallet sequence number, since we're about to use one. wallet.next_sequence_num += 1 transaction_json = transaction_json_to_binary_codec_form( transaction.to_dict()) transaction_json["SigningPubKey"] = wallet.pub_key serialized_for_signing = encode_for_signing(transaction_json) serialized_bytes = bytes.fromhex(serialized_for_signing) signature = sign(serialized_bytes, wallet.priv_key) transaction_json["TxnSignature"] = signature return encode(transaction_json)
def _prepare_transaction( transaction: Transaction, wallet: Wallet, ) -> Dict[str, Any]: """ Prepares a Transaction by converting it to a JSON-like dictionary, converting the field names to CamelCase. If a Client is provided, then it also autofills any relevant fields. Args: transaction: the Transaction to be prepared. wallet: the wallet that will be used for signing. Returns: A JSON-like dictionary that is ready to be signed. Raises: XRPLException: if both LastLedgerSequence and `ledger_offset` are provided, or if an address tag is provided that does not match the X-Address tag. """ transaction_json = transaction_json_to_binary_codec_form( transaction.to_dict()) transaction_json["SigningPubKey"] = wallet.public_key _validate_account_xaddress(transaction_json, "Account", "SourceTag") if "Destination" in transaction_json: _validate_account_xaddress(transaction_json, "Destination", "DestinationTag") # DepositPreauth _convert_to_classic_address(transaction_json, "Authorize") _convert_to_classic_address(transaction_json, "Unauthorize") # EscrowCancel, EscrowFinish _convert_to_classic_address(transaction_json, "Owner") # SetRegularKey _convert_to_classic_address(transaction_json, "RegularKey") return transaction_json
def test_from_xrpl_memos(self): memo_type = "687474703a2f2f6578616d706c652e636f6d2f6d656d6f2f67656e65726963" tx = { "Account": "rnoGkgSpt6AX1nQxZ2qVGx7Fgw6JEcoQas", "TransactionType": "TrustSet", "Fee": "10", "Sequence": 17892983, "Flags": 131072, "Memos": [{ "Memo": { "MemoType": memo_type, "MemoData": "72656e74", } }], "SigningPubKey": "", "LimitAmount": { "currency": "USD", "issuer": "rBPvTKisx7UCGLDtiUZ6mDssXNREuVuL8Y", "value": "10", }, } expected = TrustSet( account="rnoGkgSpt6AX1nQxZ2qVGx7Fgw6JEcoQas", fee="10", sequence=17892983, flags=131072, memos=[Memo( memo_type=memo_type, memo_data="72656e74", )], limit_amount=IssuedCurrencyAmount( currency="USD", issuer="rBPvTKisx7UCGLDtiUZ6mDssXNREuVuL8Y", value="10"), ) self.assertEqual(Transaction.from_xrpl(tx), expected)
def test_from_xrpl_signers(self): txn_sig1 = ( "F80E201FE295AA08678F8542D8FC18EA18D582A0BD19BE77B9A24479418ADBCF4CAD28E7BD" "96137F88DE7736827C7AC6204FBA8DDADB7394E6D704CD1F4CD609") txn_sig2 = ( "036E95B8100EBA2A4A447A3AF24500261BF480A0E8D62EE15D03A697C85E73237A5202BD9A" "F2D9C68B8E8A5FA8B8DA4F8DABABE95E8401C5E57EC783291EF80C") pubkey1 = "ED621D6D4FF54E809397195C4E24EF05E8500A7CE45CDD211F523A892CDBCDCDB2" pubkey2 = "EDD3ABCFF008ECE9ED3073B41913619341519BFF01F07331B56E5D6D2EC4A94A57" tx = { "Account": "rnoGkgSpt6AX1nQxZ2qVGx7Fgw6JEcoQas", "TransactionType": "TrustSet", "Fee": "10", "Sequence": 17892983, "Flags": 131072, "Signers": [ { "Signer": { "Account": "rGVXgBz4NraZcwi5vqpmwPW6P4y74A4YvX", "TxnSignature": txn_sig1, "SigningPubKey": pubkey1, } }, { "Signer": { "Account": "rB5q2wsHeXdQeh2KFzBb1CujNAfSKys6GN", "TxnSignature": txn_sig2, "SigningPubKey": pubkey2, } }, ], "SigningPubKey": "", "LimitAmount": { "currency": "USD", "issuer": "rBPvTKisx7UCGLDtiUZ6mDssXNREuVuL8Y", "value": "10", }, } expected = TrustSet( account="rnoGkgSpt6AX1nQxZ2qVGx7Fgw6JEcoQas", fee="10", sequence=17892983, flags=131072, signers=[ Signer( account="rGVXgBz4NraZcwi5vqpmwPW6P4y74A4YvX", txn_signature=txn_sig1, signing_pub_key=pubkey1, ), Signer( account="rB5q2wsHeXdQeh2KFzBb1CujNAfSKys6GN", txn_signature=txn_sig2, signing_pub_key=pubkey2, ), ], limit_amount=IssuedCurrencyAmount( currency="USD", issuer="rBPvTKisx7UCGLDtiUZ6mDssXNREuVuL8Y", value="10"), ) self.assertEqual(Transaction.from_xrpl(tx), expected)