diff --git a/common/protob/messages-stellar.proto b/common/protob/messages-stellar.proto index 21ca7e8a7..22473f7f4 100644 --- a/common/protob/messages-stellar.proto +++ b/common/protob/messages-stellar.proto @@ -75,6 +75,7 @@ message StellarSignTx { * @next StellarPaymentOp * @next StellarCreateAccountOp * @next StellarPathPaymentStrictReceiveOp + * @next StellarPathPaymentStrictSendOp * @next StellarManageSellOfferOp * @next StellarManageBuyOfferOp * @next StellarCreatePassiveOfferOp @@ -117,13 +118,28 @@ message StellarCreateAccountOp { * @next StellarSignedTx */ message StellarPathPaymentStrictReceiveOp { - optional string source_account = 1; // (optional) source address - required StellarAsset send_asset = 2; - required sint64 send_max = 3; - required string destination_account = 4; - required StellarAsset destination_asset = 5; - required sint64 destination_amount = 6; - repeated StellarAsset paths = 7; + optional string source_account = 1; // (optional) source address + required StellarAsset send_asset = 2; // asset we pay with + required sint64 send_max = 3; // the maximum amount of sendAsset to send (excluding fees) + required string destination_account = 4; // recipient of the payment + required StellarAsset destination_asset = 5; // what they end up with + required sint64 destination_amount = 6; // amount they end up with + repeated StellarAsset paths = 7; // additional hops it must go through to get there +} + +/** + * Request: ask device to confirm this operation type + * @next StellarTxOpRequest + * @next StellarSignedTx + */ +message StellarPathPaymentStrictSendOp { + optional string source_account = 1; // (optional) source address + required StellarAsset send_asset = 2; // asset we pay with + required sint64 send_amount = 3; // amount of sendAsset to send (excluding fees) + required string destination_account = 4; // recipient of the payment + required StellarAsset destination_asset = 5; // what they end up with + required sint64 destination_min = 6; // the minimum amount of dest asset to be received + repeated StellarAsset paths = 7; //additional hops it must go through to get there } /** diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 935d8e43a..fa6e9e363 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -240,6 +240,7 @@ enum MessageType { MessageType_StellarManageDataOp = 220 [(wire_in) = true]; MessageType_StellarBumpSequenceOp = 221 [(wire_in) = true]; MessageType_StellarManageBuyOfferOp = 222 [(wire_in) = true]; + MessageType_StellarPathPaymentStrictSendOp = 223 [(wire_in) = true]; MessageType_StellarSignedTx = 230 [(wire_out) = true]; // Cardano diff --git a/common/tests/fixtures/stellar/sign_tx.json b/common/tests/fixtures/stellar/sign_tx.json index 455db179b..587b68589 100644 --- a/common/tests/fixtures/stellar/sign_tx.json +++ b/common/tests/fixtures/stellar/sign_tx.json @@ -451,6 +451,56 @@ "signature": "A/ccrRMTEy3GXaZ7Lo5frX3ME5fy3bDMrmYaZ8oPtpPk+cnRStbcSAgdTKnRq/dPGRLfh2btvPJD9ETMe1ajDA==" } }, + { + "name": "StellarPathPaymentStrictSendOp", + "parameters": { + "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAAGQAAAAAAAAD6AAAAAEAAAAAG4J3zQAAAABd5CqEAAAAAAAAAAEAAAAAAAAADQAAAAFYAAAAAAAAAHCLqjFfflwWbYS0ZL+zj9YleP460nIn52zLt6F5xFEhAAAAAB3PFpgAAAAAXVVkJGaxhbhDFS6eIZFR28WJICfsQBAaUXvtXKAwwuAAAAACQUJDREVGR0hJSktMAAAAACmElgLCDlg2XxI8Eth6HKRVDRDNIbCuDhjUTj5W/WoVAAAAAAAB4kAAAAACAAAAAUpQWQAAAAAA/Pr8d4Jv8Bq3LDTJiXMR/LjsnkO0R2sDyP5fodnIhB4AAAACQkFOQU5BAAAAAAAAAAAAAL5FO387ZKhuvo51/hHfkV1gShE677dHpUZV6jK+Wgg7AAAAAAAAAAA=", + "address_n": "m/44'/148'/0'", + "network_passphrase": "Test SDF Network ; September 2015", + "tx": { + "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV", + "fee": 100, + "sequence_number": 1000, + "timebounds_start": 461535181, + "timebounds_end": 1575234180, + "memo_type": "NONE" + }, + "operations": [ + { + "_message_type": "StellarPathPaymentStrictSendOp", + "send_asset": { + "type": "ALPHANUM4", + "code": "X", + "issuer": "GBYIXKRRL57FYFTNQS2GJP5TR7LCK6H6HLJHEJ7HNTF3PILZYRISDLNQ" + }, + "send_amount": 500111000, + "destination_account": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V", + "destination_asset": { + "type": "ALPHANUM12", + "code": "ABCDEFGHIJKL", + "issuer": "GAUYJFQCYIHFQNS7CI6BFWD2DSSFKDIQZUQ3BLQODDKE4PSW7VVBKENC" + }, + "destination_min": 123456, + "paths": [ + { + "type": "ALPHANUM4", + "code": "JPY", + "issuer": "GD6PV7DXQJX7AGVXFQ2MTCLTCH6LR3E6IO2EO2YDZD7F7IOZZCCB5DSQ" + }, + { + "type": "ALPHANUM12", + "code": "BANANA", + "issuer": "GC7EKO37HNSKQ3V6RZ274EO7SFOWASQRHLX3OR5FIZK6UMV6LIEDXHGZ" + } + ] + } + ] + }, + "result": { + "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292", + "signature": "ZoJRiq9zQsbv/w5CA4IOqRPsPY46kzrQ0uMgY+Y9Ec6kKk/0ktFt2icEKNvVAKZUYmfEEigKhki/Rt9meN43CQ==" + } + }, { "name": "StellarManageDataOp", "parameters": { diff --git a/core/.changelog.d/1838.added b/core/.changelog.d/1838.added index 657fef175..43800dd76 100644 --- a/core/.changelog.d/1838.added +++ b/core/.changelog.d/1838.added @@ -1 +1 @@ -Stellar: add support for StellarManageBuyOfferOp. +Stellar: add support for StellarManageBuyOfferOp and StellarPathPaymentStrictSendOp. diff --git a/core/src/apps/stellar/README.md b/core/src/apps/stellar/README.md index e0ed21173..5597523c6 100644 --- a/core/src/apps/stellar/README.md +++ b/core/src/apps/stellar/README.md @@ -28,6 +28,7 @@ Stellar transaction is composed of one or more operations. We support all [opera - Manage Buy Offer - Manage Sell Offer - Path Payment Strict Receive +- Path Payment Strict Send - Payment - Set Options diff --git a/core/src/apps/stellar/consts.py b/core/src/apps/stellar/consts.py index 7028db6d9..45cbb0ff1 100644 --- a/core/src/apps/stellar/consts.py +++ b/core/src/apps/stellar/consts.py @@ -17,6 +17,7 @@ if False: StellarManageBuyOfferOp, StellarManageSellOfferOp, StellarPathPaymentStrictReceiveOp, + StellarPathPaymentStrictSendOp, StellarPaymentOp, StellarSetOptionsOp, ) @@ -32,6 +33,7 @@ if False: StellarManageBuyOfferOp, StellarManageSellOfferOp, StellarPathPaymentStrictReceiveOp, + StellarPathPaymentStrictSendOp, StellarPaymentOp, StellarSetOptionsOp, ] @@ -52,6 +54,7 @@ op_codes: dict[int, int] = { MessageType.StellarManageBuyOfferOp: 12, MessageType.StellarManageSellOfferOp: 3, MessageType.StellarPathPaymentStrictReceiveOp: 2, + MessageType.StellarPathPaymentStrictSendOp: 13, MessageType.StellarPaymentOp: 1, MessageType.StellarSetOptionsOp: 5, } @@ -67,6 +70,7 @@ op_wire_types = [ MessageType.StellarManageBuyOfferOp, MessageType.StellarManageSellOfferOp, MessageType.StellarPathPaymentStrictReceiveOp, + MessageType.StellarPathPaymentStrictSendOp, MessageType.StellarPaymentOp, MessageType.StellarSetOptionsOp, ] diff --git a/core/src/apps/stellar/operations/__init__.py b/core/src/apps/stellar/operations/__init__.py index 058e5ae30..e1d93063c 100644 --- a/core/src/apps/stellar/operations/__init__.py +++ b/core/src/apps/stellar/operations/__init__.py @@ -43,6 +43,9 @@ async def process_operation( elif serialize.StellarPathPaymentStrictReceiveOp.is_type_of(op): await layout.confirm_path_payment_strict_receive_op(ctx, op) serialize.write_path_payment_strict_receive_op(w, op) + elif serialize.StellarPathPaymentStrictSendOp.is_type_of(op): + await layout.confirm_path_payment_strict_send_op(ctx, op) + serialize.write_path_payment_strict_send_op(w, op) elif serialize.StellarPaymentOp.is_type_of(op): await layout.confirm_payment_op(ctx, op) serialize.write_payment_op(w, op) diff --git a/core/src/apps/stellar/operations/layout.py b/core/src/apps/stellar/operations/layout.py index e5e3e8596..53c3ab0ce 100644 --- a/core/src/apps/stellar/operations/layout.py +++ b/core/src/apps/stellar/operations/layout.py @@ -11,6 +11,7 @@ from trezor.messages import ( StellarManageDataOp, StellarManageSellOfferOp, StellarPathPaymentStrictReceiveOp, + StellarPathPaymentStrictSendOp, StellarPaymentOp, StellarSetOptionsOp, ) @@ -213,6 +214,27 @@ async def confirm_path_payment_strict_receive_op( await confirm_asset_issuer(ctx, op.send_asset) +async def confirm_path_payment_strict_send_op( + ctx: Context, op: StellarPathPaymentStrictSendOp +) -> None: + await confirm_output( + ctx, + address=op.destination_account, + amount=format_amount(op.destination_min, op.destination_asset), + title="Path Pay at least", + ) + await confirm_asset_issuer(ctx, op.destination_asset) + # confirm what the sender is using to pay + await confirm_amount( + ctx, + title="Debited amount", + amount=format_amount(op.send_amount, op.send_asset), + description="Pay:", + br_type="op_path_payment_strict_send", + ) + await confirm_asset_issuer(ctx, op.send_asset) + + async def confirm_payment_op(ctx: Context, op: StellarPaymentOp) -> None: await confirm_output( ctx, diff --git a/core/src/apps/stellar/operations/serialize.py b/core/src/apps/stellar/operations/serialize.py index e730958e8..5ac50276b 100644 --- a/core/src/apps/stellar/operations/serialize.py +++ b/core/src/apps/stellar/operations/serialize.py @@ -11,6 +11,7 @@ from trezor.messages import ( StellarManageDataOp, StellarManageSellOfferOp, StellarPathPaymentStrictReceiveOp, + StellarPathPaymentStrictSendOp, StellarPaymentOp, StellarSetOptionsOp, ) @@ -99,6 +100,20 @@ def write_path_payment_strict_receive_op( _write_asset(w, p) +def write_path_payment_strict_send_op( + w: Writer, msg: StellarPathPaymentStrictSendOp +) -> None: + _write_asset(w, msg.send_asset) + writers.write_uint64(w, msg.send_amount) + writers.write_pubkey(w, msg.destination_account) + + _write_asset(w, msg.destination_asset) + writers.write_uint64(w, msg.destination_min) + writers.write_uint32(w, len(msg.paths)) + for p in msg.paths: + _write_asset(w, p) + + def write_payment_op(w: Writer, msg: StellarPaymentOp) -> None: writers.write_pubkey(w, msg.destination_account) _write_asset(w, msg.asset) diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index da6cba09b..925178e36 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -131,6 +131,7 @@ if not utils.BITCOIN_ONLY: StellarManageDataOp = 220 StellarBumpSequenceOp = 221 StellarManageBuyOfferOp = 222 + StellarPathPaymentStrictSendOp = 223 StellarSignedTx = 230 CardanoSignTx = 303 CardanoGetPublicKey = 305 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 37c9ec917..ff51725a1 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -136,6 +136,7 @@ if TYPE_CHECKING: StellarManageDataOp = 220 StellarBumpSequenceOp = 221 StellarManageBuyOfferOp = 222 + StellarPathPaymentStrictSendOp = 223 StellarSignedTx = 230 CardanoSignTx = 303 CardanoGetPublicKey = 305 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index c1d357c8a..e401fa96f 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -4798,6 +4798,32 @@ if TYPE_CHECKING: def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["StellarPathPaymentStrictReceiveOp"]: return isinstance(msg, cls) + class StellarPathPaymentStrictSendOp(protobuf.MessageType): + source_account: "str | None" + send_asset: "StellarAsset" + send_amount: "int" + destination_account: "str" + destination_asset: "StellarAsset" + destination_min: "int" + paths: "list[StellarAsset]" + + def __init__( + self, + *, + send_asset: "StellarAsset", + send_amount: "int", + destination_account: "str", + destination_asset: "StellarAsset", + destination_min: "int", + paths: "list[StellarAsset] | None" = None, + source_account: "str | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: protobuf.MessageType) -> TypeGuard["StellarPathPaymentStrictSendOp"]: + return isinstance(msg, cls) + class StellarManageSellOfferOp(protobuf.MessageType): source_account: "str | None" selling_asset: "StellarAsset" diff --git a/legacy/firmware/.changelog.d/1838.added b/legacy/firmware/.changelog.d/1838.added index 657fef175..43800dd76 100644 --- a/legacy/firmware/.changelog.d/1838.added +++ b/legacy/firmware/.changelog.d/1838.added @@ -1 +1 @@ -Stellar: add support for StellarManageBuyOfferOp. +Stellar: add support for StellarManageBuyOfferOp and StellarPathPaymentStrictSendOp. diff --git a/legacy/firmware/fsm.h b/legacy/firmware/fsm.h index aad610bb0..e27dad935 100644 --- a/legacy/firmware/fsm.h +++ b/legacy/firmware/fsm.h @@ -120,6 +120,8 @@ void fsm_msgStellarPaymentOp(const StellarPaymentOp *msg); void fsm_msgStellarCreateAccountOp(const StellarCreateAccountOp *msg); void fsm_msgStellarPathPaymentStrictReceiveOp( const StellarPathPaymentStrictReceiveOp *msg); +void fsm_msgStellarPathPaymentStrictSendOp( + const StellarPathPaymentStrictSendOp *msg); void fsm_msgStellarManageBuyOfferOp(const StellarManageBuyOfferOp *msg); void fsm_msgStellarManageSellOfferOp(const StellarManageSellOfferOp *msg); void fsm_msgStellarCreatePassiveOfferOp(const StellarCreatePassiveOfferOp *msg); diff --git a/legacy/firmware/fsm_msg_stellar.h b/legacy/firmware/fsm_msg_stellar.h index 62c4ba4c2..84a074764 100644 --- a/legacy/firmware/fsm_msg_stellar.h +++ b/legacy/firmware/fsm_msg_stellar.h @@ -128,6 +128,25 @@ void fsm_msgStellarPathPaymentStrictReceiveOp( } } +void fsm_msgStellarPathPaymentStrictSendOp( + const StellarPathPaymentStrictSendOp *msg) { + if (!stellar_confirmPathPaymentStrictSendOp(msg)) return; + + if (stellar_allOperationsConfirmed()) { + RESP_INIT(StellarSignedTx); + + stellar_fillSignedTx(resp); + msg_write(MessageType_MessageType_StellarSignedTx, resp); + layoutHome(); + } + // Request the next operation to sign + else { + RESP_INIT(StellarTxOpRequest); + + msg_write(MessageType_MessageType_StellarTxOpRequest, resp); + } +} + void fsm_msgStellarManageBuyOfferOp(const StellarManageBuyOfferOp *msg) { if (!stellar_confirmManageBuyOfferOp(msg)) return; diff --git a/legacy/firmware/protob/messages-stellar.options b/legacy/firmware/protob/messages-stellar.options index 61ccb204b..748f9aa3a 100644 --- a/legacy/firmware/protob/messages-stellar.options +++ b/legacy/firmware/protob/messages-stellar.options @@ -21,6 +21,10 @@ StellarPathPaymentStrictReceiveOp.source_account max_size:57 StellarPathPaymentStrictReceiveOp.destination_account max_size:57 StellarPathPaymentStrictReceiveOp.paths max_count:5 +StellarPathPaymentStrictSendOp.source_account max_size:57 +StellarPathPaymentStrictSendOp.destination_account max_size:57 +StellarPathPaymentStrictSendOp.paths max_count:5 + StellarManageBuyOfferOp.source_account max_size:57 StellarManageSellOfferOp.source_account max_size:57 diff --git a/legacy/firmware/stellar.c b/legacy/firmware/stellar.c index 4a13e72a4..4167c7811 100644 --- a/legacy/firmware/stellar.c +++ b/legacy/firmware/stellar.c @@ -356,9 +356,9 @@ bool stellar_confirmPathPaymentStrictReceiveOp( strlcpy(str_source_amount, _("Pay Using "), sizeof(str_source_amount)); strlcat(str_source_amount, str_source_number, sizeof(str_source_amount)); - stellar_layoutTransactionDialog(str_source_amount, str_send_asset, NULL, - _("This is the amount debited"), - _("from your account.")); + stellar_layoutTransactionDialog(str_source_amount, str_send_asset, + _("This is the max"), + _("amount debited from your"), _("account.")); if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { stellar_signingAbort(_("User canceled")); return false; @@ -388,6 +388,104 @@ bool stellar_confirmPathPaymentStrictReceiveOp( return true; } +bool stellar_confirmPathPaymentStrictSendOp( + const StellarPathPaymentStrictSendOp *msg) { + if (!stellar_signing) return false; + + if (!stellar_confirmSourceAccount(msg->has_source_account, + msg->source_account)) { + stellar_signingAbort(_("Source account error")); + return false; + } + + // Hash: operation type + stellar_hashupdate_uint32(13); + + // Validate destination account and convert to bytes + uint8_t destination_account_bytes[STELLAR_KEY_SIZE] = {0}; + if (!stellar_getAddressBytes(msg->destination_account, + destination_account_bytes)) { + stellar_signingAbort(_("Invalid destination account")); + return false; + } + const char **str_dest_rows = + stellar_lineBreakAddress(destination_account_bytes); + + // To: G... + char str_to[32] = {0}; + strlcpy(str_to, _("To: "), sizeof(str_to)); + strlcat(str_to, str_dest_rows[0], sizeof(str_to)); + + char str_send_asset[32] = {0}; + char str_dest_asset[32] = {0}; + stellar_format_asset(&(msg->send_asset), str_send_asset, + sizeof(str_send_asset)); + stellar_format_asset(&(msg->destination_asset), str_dest_asset, + sizeof(str_dest_asset)); + + char str_pay_amount[32] = {0}; + char str_amount[32] = {0}; + stellar_format_stroops(msg->destination_min, str_amount, sizeof(str_amount)); + + strlcat(str_pay_amount, str_amount, sizeof(str_pay_amount)); + + // Confirm what the receiver will get + /* + Path Pay at least + 100.0000000 + JPY (G1234ABCDEF) + To: G.... + .... + .... + */ + stellar_layoutTransactionDialog(_("Path Pay at least"), str_pay_amount, + str_dest_asset, str_to, str_dest_rows[1]); + if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { + stellar_signingAbort(_("User canceled")); + return false; + } + + // Confirm what the sender is using to pay + char str_source_amount[32] = {0}; + char str_source_number[32] = {0}; + stellar_format_stroops(msg->send_amount, str_source_number, + sizeof(str_source_number)); + + strlcpy(str_source_amount, _("Pay Using "), sizeof(str_source_amount)); + strlcat(str_source_amount, str_source_number, sizeof(str_source_amount)); + + stellar_layoutTransactionDialog( + str_dest_rows[2], str_source_amount, str_send_asset, + _("This is the amount debited"), _("from your account.")); + if (!protectButton(ButtonRequestType_ButtonRequest_ProtectCall, false)) { + stellar_signingAbort(_("User canceled")); + return false; + } + // Note: no confirmation for intermediate steps since they don't impact the + // user + + // Hash send asset + stellar_hashupdate_asset(&(msg->send_asset)); + // send amount (signed vs. unsigned doesn't matter wrt hashing) + stellar_hashupdate_uint64(msg->send_amount); + // destination account + stellar_hashupdate_address(destination_account_bytes); + // destination asset + stellar_hashupdate_asset(&(msg->destination_asset)); + // destination amount + stellar_hashupdate_uint64(msg->destination_min); + + // paths are stored as an array so hash the number of elements as a uint32 + stellar_hashupdate_uint32(msg->paths_count); + for (uint8_t i = 0; i < msg->paths_count; i++) { + stellar_hashupdate_asset(&(msg->paths[i])); + } + + // At this point, the operation is confirmed + stellar_activeTx.confirmed_operations++; + return true; +} + bool stellar_confirmManageBuyOfferOp(const StellarManageBuyOfferOp *msg) { if (!stellar_signing) return false; diff --git a/legacy/firmware/stellar.h b/legacy/firmware/stellar.h index d50c66635..9fecb6956 100644 --- a/legacy/firmware/stellar.h +++ b/legacy/firmware/stellar.h @@ -61,6 +61,8 @@ bool stellar_confirmCreateAccountOp(const StellarCreateAccountOp *msg); bool stellar_confirmPaymentOp(const StellarPaymentOp *msg); bool stellar_confirmPathPaymentStrictReceiveOp( const StellarPathPaymentStrictReceiveOp *msg); +bool stellar_confirmPathPaymentStrictSendOp( + const StellarPathPaymentStrictSendOp *msg); bool stellar_confirmManageBuyOfferOp(const StellarManageBuyOfferOp *msg); bool stellar_confirmManageSellOfferOp(const StellarManageSellOfferOp *msg); bool stellar_confirmCreatePassiveOfferOp( diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 52b7c3bb3..830f69bf5 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -157,6 +157,7 @@ class MessageType(IntEnum): StellarManageDataOp = 220 StellarBumpSequenceOp = 221 StellarManageBuyOfferOp = 222 + StellarPathPaymentStrictSendOp = 223 StellarSignedTx = 230 CardanoSignTx = 303 CardanoGetPublicKey = 305 @@ -6357,6 +6358,38 @@ class StellarPathPaymentStrictReceiveOp(protobuf.MessageType): self.source_account = source_account +class StellarPathPaymentStrictSendOp(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 223 + FIELDS = { + 1: protobuf.Field("source_account", "string", repeated=False, required=False), + 2: protobuf.Field("send_asset", "StellarAsset", repeated=False, required=True), + 3: protobuf.Field("send_amount", "sint64", repeated=False, required=True), + 4: protobuf.Field("destination_account", "string", repeated=False, required=True), + 5: protobuf.Field("destination_asset", "StellarAsset", repeated=False, required=True), + 6: protobuf.Field("destination_min", "sint64", repeated=False, required=True), + 7: protobuf.Field("paths", "StellarAsset", repeated=True, required=False), + } + + def __init__( + self, + *, + send_asset: "StellarAsset", + send_amount: "int", + destination_account: "str", + destination_asset: "StellarAsset", + destination_min: "int", + paths: Optional[List["StellarAsset"]] = None, + source_account: Optional["str"] = None, + ) -> None: + self.paths = paths if paths is not None else [] + self.send_asset = send_asset + self.send_amount = send_amount + self.destination_account = destination_account + self.destination_asset = destination_asset + self.destination_min = destination_min + self.source_account = source_account + + class StellarManageSellOfferOp(protobuf.MessageType): MESSAGE_WIRE_TYPE = 213 FIELDS = { diff --git a/python/src/trezorlib/stellar.py b/python/src/trezorlib/stellar.py index b0f969f8f..00561f51f 100644 --- a/python/src/trezorlib/stellar.py +++ b/python/src/trezorlib/stellar.py @@ -35,6 +35,7 @@ try: NoneMemo, Operation, PathPaymentStrictReceive, + PathPaymentStrictSend, Payment, ReturnHashMemo, SetOptions, @@ -237,6 +238,17 @@ def _read_operation(op: "Operation"): price_d=price.d, offer_id=op.offer_id, ) + if isinstance(op, PathPaymentStrictSend): + operation = messages.StellarPathPaymentStrictSendOp( + source_account=source_account, + send_asset=_read_asset(op.send_asset), + send_amount=_read_amount(op.send_amount), + destination_account=op.destination.account_id, + destination_asset=_read_asset(op.dest_asset), + destination_min=_read_amount(op.dest_min), + paths=[_read_asset(asset) for asset in op.path], + ) + return operation raise ValueError(f"Unknown operation type: {op.__class__.__name__}") diff --git a/python/tests/test_stellar.py b/python/tests/test_stellar.py index ebd773a81..2fcbc03a8 100644 --- a/python/tests/test_stellar.py +++ b/python/tests/test_stellar.py @@ -330,6 +330,7 @@ def test_path_payment_strict_receive(): assert operations[0].destination_account == destination assert operations[0].send_asset.type == messages.StellarAssetType.NATIVE assert operations[0].send_max == 500111000 + assert operations[0].destination_amount == 1000000000 assert operations[0].destination_asset.type == messages.StellarAssetType.ALPHANUM4 assert operations[0].destination_asset.code == dest_code assert operations[0].destination_asset.issuer == dest_issuer @@ -782,3 +783,57 @@ def test_manage_buy_offer_update_offer(): assert operations[0].price_n == 1 assert operations[0].price_d == 2 assert operations[0].offer_id == offer_id + + +def test_path_payment_strict_send(): + tx = make_default_tx() + destination = "GDNSSYSCSSJ76FER5WEEXME5G4MTCUBKDRQSKOYP36KUKVDB2VCMERS6" + send_amount = "50.0112" + dest_min = "120" + send_code = "XLM" + send_issuer = None + dest_code = "USD" + dest_issuer = "GCSJ7MFIIGIRMAS4R3VT5FIFIAOXNMGDI5HPYTWS5X7HH74FSJ6STSGF" + operation_source = "GAEB4MRKRCONK4J7MVQXAHTNDPAECUCCCNE7YC5CKM34U3OJ673A4D6V" + path_asset1 = Asset( + "JPY", "GD6PV7DXQJX7AGVXFQ2MTCLTCH6LR3E6IO2EO2YDZD7F7IOZZCCB5DSQ" + ) + path_asset2 = Asset( + "BANANA", "GC7EKO37HNSKQ3V6RZ274EO7SFOWASQRHLX3OR5FIZK6UMV6LIEDXHGZ" + ) + + envelope = ( + tx + .append_path_payment_strict_send_op( + destination=destination, + send_code=send_code, + send_issuer=send_issuer, + send_amount=send_amount, + dest_code=dest_code, + dest_issuer=dest_issuer, + dest_min=dest_min, + path=[path_asset1, path_asset2], + source=operation_source, + ) + .build() + ) + + tx, operations = stellar.from_envelope(envelope) + assert len(operations) == 1 + + assert isinstance(operations[0], messages.StellarPathPaymentStrictSendOp) + assert operations[0].source_account == operation_source + assert operations[0].destination_account == destination + assert operations[0].send_asset.type == messages.StellarAssetType.NATIVE + assert operations[0].send_amount == 500112000 + assert operations[0].destination_min == 1200000000 + assert operations[0].destination_asset.type == messages.StellarAssetType.ALPHANUM4 + assert operations[0].destination_asset.code == dest_code + assert operations[0].destination_asset.issuer == dest_issuer + assert len(operations[0].paths) == 2 + assert operations[0].paths[0].type == messages.StellarAssetType.ALPHANUM4 + assert operations[0].paths[0].code == path_asset1.code + assert operations[0].paths[0].issuer == path_asset1.issuer + assert operations[0].paths[1].type == messages.StellarAssetType.ALPHANUM12 + assert operations[0].paths[1].code == path_asset2.code + assert operations[0].paths[1].issuer == path_asset2.issuer diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index cd80afe66..cfc76e58a 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -849,6 +849,7 @@ "test_stellar.py::test_sign_tx[StellarManageBuyOfferOp]": "fc57e1ca8b65588aa16cc3524d6dc0f01e094ad5d16a6f7e739a69c101b554bc", "test_stellar.py::test_sign_tx[StellarManageSellOfferOp]": "6ed84765b2ed46711be0ed1219d91c27e927119d352f37b2baf8c6501186bbce", "test_stellar.py::test_sign_tx[StellarPathPaymentStrictReceiveOp]": "58f3bfaece0706bc172d6e6564b728ec0b7f8e2629d8c64dc60672786586076d", +"test_stellar.py::test_sign_tx[StellarPathPaymentStrictSendOp]": "fdd36a59520317d514e03f535dfeb93339af0f7ea5ee07c556bee3c8784c94ed", "test_stellar.py::test_sign_tx[StellarPaymentOp-asset12]": "1d8e9d5d65420a259f7e2deef1efaf0ce5be966a0f1e5b8e95b832f176f00de2", "test_stellar.py::test_sign_tx[StellarPaymentOp-asset4]": "0de0b815dad5d348a3b9d06e37da94800363e5de8e6ca9cd0f84e5070f7e1b22", "test_stellar.py::test_sign_tx[StellarPaymentOp-native_asset]": "b2015b9e0f9ff60e2ea4fca2942e97b70a320386c2043fb36acde4a830272098",