feat(common,core,python): improve UX for SEP-41 token invocations of Stellar Asset Contracts.
What changed, and why it matters
This commit improves the on-screen display for certain Stellar smart-contract token operations. It lets a connected wallet tell the Trezor device that a contract call is actually a well-known token transfer or approval, so the user sees a friendlier 'Send' or 'Approve' screen instead of raw technical data. The device independently recalculates the token contract address from the hint and only uses the friendly screen if the address matches; a wrong hint falls back to the raw display. The change is a user-experience enhancement, not a fix for a known security flaw.
No immediate action required. This is a UX feature. As with any UI change, vendors should verify that the new token-flow screens display all security-critical fields (sender, recipient, amount, token contract, spender, valid-until ledger) clearly and cannot be confused by a malicious host into hiding them. The existing derivation-based guard is a good defensive measure; ensure it remains mandatory in future refactors.
Security signals we found
New optional untrusted input field (`asset_hint`) introduced, but its use is gated by an independent on-device derivation check.
Friendly UI path is only taken after strict structural validation of function name and argument types/counts.
Negative i128 amounts are explicitly rejected from the token UI and forced back to the raw contract flow.
No signature data is influenced by the hint; the comment in the proto file explicitly states it is not part of the XDR and is never covered by the signature.
No memory-unsafe code, no cryptographic changes, no key handling changes observed.
Evidence from the diff
The patch adds an optional asset_hint field to StellarInvokeContractArgs and uses it to render SEP-41 token invocations of Stellar Asset Contracts (SAC) with dedicated UI flows. The device derives the SAC address from the hinted asset using CAP-46-2 and only accepts the hint when the derived address equals the invoked contract address. Supported shapes are transfer(from,to,amount) and approve(from,spender,amount,valid_until), with negative amounts and mismatched hints falling back to the generic contract UI. The change touches protobuf definitions, Python host helpers, core firmware layout code, translations, and test fixtures.
Changed components
common/protob/messages-stellar.protocore/src/apps/stellar/helpers.pycore/src/apps/stellar/layout.pycore/src/apps/stellar/operations/layout.pycore/src/apps/stellar/sign_soroban_authorization.pycore/src/apps/stellar/sign_tx.pypython/src/trezorlib/stellar.pypython/src/trezorlib/cli/stellar.pyInspect captured patch +2341 / −112
### common/protob/messages-stellar.proto
@@ -388,6 +388,13 @@ message StellarInvokeContractArgs {
required string contract_address = 1;
required string function_name = 2;
repeated StellarSCVal args = 3;
+ // Hint that contract_address is the Stellar Asset Contract (SAC) of this
+ // asset. The device re-derives the SAC address of the asset (CAP-46-2) and
+ // uses the hint only if it matches contract_address, so a wrong hint cannot
+ // mislead the user. A mismatch falls back to the raw contract UI. When it
+ // matches, SEP-41 token functions are shown as token operations.
+ // Not part of the XDR, hence never covered by the signature.
+ optional StellarAsset asset_hint = 4;
}
/**
### common/tests/fixtures/stellar/sign_soroban_authorization.json
@@ -232,6 +232,200 @@
"public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
"signature": "d4wI4YbGsIl7geBDLoN7Y55hxN/6cOEap2oQZGiWte77XjA6FFlFwS7T+gEdDtwxyOB6LUEqwENp1f0TUzGBDw=="
}
+ },
+ {
+ "name": "StellarSorobanAuthorization-sac-transfer",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAAADU/3rAAKrmAAAAABAAAAAAAAAAFQRc1ewHKado/VrQJQWFLfTwKNzoMOWsUiCbpISDsvAQAAAAh0cmFuc2ZlcgAAAAMAAAASAAAAAAAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAABIAAAAAAAAAAF1VZCRmsYW4QxUuniGRUdvFiSAn7EAQGlF77VygMMLgAAAACgAAAAAAAAAAAAAAAAvrwgAAAAAA",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "authorization": {
+ "nonce": 223344556,
+ "signature_expiration_ledger": 700000,
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "invocation": {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "D15cIytwNzklWMhk+WexWrcsW+XBcPrwt+dSZKBcaqsyP4D1aZUqKEhMmn2nXDYpMd5c6g6QpJCPrt/FEf+iAA=="
+ }
+ },
+ {
+ "name": "StellarSorobanAuthorization-sac-approve",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAAAE+9jcwAKrmAAAAABAAAAAAAAAAFQRc1ewHKado/VrQJQWFLfTwKNzoMOWsUiCbpISDsvAQAAAAdhcHByb3ZlAAAAAAQAAAASAAAAAAAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAABIAAAAAAAAAAJulaObuwWvqT+0PF7E0odaS6uGZpXi6b7RMDCRDG/20AAAACgAAAAAAAAAAAAAAAB3NZQAAAAADAAw1AAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "authorization": {
+ "nonce": 334455667,
+ "signature_expiration_ledger": 700000,
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "invocation": {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "approve",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2U6"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 500000000
+ }
+ },
+ {
+ "type": "SCV_U32",
+ "u32": 800000
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "8cUMRlNWsVOjX+UOWAu1UQgRGs8t3eAszHA9NAfgHJfv6IduCJxjEQhO4wWaZOP/XeVAkJfzIxDDY2ATv5EBDg=="
+ }
+ },
+ {
+ "name": "StellarSorobanAuthorization-sac-tree",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAAAGo7POgAKrmAAAAABAAAAAAAAAAEREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMAAAAARzd2FwAAAAAAAAAAIAAAAAAAAAAVBFzV7Acpp2j9WtAlBYUt9PAo3Ogw5axSIJukhIOy8BAAAABGJ1cm4AAAACAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAAKAAAAAAAAAAAAAAAAAvrwgAAAAAAAAAAAAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAACHRyYW5zZmVyAAAAAwAAABIAAAABERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAAAAASAAAAAAAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAAAoAAAAAAAAAAAAAAAAKupUAAAAAAA==",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ "native"
+ ],
+ "authorization": {
+ "nonce": 445566778,
+ "signature_expiration_ledger": 700000,
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "invocation": {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V",
+ "function_name": "swap"
+ }
+ },
+ "sub_invocations": [
+ {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "burn",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 50000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ },
+ {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 180000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "NATIVE"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "rUo5bk3oEMcnj7YDXiyBWU1XFoPBF2wbaVDg2jFj+cep/Lh+rIhxpoVRaA0uM327cRPJ3LF0AMZcAjI2idKqBQ=="
+ }
}
]
}
### common/tests/fixtures/stellar/sign_tx.json
@@ -1895,6 +1895,767 @@
"signature": "5uv43tMSDRDgs788M4uIkEkA6QvwN4eu0EP7yq2niwVSsVRHTqrnc6S3JrH2vu11ZwbUfaHL0BhGhPDm90vaAw=="
},
"skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-transfer",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALugAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAASAAAAAAAAAABdVWQkZrGFuEMVLp4hkVHbxYkgJ+xAEBpRe+1coDDC4AAAAAoAAAAAAAAAAAAAAAAL68IAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3002,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "N91DfNLFpj1UdkUpfNDlmW6qYLVOGTerCMyAQ/klkwKPpzQ0+qyen+tuKpqcpfTz7QczNLmK5mpwLFYik4DxCA=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-transfer-no-hint",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALugAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAASAAAAAAAAAABdVWQkZrGFuEMVLp4hkVHbxYkgJ+xAEBpRe+1coDDC4AAAAAoAAAAAAAAAAAAAAAAL68IAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3002,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "N91DfNLFpj1UdkUpfNDlmW6qYLVOGTerCMyAQ/klkwKPpzQ0+qyen+tuKpqcpfTz7QczNLmK5mpwLFYik4DxCA=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-transfer-external-from",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALuwAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAEREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMAAAABIAAAAAAAAAAF1VZCRmsYW4QxUuniGRUdvFiSAn7EAQGlF77VygMMLgAAAACgAAAAAAAAAAAAAAAAvrwgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3003,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "oGpGgZtOV6FkjJC727C9bA1ffwDdPGhTbWfz8LarIiD4zfUutHYaJkhMS+Hk28QwJiM/fIMk7GjAJiNDqJwSAg=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-transfer-native",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALvAAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAAB15KLcsJwPM/q9+uf9O9NUEpVqLl5/JtFDqLIQrTRzmEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAASAAAAAAAAAABdVWQkZrGFuEMVLp4hkVHbxYkgJ+xAEBpRe+1coDDC4AAAAAoAAAAAAAAAAAAAAAAL68IAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "native"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3004,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "NATIVE"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "C7wpZY+2Mn4zVrZyiDmHcce3EiT2D10fcQWgLwHSkMkZDTt7JN8EMisRpHhJGdQNnmWCgLbfifeyC2TodZgqDA=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-approve",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALvQAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAHYXBwcm92ZQAAAAAEAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAASAAAAAAAAAACbpWjm7sFr6k/tDxexNKHWkurhmaV4um+0TAwkQxv9tAAAAAoAAAAAAAAAAAAAAAAdzWUAAAAAAwAMNQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3005,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "approve",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2U6"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 500000000
+ }
+ },
+ {
+ "type": "SCV_U32",
+ "u32": 800000
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "7hgiCELqb11PHdRvzIapaeG3oxeEkrRYRpza/oslXuwlyRxg+vd2g28iXH6gjcH30l0Yn8cJKHb2jQP4lNv6Bg=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-revoke",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALvgAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAHYXBwcm92ZQAAAAAEAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAASAAAAAAAAAACbpWjm7sFr6k/tDxexNKHWkurhmaV4um+0TAwkQxv9tAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAwAMNQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3006,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "approve",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2U6"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 0
+ }
+ },
+ {
+ "type": "SCV_U32",
+ "u32": 800000
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "H3WM1p/BpejbrRk4S6w1Gr6IV2on2qtJfxVLrdi4pFgmn/p66J3pjP8dEfL4DpJVGgFG9okIan91DwyjwjGkBQ=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-approve-external-from",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALwgAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAHYXBwcm92ZQAAAAAEAAAAEgAAAAEREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMAAAABIAAAAAAAAAAJulaObuwWvqT+0PF7E0odaS6uGZpXi6b7RMDCRDG/20AAAACgAAAAAAAAAAAAAAAB3NZQAAAAADAAw1AAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3010,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "approve",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GCN2K2HG53AWX2SP5UHRPMJUUHLJF2XBTGSXROTPWRGAYJCDDP63J2U6"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 500000000
+ }
+ },
+ {
+ "type": "SCV_U32",
+ "u32": 800000
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "OX3oNza4LE6TG2DwaBbTcUA/ZivrTN6Lxhi1kidXZ3DjUayIUDJTOvgodwGljX1f9Bd9vNb2q6KsyK46B0PiCA=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-fallback-burn",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALvwAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAEYnVybgAAAAIAAAASAAAAAAAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAAAoAAAAAAAAAAAAAAAAC+vCAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3007,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "burn",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 50000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "zsdCiyEAWVXHXL32N+d7UmacrioMlxInMtBnz+vHY513KQhV+V+QzuE4h4nxR5B0H3XTbuGvlEGsqfGFhdE3AQ=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-fallback-negative-amount",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALwAAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABUEXNXsBymnaP1a0CUFhS308Cjc6DDlrFIgm6SEg7LwEAAAAIdHJhbnNmZXIAAAADAAAAEgAAAAAAAAAALyK5xi8It3Tz6+bdbn25PD7Cy94CeVYaPZxSJbjDIpIAAAASAAAAAAAAAABdVWQkZrGFuEMVLp4hkVHbxYkgJ+xAEBpRe+1coDDC4AAAAAr////////////////0FD4AAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3008,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": -1,
+ "lo": 18446744073509551616
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "Vdd0eNPZRF+i/OmuWo0FzaYXwIM3vuzHir0QisfGG6CWN86PEhl61doh/PzjhveUg1uRvlfSyFEWJTrs1j2RDg=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-swap-auth-tree",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALwQAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAAAAAEc3dhcAAAAAAAAAABAAAAAAAAAAAAAAABERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAAAAAEc3dhcAAAAAAAAAACAAAAAAAAAAFQRc1ewHKado/VrQJQWFLfTwKNzoMOWsUiCbpISDsvAQAAAAh0cmFuc2ZlcgAAAAMAAAASAAAAAAAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAABIAAAABERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAAAAAKAAAAAAAAAAAAAAAAC+vCAAAAAAAAAAAAAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAACHRyYW5zZmVyAAAAAwAAABIAAAABERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAAAAASAAAAAAAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAAAoAAAAAAAAAAAAAAAAKupUAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
+ "native"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3009,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V",
+ "function_name": "swap"
+ }
+ },
+ "auth": [
+ {
+ "credentials": {
+ "type": "SOROBAN_CREDENTIALS_SOURCE_ACCOUNT"
+ },
+ "root_invocation": {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V",
+ "function_name": "swap"
+ }
+ },
+ "sub_invocations": [
+ {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ },
+ {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 180000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "NATIVE"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "Ux6Oa147u4+CbGsEmAxHfU0D+k6v6Ok+DfaNSBe/8hbGQpTC1U1GhiARtjdINeq4ziK9R9aEhIxZ9JaSxFipAA=="
+ },
+ "skip_models": ["t1b1"]
+ },
+ {
+ "name": "StellarInvokeHostFunction-sac-transfer-address-entry",
+ "parameters": {
+ "xdr": "AAAAAgAAAAAvIrnGLwi3dPPr5t1ufbk8PsLL3gJ5Vho9nFIluMMikgAAASwAAAAAAAALwwAAAAEAAAAAAAAAAAAAAABw29iAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAAAAAEc3dhcAAAAAAAAAABAAAAAgAAAAAAAAAA7UkoxijRwsbq6QM4kFmVYSlZJzpcY/k2NsFGFKyHN9EAAAAAB1a1swAMNQAAAAABAAAAAAAAAAFQRc1ewHKado/VrQJQWFLfTwKNzoMOWsUiCbpISDsvAQAAAAh0cmFuc2ZlcgAAAAMAAAASAAAAAAAAAADtSSjGKNHCxurpAziQWZVhKVknOlxj+TY2wUYUrIc30QAAABIAAAAAAAAAAC8iucYvCLd08+vm3W59uTw+wsveAnlWGj2cUiW4wyKSAAAACgAAAAAAAAAAAAAAAAvrwgAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+ "address_n": "m/44'/148'/0'",
+ "network_passphrase": "Test SDF Network ; September 2015",
+ "asset_hints": [
+ "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ ],
+ "tx": {
+ "source_account": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV",
+ "fee": 300,
+ "sequence_number": 3011,
+ "timebounds_start": 0,
+ "timebounds_end": 1893456000,
+ "memo_type": "NONE"
+ },
+ "operations": [
+ {
+ "_message_type": "StellarInvokeHostFunctionOp",
+ "function": {
+ "type": "HOST_FUNCTION_TYPE_INVOKE_CONTRACT",
+ "invoke_contract": {
+ "contract_address": "CAIREEYUCULBOGAZDINRYHI6D4QCCIRDEQSSMJZIFEVCWLBNFYXTBJ6V",
+ "function_name": "swap"
+ }
+ },
+ "auth": [
+ {
+ "credentials": {
+ "type": "SOROBAN_CREDENTIALS_ADDRESS_V2",
+ "address_v2": {
+ "address": "GDWUSKGGFDI4FRXK5EBTRECZSVQSSWJHHJOGH6JWG3AUMFFMQ435DIAG",
+ "nonce": 123123123,
+ "signature_expiration_ledger": 800000,
+ "signature": {
+ "type": "SCV_VOID"
+ }
+ }
+ },
+ "root_invocation": {
+ "function": {
+ "type": "SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN",
+ "contract_fn": {
+ "contract_address": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
+ "function_name": "transfer",
+ "args": [
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GDWUSKGGFDI4FRXK5EBTRECZSVQSSWJHHJOGH6JWG3AUMFFMQ435DIAG"
+ },
+ {
+ "type": "SCV_ADDRESS",
+ "address": "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+ },
+ {
+ "type": "SCV_I128",
+ "i128": {
+ "hi": 0,
+ "lo": 200000000
+ }
+ }
+ ],
+ "asset_hint": {
+ "type": "ALPHANUM4",
+ "code": "USDC",
+ "issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "ext": {
+ "v": 1,
+ "soroban_data": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "result": {
+ "public_key": "2f22b9c62f08b774f3ebe6dd6e7db93c3ec2cbde0279561a3d9c5225b8c32292",
+ "signature": "V8/e+0SaCFgaEQCZRvPq3b++X4LtnMeDBicaZV7g5vnJfita3nMrdacP5nQ9Ikg3dfqku8qPE350/y+Z/+NRCw=="
+ },
+ "skip_models": ["t1b1"]
}
]
}
### core/.changelog.d/7310.added
@@ -0,0 +1 @@
+Stellar: Show SEP-41 `transfer` and `approve` invocations of Stellar Asset Contracts as token operations.
### core/embed/rust/librust_qstr.h
@@ -1491,6 +1491,7 @@ static void _librust_qstrs(void) {
MP_QSTR_stellar__add_trust;
MP_QSTR_stellar__all_will_be_sent_to;
MP_QSTR_stellar__allow_trust;
+ MP_QSTR_stellar__approve_token;
MP_QSTR_stellar__balance_id;
MP_QSTR_stellar__bump_sequence;
MP_QSTR_stellar__buying;
@@ -1509,6 +1510,7 @@ static void _librust_qstrs(void) {
MP_QSTR_stellar__ext_auth;
MP_QSTR_stellar__ext_auth_message;
MP_QSTR_stellar__final_confirm;
+ MP_QSTR_stellar__from;
MP_QSTR_stellar__hash;
MP_QSTR_stellar__high;
MP_QSTR_stellar__home_domain;
@@ -1532,6 +1534,7 @@ static void _librust_qstrs(void) {
MP_QSTR_stellar__preauth_transaction;
MP_QSTR_stellar__price_per_template;
MP_QSTR_stellar__remove_signer;
+ MP_QSTR_stellar__revoke_approval;
MP_QSTR_stellar__revoke_trust;
MP_QSTR_stellar__selling;
MP_QSTR_stellar__set_data;
@@ -1540,10 +1543,14 @@ static void _librust_qstrs(void) {
MP_QSTR_stellar__sign_authorization;
MP_QSTR_stellar__sign_with;
MP_QSTR_stellar__source_account;
+ MP_QSTR_stellar__spender;
MP_QSTR_stellar__timebounds;
+ MP_QSTR_stellar__to;
+ MP_QSTR_stellar__token_contract;
MP_QSTR_stellar__token_info;
MP_QSTR_stellar__transaction_source;
MP_QSTR_stellar__transaction_source_diff_warning;
+ MP_QSTR_stellar__transfer_token;
MP_QSTR_stellar__trusted_account;
MP_QSTR_stellar__update;
MP_QSTR_stellar__valid_from;
### core/embed/rust/src/translations/generated/translated_string.rs
@@ -1664,6 +1664,20 @@ pub enum TranslatedString {
buttons__forget = 1274, // {"Bolt": "", "Caesar": "", "Delizia": "", "Eckhart": "Forget"}
buttons__show_next = 1275, // {"Bolt": "Show next", "Caesar": "", "Delizia": "", "Eckhart": "Show next"}
buttons__wipe = 1276, // "Wipe"
+ #[cfg(feature = "universal_fw")]
+ stellar__approve_token = 1277, // "Approve token"
+ #[cfg(feature = "universal_fw")]
+ stellar__from = 1278, // "From"
+ #[cfg(feature = "universal_fw")]
+ stellar__revoke_approval = 1279, // "Revoke approval"
+ #[cfg(feature = "universal_fw")]
+ stellar__spender = 1280, // "Spender"
+ #[cfg(feature = "universal_fw")]
+ stellar__to = 1281, // "To"
+ #[cfg(feature = "universal_fw")]
+ stellar__token_contract = 1282, // "Token contract"
+ #[cfg(feature = "universal_fw")]
+ stellar__transfer_token = 1283, // "Transfer token"
}
impl TranslatedString {
@@ -2948,6 +2962,13 @@ impl TranslatedString {
"",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", feature = "universal_fw"))]
@@ -4230,6 +4251,13 @@ impl TranslatedString {
18939,
18948,
18952,
+ 18965,
+ 18969,
+ 18984,
+ 18991,
+ 18993,
+ 19007,
+ 19021,
];
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -5511,6 +5539,13 @@ impl TranslatedString {
"",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -6793,6 +6828,13 @@ impl TranslatedString {
18939,
18948,
18952,
+ 18965,
+ 18969,
+ 18984,
+ 18991,
+ 18993,
+ 19007,
+ 19021,
];
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -8074,6 +8116,13 @@ impl TranslatedString {
"",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -9356,6 +9405,13 @@ impl TranslatedString {
18939,
18948,
18952,
+ 18965,
+ 18969,
+ 18984,
+ 18991,
+ 18993,
+ 19007,
+ 19021,
];
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -10637,6 +10693,13 @@ impl TranslatedString {
"",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -11919,6 +11982,13 @@ impl TranslatedString {
18939,
18948,
18952,
+ 18965,
+ 18969,
+ 18984,
+ 18991,
+ 18993,
+ 19007,
+ 19021,
];
} else if #[cfg(feature = "layout_caesar")] {
@@ -13201,6 +13271,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", feature = "universal_fw"))]
@@ -14483,6 +14560,13 @@ impl TranslatedString {
16757,
16757,
16761,
+ 16774,
+ 16778,
+ 16793,
+ 16800,
+ 16802,
+ 16816,
+ 16830,
];
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -15764,6 +15848,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -17046,6 +17137,13 @@ impl TranslatedString {
16757,
16757,
16761,
+ 16774,
+ 16778,
+ 16793,
+ 16800,
+ 16802,
+ 16816,
+ 16830,
];
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -18327,6 +18425,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -19609,6 +19714,13 @@ impl TranslatedString {
16757,
16757,
16761,
+ 16774,
+ 16778,
+ 16793,
+ 16800,
+ 16802,
+ 16816,
+ 16830,
];
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -20890,6 +21002,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -22172,6 +22291,13 @@ impl TranslatedString {
16757,
16757,
16761,
+ 16774,
+ 16778,
+ 16793,
+ 16800,
+ 16802,
+ 16816,
+ 16830,
];
} else if #[cfg(feature = "layout_delizia")] {
@@ -23454,6 +23580,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", feature = "universal_fw"))]
@@ -24736,6 +24869,13 @@ impl TranslatedString {
18864,
18864,
18868,
+ 18881,
+ 18885,
+ 18900,
+ 18907,
+ 18909,
+ 18923,
+ 18937,
];
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -26017,6 +26157,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -27299,6 +27446,13 @@ impl TranslatedString {
18864,
18864,
18868,
+ 18881,
+ 18885,
+ 18900,
+ 18907,
+ 18909,
+ 18923,
+ 18937,
];
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -28580,6 +28734,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -29862,6 +30023,13 @@ impl TranslatedString {
18864,
18864,
18868,
+ 18881,
+ 18885,
+ 18900,
+ 18907,
+ 18909,
+ 18923,
+ 18937,
];
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -31143,6 +31311,13 @@ impl TranslatedString {
"",
"",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -32425,6 +32600,13 @@ impl TranslatedString {
18864,
18864,
18868,
+ 18881,
+ 18885,
+ 18900,
+ 18907,
+ 18909,
+ 18923,
+ 18937,
];
} else if #[cfg(feature = "layout_eckhart")] {
@@ -33707,6 +33889,13 @@ impl TranslatedString {
"Forget",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", feature = "universal_fw"))]
@@ -34989,6 +35178,13 @@ impl TranslatedString {
20275,
20284,
20288,
+ 20301,
+ 20305,
+ 20320,
+ 20327,
+ 20329,
+ 20343,
+ 20357,
];
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -36270,6 +36466,13 @@ impl TranslatedString {
"Forget",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(feature = "debug", not(feature = "universal_fw")))]
@@ -37552,6 +37755,13 @@ impl TranslatedString {
20275,
20284,
20288,
+ 20301,
+ 20305,
+ 20320,
+ 20327,
+ 20329,
+ 20343,
+ 20357,
];
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -38833,6 +39043,13 @@ impl TranslatedString {
"Forget",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), feature = "universal_fw"))]
@@ -40115,6 +40332,13 @@ impl TranslatedString {
20275,
20284,
20288,
+ 20301,
+ 20305,
+ 20320,
+ 20327,
+ 20329,
+ 20343,
+ 20357,
];
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -41396,6 +41620,13 @@ impl TranslatedString {
"Forget",
"Show next",
"Wipe",
+ "Approve token",
+ "From",
+ "Revoke approval",
+ "Spender",
+ "To",
+ "Token contract",
+ "Transfer token",
);
#[cfg(all(not(feature = "debug"), not(feature = "universal_fw")))]
@@ -42678,6 +42909,13 @@ impl TranslatedString {
20275,
20284,
20288,
+ 20301,
+ 20305,
+ 20320,
+ 20327,
+ 20329,
+ 20343,
+ 20357,
];
}
@@ -44006,6 +44244,8 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__allow_trust, Self::stellar__allow_trust),
#[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__approve_token, Self::stellar__approve_token),
+ #[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__balance_id, Self::stellar__balance_id),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__bump_sequence, Self::stellar__bump_sequence),
@@ -44042,6 +44282,8 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__final_confirm, Self::stellar__final_confirm),
#[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__from, Self::stellar__from),
+ #[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__hash, Self::stellar__hash),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__high, Self::stellar__high),
@@ -44088,6 +44330,8 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__remove_signer, Self::stellar__remove_signer),
#[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__revoke_approval, Self::stellar__revoke_approval),
+ #[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__revoke_trust, Self::stellar__revoke_trust),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__selling, Self::stellar__selling),
@@ -44104,14 +44348,22 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__source_account, Self::stellar__source_account),
#[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__spender, Self::stellar__spender),
+ #[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__timebounds, Self::stellar__timebounds),
#[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__to, Self::stellar__to),
+ #[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__token_contract, Self::stellar__token_contract),
+ #[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__token_info, Self::stellar__token_info),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__transaction_source, Self::stellar__transaction_source),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__transaction_source_diff_warning, Self::stellar__transaction_source_diff_warning),
#[cfg(feature = "universal_fw")]
+ (Qstr::MP_QSTR_stellar__transfer_token, Self::stellar__transfer_token),
+ #[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__trusted_account, Self::stellar__trusted_account),
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_stellar__update, Self::stellar__update),
### core/mocks/trezortranslate_keys.pyi
@@ -934,6 +934,7 @@ class TR:
stellar__add_trust: str = "Add trust"
stellar__all_will_be_sent_to: str = "All XLM will be sent to"
stellar__allow_trust: str = "Allow trust"
+ stellar__approve_token: str = "Approve token"
stellar__balance_id: str = "Balance ID"
stellar__bump_sequence: str = "Bump Sequence"
stellar__buying: str = "Buying"
@@ -952,6 +953,7 @@ class TR:
stellar__ext_auth: str = "External Authorizations"
stellar__ext_auth_message: str = "Transaction contains additional invocations authorized by external means."
stellar__final_confirm: str = "Final confirm"
+ stellar__from: str = "From"
stellar__hash: str = "Hash"
stellar__high: str = "High"
stellar__home_domain: str = "Home Domain"
@@ -975,6 +977,7 @@ class TR:
stellar__preauth_transaction: str = "Pre-auth transaction"
stellar__price_per_template: str = "Price per {0}"
stellar__remove_signer: str = "Remove Signer"
+ stellar__revoke_approval: str = "Revoke approval"
stellar__revoke_trust: str = "Revoke trust"
stellar__selling: str = "Selling"
stellar__set_data: str = "Set data"
@@ -983,10 +986,14 @@ class TR:
stellar__sign_authorization: str = "Sign authorization"
stellar__sign_with: str = "Sign with"
stellar__source_account: str = "Source account"
+ stellar__spender: str = "Spender"
stellar__timebounds: str = "Timebounds"
+ stellar__to: str = "To"
+ stellar__token_contract: str = "Token contract"
stellar__token_info: str = "Token info"
stellar__transaction_source: str = "Transaction source"
stellar__transaction_source_diff_warning: str = "Transaction source does not belong to this Trezor."
+ stellar__transfer_token: str = "Transfer token"
stellar__trusted_account: str = "Trusted Account"
stellar__update: str = "Update"
stellar__valid_from: str = "Valid from (UTC)"
### core/src/apps/stellar/helpers.py
@@ -6,6 +6,8 @@
if TYPE_CHECKING:
from buffer_types import AnyBytes
+ from trezor.messages import StellarAsset, StellarInvokeContractArgs
+
# Stellar strkey version bytes
# See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0023.md
STRKEY_ED25519_PUBLIC_KEY = const(6) # G...
@@ -41,6 +43,47 @@ def address_from_public_key(pubkey: AnyBytes) -> str:
return encode_strkey(STRKEY_ED25519_PUBLIC_KEY, pubkey)
+def sac_address_from_asset(network_id: AnyBytes, asset: StellarAsset) -> str:
+ """Derive the address of the Stellar Asset Contract (SAC) of an asset (C...).
+
+ See https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-02.md#contract-identifier-preimage-type
+ """
+ from trezor.crypto.hashlib import sha256
+
+ from .writers import write_asset, write_bytes_fixed, write_uint32
+
+ w = bytearray()
+ write_uint32(w, 8) # ENVELOPE_TYPE_CONTRACT_ID
+ write_bytes_fixed(w, network_id, 32)
+ write_uint32(w, 1) # CONTRACT_ID_PREIMAGE_FROM_ASSET
+ write_asset(w, asset)
+ return encode_strkey(STRKEY_CONTRACT, sha256(w).digest())
+
+
+def resolve_sep41_token(
+ args: StellarInvokeContractArgs, network_id: AnyBytes
+) -> StellarAsset | None:
+ """Resolve token metadata for the dedicated SEP-41 UI.
+
+ Currently, the host may identify a Stellar Asset Contract by supplying its
+ underlying asset. The hint is used only when its derived SAC address matches
+ the invoked contract; an absent, mismatched, or malformed hint leaves the
+ invocation to the generic contract UI.
+ """
+ from trezor.wire import DataError
+
+ asset = args.asset_hint
+ if asset is None:
+ return None
+ try:
+ sac_address = sac_address_from_asset(network_id, asset)
+ except DataError:
+ return None
+ if sac_address != args.contract_address:
+ return None
+ return asset
+
+
def encode_strkey(version: int, data: AnyBytes) -> str:
"""Encode data to Stellar strkey format."""
payload = bytearray()
### core/src/apps/stellar/layout.py
@@ -8,8 +8,10 @@
from apps.common.paths import address_n_to_str
from . import consts
+from .helpers import resolve_sep41_token
if TYPE_CHECKING:
+ from buffer_types import AnyBytes
from typing import Iterable
from trezor.enums import StellarMemoType
@@ -245,24 +247,28 @@ async def require_confirm_signature_expiration_ledger(
)
-async def confirm_invoke_contract_args(
+async def _confirm_invoke_contract_args(
args: StellarInvokeContractArgs,
br_name_prefix: str,
- title: str | None = None,
+ authorization_title: str | None = None,
) -> None:
- # If title is not empty, it is shared across screens;
- # the per-screen label moves into the description / subtitle.
+ """Confirm a contract call using the generic, unparsed arguments UI.
+
+ `authorization_title` is omitted for the function invoked directly by a
+ transaction. For an authorization-tree node, it identifies the node while
+ each screen's usual title moves into its description or subtitle.
+ """
await layouts.confirm_address(
- title or TR.stellar__invoke_contract,
+ authorization_title or TR.stellar__invoke_contract,
args.contract_address,
- description=TR.stellar__invoke_contract if title else None,
+ description=TR.stellar__invoke_contract if authorization_title else None,
br_name=f"{br_name_prefix}_contract_address",
)
await layouts.confirm_text(
f"{br_name_prefix}_function",
- title or TR.words__function,
+ authorization_title or TR.words__function,
args.function_name,
- description=TR.words__function if title else None,
+ description=TR.words__function if authorization_title else None,
)
if not args.args:
return
@@ -272,26 +278,261 @@ async def confirm_invoke_contract_args(
]
await layouts.confirm_properties(
f"{br_name_prefix}_args",
- title or TR.words__arguments,
+ authorization_title or TR.words__arguments,
props,
- TR.words__arguments if title else None,
+ TR.words__arguments if authorization_title else None,
+ )
+
+
+# SEP-41 token functions given a dedicated UI, see
+# https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md
+_SEP41_TRANSFER = "transfer"
+_SEP41_APPROVE = "approve"
+
+
+def _parse_sep41_transfer(
+ args: StellarInvokeContractArgs,
+) -> tuple[str, str, int] | None:
+ """Read a SEP-41 `transfer(from, to, amount)` call, or None if it is not one."""
+ if args.function_name != _SEP41_TRANSFER or len(args.args) != 3:
+ return None
+ from_address = _sc_address(args.args[0])
+ to_address = _sc_address(args.args[1])
+ amount = _sc_amount(args.args[2])
+ if from_address is None or to_address is None or amount is None:
+ return None
+ return from_address, to_address, amount
+
+
+def _parse_sep41_approve(
+ args: StellarInvokeContractArgs,
+) -> tuple[str, str, int, int] | None:
+ """Read a SEP-41 `approve(from, spender, amount, live_until_ledger)` call."""
+ if args.function_name != _SEP41_APPROVE or len(args.args) != 4:
+ return None
+ from_address = _sc_address(args.args[0])
+ spender = _sc_address(args.args[1])
+ amount = _sc_amount(args.args[2])
+ live_until_ledger = _sc_u32(args.args[3])
+ if (
+ from_address is None
+ or spender is None
+ or amount is None
+ or live_until_ledger is None
+ ):
+ return None
+ return from_address, spender, amount, live_until_ledger
+
+
+def _sc_address(val: StellarSCVal) -> str | None:
+ if val.type != StellarSCValType.SCV_ADDRESS:
+ return None
+ return val.address
+
+
+def _sc_amount(val: StellarSCVal) -> int | None:
+ """Read an amount suitable for the dedicated token UI.
+
+ Stellar Asset Contracts reject negative amounts. Custom SEP-41 contracts
+ may accept them, but we deliberately leave such calls to the raw contract
+ flow instead of presenting them as regular token operations.
+ """
+ if val.type != StellarSCValType.SCV_I128 or val.i128 is None:
+ return None
+ amount = _i128_value(val.i128)
+ if amount < 0:
+ return None
+ return amount
+
+
+def _sc_u32(val: StellarSCVal) -> int | None:
+ if val.type != StellarSCValType.SCV_U32:
+ return None
+ return val.u32
+
+
+async def confirm_invoke_contract(
+ args: StellarInvokeContractArgs,
+ network_id: AnyBytes,
+ authorizing_address: str,
+ authorization_title: str | None = None,
+) -> None:
+ """Confirm a contract call, using the dedicated token UI when possible.
+
+ `authorization_title` is omitted for the host function invoked directly by
+ a transaction; transfers then retain the standard payment-style output
+ screen. An invocation from either kind of authorization tree provides a
+ title identifying the node, and the token action becomes its subtitle.
+
+ `authorizing_address` is used in both contexts to avoid repeating the
+ address that already authorizes the transaction operation or the whole
+ authorization tree.
+ """
+ br_name_prefix = "op_invoke" if authorization_title is None else "op_auth"
+
+ asset = resolve_sep41_token(args, network_id)
+ if asset is not None:
+ transfer = _parse_sep41_transfer(args)
+ if transfer is not None:
+ await _confirm_sep41_transfer(
+ transfer,
+ asset,
+ args.contract_address,
+ authorizing_address,
+ br_name_prefix,
+ authorization_title,
+ )
+ return
+
+ approve = _parse_sep41_approve(args)
+ if approve is not None:
+ await _confirm_sep41_approve(
+ approve,
+ asset,
+ args.contract_address,
+ authorizing_address,
+ br_name_prefix,
+ authorization_title,
+ )
+ return
+
+ await _confirm_invoke_contract_args(
+ args,
+ br_name_prefix,
+ authorization_title,
+ )
+
+
+async def _confirm_sep41_transfer(
+ transfer: tuple[str, str, int],
+ asset: StellarAsset,
+ token_contract: str,
+ authorizing_address: str,
+ br_name_prefix: str,
+ authorization_title: str | None,
+) -> None:
+ """Confirm a parsed SEP-41 transfer using its dedicated token UI."""
+ from_address, to_address, amount = transfer
+ # Without an authorization title, this is the transaction's own action and
+ # is framed like a classic payment. A tree node instead describes what a
+ # signature permits and may belong to another party altogether, so it gets
+ # a neutral, perspective-free label, in line with "Approve token".
+ action = (
+ TR.words__send if authorization_title is None else TR.stellar__transfer_token
+ )
+ screen_title = authorization_title or action
+ # For a direct transaction, "Send" is the title and
+ # `confirm_stellar_output` supplies the recipient context. In an
+ # authorization tree, its node title is retained and the action is shown as
+ # the subtitle.
+ subtitle = "" if authorization_title is None else action
+ if from_address != authorizing_address:
+ await layouts.confirm_stellar_address(
+ screen_title,
+ subtitle,
+ from_address,
+ TR.stellar__from,
+ f"{br_name_prefix}_from",
+ )
+
+ if authorization_title is not None:
+ await layouts.confirm_stellar_address(
+ screen_title,
+ subtitle,
+ to_address,
+ TR.stellar__to,
+ f"{br_name_prefix}_to",
+ )
+ await layouts.confirm_stellar_output_amount(
+ screen_title,
+ subtitle,
+ format_amount(amount, asset),
+ asset,
+ TR.words__amount,
+ token_contract=token_contract,
+ )
+ else:
+ await layouts.confirm_stellar_output(
+ to_address,
+ format_amount(amount, asset),
+ output_index=0, # a Soroban operation is always the only one
+ asset=asset,
+ token_contract=token_contract,
+ )
+
+
+async def _confirm_sep41_approve(
+ approve: tuple[str, str, int, int],
+ asset: StellarAsset,
+ token_contract: str,
+ authorizing_address: str,
+ br_name_prefix: str,
+ authorization_title: str | None,
+) -> None:
+ """Confirm a parsed SEP-41 approval or revocation using its dedicated UI."""
+ from_address, spender, amount, live_until_ledger = approve
+ action = TR.stellar__revoke_approval if amount == 0 else TR.stellar__approve_token
+ screen_title = authorization_title or action
+ subtitle = "" if authorization_title is None else action
+ if from_address != authorizing_address:
+ await layouts.confirm_stellar_address(
+ screen_title,
+ subtitle,
+ from_address,
+ TR.stellar__from,
+ f"{br_name_prefix}_from",
+ )
+ await layouts.confirm_stellar_address(
+ screen_title,
+ subtitle,
+ spender,
+ TR.stellar__spender,
+ f"{br_name_prefix}_spender",
+ )
+ if amount == 0:
+ # "Revoke approval" already communicates the zero allowance, so
+ # identify the token instead of displaying the omitted amount.
+ display_value = format_asset(asset)
+ value_label = TR.words__token
+ else:
+ display_value = format_amount(amount, asset)
+ value_label = TR.words__amount
+ await layouts.confirm_stellar_output_amount(
+ screen_title,
+ subtitle,
+ display_value,
+ asset,
+ value_label,
+ token_contract=token_contract,
+ )
+ await layouts.confirm_stellar_valid_until(
+ screen_title,
+ subtitle,
+ live_until_ledger,
+ f"{br_name_prefix}_valid_until",
)
async def confirm_authorized_invocation(
invocation: StellarSorobanAuthorizedInvocation,
+ network_id: AnyBytes,
+ authorizing_address: str,
) -> None:
"""Confirm a standalone authorized invocation tree (auth entry signing).
Unlike in a transaction, there is always exactly one entry being signed, so
its root label is empty; sub-invocations are numbered relative to it
(".1", ".1.2", ...), the same paths they would have inside a transaction.
"""
- await confirm_invocation(invocation, "")
+ await confirm_invocation(invocation, "", network_id, authorizing_address)
async def confirm_invocation(
- invocation: StellarSorobanAuthorizedInvocation, position: str, is_root: bool = False
+ invocation: StellarSorobanAuthorizedInvocation,
+ position: str,
+ network_id: AnyBytes,
+ authorizing_address: str,
+ is_root: bool = False,
) -> None:
"""Confirm an authorized invocation and its sub-invocations recursively.
@@ -300,7 +541,8 @@ async def confirm_invocation(
label plus the dot-delimited path in the auth tree (e.g. "#2", "#2.1" in a
transaction), or empty for the unlabeled root of a standalone authorization
entry (whose children are then ".1", ".1.2", ...), so a given entry's
- children carry the same paths in both flows.
+ children carry the same paths in both flows. `authorizing_address` belongs
+ to the whole tree and is propagated unchanged to every child.
"""
from trezor.enums import StellarSorobanAuthorizedFunctionType
@@ -314,19 +556,22 @@ async def confirm_invocation(
raise DataError("Stellar: missing contract_fn")
if position:
- title = f"{TR.words__authorization} {position}"
+ authorization_title = f"{TR.words__authorization} {position}"
else:
- title = TR.words__authorization
+ authorization_title = TR.words__authorization
if not is_root:
- await confirm_invoke_contract_args(
+ await confirm_invoke_contract(
func.contract_fn,
- br_name_prefix="op_auth",
- title=title,
+ network_id,
+ authorizing_address,
+ authorization_title=authorization_title,
)
for i, sub in enumerate(invocation.sub_invocations):
- await confirm_invocation(sub, f"{position}.{i + 1}")
+ await confirm_invocation(
+ sub, f"{position}.{i + 1}", network_id, authorizing_address
+ )
def _escape_str(s: str) -> str:
@@ -459,11 +704,15 @@ def _format_u128(parts: StellarUInt128Parts) -> str:
return str(value)
-def _format_i128(parts: StellarInt128Parts) -> str:
+def _i128_value(parts: StellarInt128Parts) -> int:
value = ((parts.hi & _MASK64) << 64) | (parts.lo & _MASK64)
if parts.hi < 0:
value -= 1 << 128
- return str(value)
+ return value
+
+
+def _format_i128(parts: StellarInt128Parts) -> str:
+ return str(_i128_value(parts))
def _format_u256(parts: StellarUInt256Parts) -> str:
### core/src/apps/stellar/operations/__init__.py
@@ -1,6 +1,8 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
+ from buffer_types import AnyBytes
+
from consts import StellarMessageType
from trezor.utils import Writer
@@ -12,6 +14,8 @@ async def process_operation(
op: StellarMessageType,
output_index: int,
payment_request_verifier: PaymentRequestVerifier | None,
+ tx_source_account: str,
+ network_id: AnyBytes,
) -> None:
# Importing the stuff inside (only) function saves around 100 bytes here
# (probably because the local lookup is more efficient than a global lookup)
@@ -80,7 +84,7 @@ async def process_operation(
await layout.confirm_claim_claimable_balance_op(op)
serialize.write_claim_claimable_balance_op(w, op)
elif messages.StellarInvokeHostFunctionOp.is_type_of(op):
- await layout.confirm_invoke_host_function_op(op)
+ await layout.confirm_invoke_host_function_op(op, tx_source_account, network_id)
serialize.write_invoke_host_function_op(w, op)
else:
raise ValueError("Unknown operation")
### core/src/apps/stellar/operations/layout.py
@@ -10,10 +10,10 @@
)
from trezor.wire import DataError, ProcessError
-from ..layout import confirm_invocation, confirm_invoke_contract_args, format_amount
+from ..layout import confirm_invocation, confirm_invoke_contract, format_amount
if TYPE_CHECKING:
- from buffer_types import StrOrBytes
+ from buffer_types import AnyBytes, StrOrBytes
from trezor.messages import (
StellarAccountMergeOp,
@@ -451,19 +451,26 @@ def _is_root_auth_entry(
return False
-async def confirm_invoke_host_function_op(op: StellarInvokeHostFunctionOp) -> None:
+async def confirm_invoke_host_function_op(
+ op: StellarInvokeHostFunctionOp, tx_source_account: str, network_id: AnyBytes
+) -> None:
from trezor.enums import StellarHostFunctionType, StellarSorobanCredentialsType
from trezor.ui.layouts import should_show_more
function = op.function
+ # the account whose signature authorizes the operation anyway: its
+ # explicit source account, or the transaction's otherwise
+ source_account = op.source_account or tx_source_account
+
if function.type == StellarHostFunctionType.HOST_FUNCTION_TYPE_INVOKE_CONTRACT:
if function.invoke_contract is None:
raise DataError("Stellar: missing invoke_contract")
- await confirm_invoke_contract_args(
+ await confirm_invoke_contract(
function.invoke_contract,
- br_name_prefix="op_invoke",
+ network_id,
+ source_account,
)
else:
raise ProcessError("Stellar: unsupported host function type")
@@ -490,7 +497,11 @@ async def confirm_invoke_host_function_op(op: StellarInvokeHostFunctionOp) -> No
):
shown += 1
await _confirm_auth_entry(
- auth_entry, shown, _is_root_auth_entry(auth_entry, function)
+ auth_entry,
+ shown,
+ network_id,
+ source_account,
+ is_root=_is_root_auth_entry(auth_entry, function),
)
else:
non_src_entries.append(auth_entry)
@@ -504,28 +515,48 @@ async def confirm_invoke_host_function_op(op: StellarInvokeHostFunctionOp) -> No
for auth_entry in non_src_entries:
shown += 1
await _confirm_auth_entry(
- auth_entry, shown, _is_root_auth_entry(auth_entry, function)
+ auth_entry,
+ shown,
+ network_id,
+ source_account,
+ is_root=_is_root_auth_entry(auth_entry, function),
)
async def _confirm_auth_entry(
- auth: StellarSorobanAuthorizationEntry, position: int, is_root: bool = False
+ auth: StellarSorobanAuthorizationEntry,
+ position: int,
+ network_id: AnyBytes,
+ source_account: str,
+ is_root: bool = False,
) -> None:
from trezor.enums import StellarSorobanCredentialsType
creds = auth.credentials
-
- if creds.type == StellarSorobanCredentialsType.SOROBAN_CREDENTIALS_ADDRESS_V2:
+ # SOURCE_ACCOUNT credentials authorize the tree through the effective
+ # operation source; address credentials name their authorizing party.
+ if creds.type == StellarSorobanCredentialsType.SOROBAN_CREDENTIALS_SOURCE_ACCOUNT:
+ authorizing_address = source_account
+ elif creds.type == StellarSorobanCredentialsType.SOROBAN_CREDENTIALS_ADDRESS_V2:
if creds.address_v2 is None:
raise DataError("Stellar: missing address_v2 credentials")
+ authorizing_address = creds.address_v2.address
await confirm_address(
f"{TR.words__authorization} #{position}",
- creds.address_v2.address,
+ authorizing_address,
description=TR.words__address,
br_name="op_auth_entry_address",
)
+ else:
+ raise ProcessError("Stellar: unsupported credentials type")
# Show the whole authorized invocation tree starting from its root (not just the
# nested sub-invocations), so the user sees exactly what this signature authorizes.
- await confirm_invocation(auth.root_invocation, f"#{position}", is_root=is_root)
+ await confirm_invocation(
+ auth.root_invocation,
+ f"#{position}",
+ network_id,
+ authorizing_address,
+ is_root=is_root,
+ )
### core/src/apps/stellar/sign_soroban_authorization.py
@@ -46,11 +46,11 @@ async def sign_soroban_authorization(
# Serialize the ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS preimage
# (Protocol 27, CAP-46-11/CAP-71). It binds the signature to the
# authorizing address.
+ network_id = sha256(msg.network_passphrase.encode()).digest()
+
w = bytearray()
writers.write_uint32(w, msg.envelope_type)
- writers.write_bytes_fixed(
- w, sha256(msg.network_passphrase.encode()).digest(), 32 # network id
- )
+ writers.write_bytes_fixed(w, network_id, 32)
writers.write_int64(w, auth.nonce)
writers.write_uint32(w, auth.signature_expiration_ledger)
writers.write_sc_address(w, auth.address)
@@ -63,7 +63,9 @@ async def sign_soroban_authorization(
# which the device account is a signer.
await layout.require_confirm_auth_on_behalf_of(auth.address)
- await layout.confirm_authorized_invocation(auth.invocation)
+ await layout.confirm_authorized_invocation(
+ auth.invocation, network_id, auth.address
+ )
await layout.require_confirm_signature_expiration_ledger(
auth.signature_expiration_ledger
)
### core/src/apps/stellar/sign_tx.py
@@ -144,7 +144,14 @@ async def sign_tx(msg: StellarSignTx, keychain: Slip21Keychain) -> StellarSigned
# Soroban transactions do not support memos
await layout.require_confirm_memo(memo_type, memo_confirm_text)
- await process_operation(w, op, current_output_index, verifier) # type: ignore [Argument of type "StellarInvokeHostFunctionOp | MessageType" cannot be assigned to parameter "op" of type "StellarMessageType" in function "process_operation"]
+ await process_operation(
+ w,
+ op, # type: ignore [Argument of type "StellarInvokeHostFunctionOp | MessageType" cannot be assigned to parameter "op" of type "StellarMessageType" in function "process_operation"]
+ current_output_index,
+ verifier,
+ msg.source_account,
+ network_passphrase_hash,
+ )
if msg.payment_req:
assert verifier is not None
### core/src/trezor/messages.py
@@ -6669,13 +6669,15 @@ class StellarInvokeContractArgs(protobuf.MessageType):
contract_address: "str"
function_name: "str"
args: "list[StellarSCVal]"
+ asset_hint: "StellarAsset | None"
def __init__(
self,
*,
contract_address: "str",
function_name: "str",
args: "list[StellarSCVal] | None" = None,
+ asset_hint: "StellarAsset | None" = None,
) -> None:
pass
### core/src/trezor/ui/layouts/bolt/__init__.py
@@ -1754,12 +1754,45 @@ def confirm_stellar_tx(
br_code=ButtonRequestType.SignTx,
)
+ async def confirm_stellar_address(
+ title: str,
+ subtitle: str,
+ address: str,
+ description: str,
+ br_name: str,
+ ) -> None:
+ await confirm_address(
+ title,
+ address,
+ subtitle=subtitle or None,
+ description=description,
+ verb=TR.buttons__continue,
+ br_name=br_name,
+ )
+
+ async def confirm_stellar_valid_until(
+ title: str,
+ subtitle: str,
+ live_until_ledger: int,
+ br_name: str,
+ ) -> None:
+ await confirm_value(
+ title,
+ str(live_until_ledger),
+ TR.stellar__valid_until_ledger,
+ br_name,
+ subtitle=subtitle or None,
+ is_data=False,
+ verb=TR.buttons__continue,
+ )
+
async def confirm_stellar_output_amount(
title: str,
subtitle: str,
amount: str,
asset: StellarAsset,
description: str | None = None,
+ token_contract: str | None = None,
) -> None:
from trezor.enums import StellarAssetType
@@ -1772,6 +1805,8 @@ async def confirm_stellar_output_amount(
None,
)
]
+ if token_contract:
+ info_items.append((TR.stellar__token_contract, token_contract, None))
await confirm_value(
title,
@@ -1794,6 +1829,7 @@ async def confirm_stellar_output(
asset: StellarAsset | None,
address_description: str | None = None,
amount_description: str | None = None,
+ token_contract: str | None = None,
) -> None:
await confirm_address(
f"{TR.words__recipient} #{output_index + 1}",
@@ -1811,6 +1847,7 @@ async def confirm_stellar_output(
amount=amount,
asset=asset,
description=amount_description or TR.words__amount,
+ token_contract=token_contract,
)
async def confirm_tron_claim(
### core/src/trezor/ui/layouts/caesar/__init__.py
@@ -1845,12 +1845,43 @@ def _stellar_title(title: str, subtitle: str) -> str:
# the layouts used here have no subtitle slot, join both parts into the title
return f"{title}: {subtitle}" if subtitle else title
+ async def confirm_stellar_address(
+ title: str,
+ subtitle: str,
+ address: str,
+ description: str,
+ br_name: str,
+ ) -> None:
+ await confirm_address(
+ _stellar_title(title, subtitle),
+ address,
+ description=description,
+ verb=TR.buttons__continue,
+ br_name=br_name,
+ )
+
+ async def confirm_stellar_valid_until(
+ title: str,
+ subtitle: str,
+ live_until_ledger: int,
+ br_name: str,
+ ) -> None:
+ await confirm_value(
+ _stellar_title(title, subtitle),
+ str(live_until_ledger),
+ TR.stellar__valid_until_ledger,
+ br_name,
+ is_data=False,
+ verb=TR.buttons__continue,
+ )
+
async def confirm_stellar_output_amount(
title: str,
subtitle: str,
amount: str,
asset: StellarAsset,
description: str | None = None,
+ token_contract: str | None = None,
) -> None:
from trezor.enums import StellarAssetType
@@ -1863,6 +1894,8 @@ async def confirm_stellar_output_amount(
None,
)
]
+ if token_contract:
+ info_items.append((TR.stellar__token_contract, token_contract, None))
await confirm_value(
_stellar_title(title, subtitle),
@@ -1883,6 +1916,7 @@ async def confirm_stellar_output(
asset: StellarAsset | None,
address_description: str | None = None,
amount_description: str | None = None,
+ token_contract: str | None = None,
) -> None:
await confirm_address(
f"{TR.words__recipient} #{output_index + 1}",
@@ -1900,6 +1934,7 @@ async def confirm_stellar_output(
amount=amount,
asset=asset,
description=amount_description or TR.words__amount,
+ token_contract=token_contract,
)
async def confirm_tron_claim(
### core/src/trezor/ui/layouts/delizia/__init__.py
@@ -1775,12 +1775,45 @@ def confirm_stellar_tx(
br_code=ButtonRequestType.SignTx,
)
+ async def confirm_stellar_address(
+ title: str,
+ subtitle: str,
+ address: str,
+ description: str,
+ br_name: str,
+ ) -> None:
+ await confirm_address(
+ title,
+ address,
+ subtitle=subtitle or None,
+ description=description,
+ verb=TR.buttons__continue,
+ br_name=br_name,
+ )
+
+ async def confirm_stellar_valid_until(
+ title: str,
+ subtitle: str,
+ live_until_ledger: int,
+ br_name: str,
+ ) -> None:
+ await confirm_value(
+ title,
+ str(live_until_ledger),
+ TR.stellar__valid_until_ledger,
+ br_name,
+ subtitle=subtitle or None,
+ is_data=False,
+ verb=TR.buttons__continue,
+ )
+
async def confirm_stellar_output_amount(
title: str,
subtitle: str,
amount: str,
asset: StellarAsset,
description: str | None = None,
+ token_contract: str | None = None,
) -> None:
from trezor.enums import StellarAssetType
@@ -1793,6 +1826,8 @@ async def confirm_stellar_output_amount(
None,
)
]
+ if token_contract:
+ info_items.append((TR.stellar__token_contract, token_contract, None))
await confirm_value(
title,
@@ -1813,6 +1848,7 @@ async def confirm_stellar_output(
asset: StellarAsset | None,
address_description: str | None = None,
amount_description: str | None = None,
+ token_contract: str | None = None,
) -> None:
subtitle = f"{TR.words__recipient} #{output_index + 1}"
await confirm_address(
@@ -1832,6 +1868,7 @@ async def confirm_stellar_output(
amount=amount,
asset=asset,
description=amount_description or TR.words__amount,
+ token_contract=token_contract,
)
async def confirm_tron_claim(
### core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -1878,12 +1878,45 @@ def confirm_stellar_tx(
br_code=ButtonRequestType.SignTx,
)
+ async def confirm_stellar_address(
+ title: str,
+ subtitle: str,
+ address: str,
+ description: str,
+ br_name: str,
+ ) -> None:
+ await confirm_address(
+ title,
+ address,
+ subtitle=subtitle or None,
+ description=description,
+ verb=TR.buttons__continue,
+ br_name=br_name,
+ )
+
+ async def confirm_stellar_valid_until(
+ title: str,
+ subtitle: str,
+ live_until_ledger: int,
+ br_name: str,
+ ) -> None:
+ await confirm_value(
+ title,
+ str(live_until_ledger),
+ TR.stellar__valid_until_ledger,
+ br_name,
+ subtitle=subtitle or None,
+ is_data=False,
+ verb=TR.buttons__continue,
+ )
+
async def confirm_stellar_output_amount(
title: str,
subtitle: str,
amount: str,
asset: StellarAsset,
description: str | None = None,
+ token_contract: str | None = None,
) -> None:
from trezor.enums import StellarAssetType
@@ -1896,6 +1929,8 @@ async def confirm_stellar_output_amount(
None,
)
]
+ if token_contract:
+ info_items.append((TR.stellar__token_contract, token_contract, None))
await confirm_value(
title,
@@ -1918,6 +1953,7 @@ async def confirm_stellar_output(
asset: StellarAsset | None,
address_description: str | None = None,
amount_description: str | None = None,
+ token_contract: str | None = None,
) -> None:
subtitle = f"{TR.words__recipient} #{output_index + 1}"
@@ -1938,6 +1974,7 @@ async def confirm_stellar_output(
amount=amount,
asset=asset,
description=amount_description or TR.words__amount,
+ token_contract=token_contract,
)
async def confirm_tron_claim(
### core/tests/test_apps.stellar.helpers.py
@@ -0,0 +1,71 @@
+# flake8: noqa: F403,F405
+from common import * # isort:skip
+
+if not utils.BITCOIN_ONLY:
+ from trezor.crypto.hashlib import sha256
+ from trezor.enums import StellarAssetType
+ from trezor.messages import StellarAsset
+
+ from apps.stellar.consts import (
+ NETWORK_PASSPHRASE_PUBLIC,
+ NETWORK_PASSPHRASE_TESTNET,
+ )
+ from apps.stellar.helpers import sac_address_from_asset
+
+
+@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
+class TestStellarHelpers(unittest.TestCase):
+ # Expected addresses cross-checked against stellar_sdk's
+ # Asset.contract_id(); the PUBLIC USDC one is Circle's well-known SAC.
+ def test_sac_address_from_asset(self):
+ native = StellarAsset(type=StellarAssetType.NATIVE)
+ usdc = StellarAsset( # ALPHANUM4
+ type=StellarAssetType.ALPHANUM4,
+ code="USDC",
+ issuer="GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
+ )
+ ustry = StellarAsset( # ALPHANUM12
+ type=StellarAssetType.ALPHANUM12,
+ code="USTRY",
+ issuer="GCRYUGD5NVARGXT56XEZI5CIFCQETYHAPQQTHO2O3IQZTHDH4LATMYWC",
+ )
+ # the same asset resolves to a different contract on each network
+ VECTORS = (
+ (
+ NETWORK_PASSPHRASE_PUBLIC,
+ native,
+ "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
+ ),
+ (
+ NETWORK_PASSPHRASE_PUBLIC,
+ usdc,
+ "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75",
+ ),
+ (
+ NETWORK_PASSPHRASE_PUBLIC,
+ ustry,
+ "CBLV4ATSIWU67CFSQU2NVRKINQIKUZ2ODSZBUJTJ43VJVRSBTZYOPNUR",
+ ),
+ (
+ NETWORK_PASSPHRASE_TESTNET,
+ native,
+ "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
+ ),
+ (
+ NETWORK_PASSPHRASE_TESTNET,
+ usdc,
+ "CA2E53VHFZ6YSWQIEIPBXJQGT6VW3VKWWZO555XKRQXYJ63GEBJJGHY7",
+ ),
+ (
+ NETWORK_PASSPHRASE_TESTNET,
+ ustry,
+ "CBEHZAPSMUJXT6R4X4LSQYEOOSNBNUQISUTHJNDZKMPSZQEKJC753HR3",
+ ),
+ )
+ for passphrase, asset, expected in VECTORS:
+ network_id = sha256(passphrase.encode()).digest()
+ self.assertEqual(sac_address_from_asset(network_id, asset), expected)
+
+
+if __name__ == "__main__":
+ unittest.main()
### core/tests/test_apps.stellar.layout.py
@@ -30,6 +30,8 @@
_format_sc_val,
_format_u128,
_format_u256,
+ _parse_sep41_approve,
+ _parse_sep41_transfer,
)
from apps.stellar.operations.layout import _is_root_auth_entry
@@ -39,6 +41,14 @@ def _u32(value):
def _u64(value):
return StellarSCVal(type=StellarSCValType.SCV_U64, u64=value)
+ def _address(value):
+ return StellarSCVal(type=StellarSCValType.SCV_ADDRESS, address=value)
+
+ def _i128(lo, hi=0):
+ return StellarSCVal(
+ type=StellarSCValType.SCV_I128, i128=StellarInt128Parts(hi=hi, lo=lo)
+ )
+
def _bytes(value):
return StellarSCVal(type=StellarSCValType.SCV_BYTES, bytes=value)
@@ -224,6 +234,116 @@ def test_missing_val(self):
_CONTRACT_A = "CAAACAQDAQCQMBYIBEFAWDANBYHRAEISCMKBKFQXDAMRUGY4DUPB6N4O"
_CONTRACT_B = "CBSGKZTHNBUWU23MNVXG64DROJZXI5LWO54HS6T3PR6X474AQGBIHDKP"
+# valid account (G...) strkeys
+_ACCOUNT_A = "GAXSFOOGF4ELO5HT5PTN23T5XE6D5QWL3YBHSVQ2HWOFEJNYYMRJENBV"
+_ACCOUNT_B = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
+
+
+@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
+class TestStellarParseSep41Call(unittest.TestCase):
+ """The SEP-41 parsers dispatch between the token UI and the raw contract
+ flow: anything but a well-formed call must parse as None (fall back),
+ never as a mangled token operation."""
+
+ def test_parse_sep41_transfer(self):
+ TESTS = [
+ # (function_name, args, expected)
+ (
+ "transfer",
+ [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(500)],
+ (_ACCOUNT_A, _ACCOUNT_B, 500),
+ ),
+ # a zero amount is well-formed
+ (
+ "transfer",
+ [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(0)],
+ (_ACCOUNT_A, _ACCOUNT_B, 0),
+ ),
+ # amounts beyond 64 bits are deliberately still shown
+ (
+ "transfer",
+ [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(0, hi=1)],
+ (_ACCOUNT_A, _ACCOUNT_B, 2**64),
+ ),
+ # a contract can be the sender (e.g. a liquidity pool)
+ (
+ "transfer",
+ [_address(_CONTRACT_A), _address(_ACCOUNT_B), _i128(500)],
+ (_CONTRACT_A, _ACCOUNT_B, 500),
+ ),
+ # not a transfer
+ ("approve", [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(500)], None),
+ # wrong argument count
+ ("transfer", [_address(_ACCOUNT_A), _address(_ACCOUNT_B)], None),
+ (
+ "transfer",
+ [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(500), _u32(1)],
+ None,
+ ),
+ # wrong argument types
+ ("transfer", [_symbol("from"), _address(_ACCOUNT_B), _i128(500)], None),
+ ("transfer", [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _u32(500)], None),
+ # negative amounts always fall back to the generic contract UI
+ (
+ "transfer",
+ [
+ _address(_ACCOUNT_A),
+ _address(_ACCOUNT_B),
+ _i128(0xFFFFFFFFFFFFFFFF, hi=-1),
+ ],
+ None,
+ ),
+ ]
+ for function_name, args, expected in TESTS:
+ call = StellarInvokeContractArgs(
+ contract_address=_CONTRACT_A, function_name=function_name, args=args
+ )
+ self.assertEqual(_parse_sep41_transfer(call), expected)
+
+ def test_parse_sep41_approve(self):
+ approve_args = [
+ _address(_ACCOUNT_A),
+ _address(_ACCOUNT_B),
+ _i128(500),
+ _u32(800_000),
+ ]
+ TESTS = [
+ # (function_name, args, expected)
+ ("approve", approve_args, (_ACCOUNT_A, _ACCOUNT_B, 500, 800_000)),
+ # a zero amount is well-formed (it revokes the approval)
+ (
+ "approve",
+ [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(0), _u32(800_000)],
+ (_ACCOUNT_A, _ACCOUNT_B, 0, 800_000),
+ ),
+ # not an approve
+ ("transfer", approve_args, None),
+ # wrong argument count
+ ("approve", approve_args[:3], None),
+ # live_until_ledger must be a u32
+ (
+ "approve",
+ [_address(_ACCOUNT_A), _address(_ACCOUNT_B), _i128(500), _u64(800_000)],
+ None,
+ ),
+ # negative amounts always fall back to the generic contract UI
+ (
+ "approve",
+ [
+ _address(_ACCOUNT_A),
+ _address(_ACCOUNT_B),
+ _i128(0xFFFFFFFFFFFFFFFF, hi=-1),
+ _u32(800_000),
+ ],
+ None,
+ ),
+ ]
+ for function_name, args, expected in TESTS:
+ call = StellarInvokeContractArgs(
+ contract_address=_CONTRACT_A, function_name=function_name, args=args
+ )
+ self.assertEqual(_parse_sep41_approve(call), expected)
+
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestStellarIsRootAuthEntry(unittest.TestCase):
### core/translations/en.json
@@ -2866,6 +2866,7 @@
"stellar__add_trust": "Add trust",
"stellar__all_will_be_sent_to": "All XLM will be sent to",
"stellar__allow_trust": "Allow trust",
+ "stellar__approve_token": "Approve token",
"stellar__balance_id": "Balance ID",
"stellar__bump_sequence": "Bump Sequence",
"stellar__buying": "Buying",
@@ -2884,6 +2885,7 @@
"stellar__ext_auth": "External Authorizations",
"stellar__ext_auth_message": "Transaction contains additional invocations authorized by external means.",
"stellar__final_confirm": "Final confirm",
+ "stellar__from": "From",
"stellar__hash": "Hash",
"stellar__high": "High",
"stellar__home_domain": "Home Domain",
@@ -2907,6 +2909,7 @@
"stellar__preauth_transaction": "Pre-auth transaction",
"stellar__price_per_template": "Price per {0}",
"stellar__remove_signer": "Remove Signer",
+ "stellar__revoke_approval": "Revoke approval",
"stellar__revoke_trust": "Revoke trust",
"stellar__selling": "Selling",
"stellar__set_data": "Set data",
@@ -2915,10 +2918,14 @@
"stellar__sign_authorization": "Sign authorization",
"stellar__sign_with": "Sign with",
"stellar__source_account": "Source account",
+ "stellar__spender": "Spender",
"stellar__timebounds": "Timebounds",
+ "stellar__to": "To",
+ "stellar__token_contract": "Token contract",
"stellar__token_info": "Token info",
"stellar__transaction_source": "Transaction source",
"stellar__transaction_source_diff_warning": "Transaction source does not belong to this Trezor.",
+ "stellar__transfer_token": "Transfer token",
"stellar__trusted_account": "Trusted Account",
"stellar__update": "Update",
"stellar__valid_from": "Valid from (UTC)",
### core/translations/order.json
@@ -1275,5 +1275,12 @@
"1273": "buttons__disconnect",
"1274": "buttons__forget",
"1275": "buttons__show_next",
- "1276": "buttons__wipe"
+ "1276": "buttons__wipe",
+ "1277": "stellar__approve_token",
+ "1278": "stellar__from",
+ "1279": "stellar__revoke_approval",
+ "1280": "stellar__spender",
+ "1281": "stellar__to",
+ "1282": "stellar__token_contract",
+ "1283": "stellar__transfer_token"
}
### core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "60c6135faa4787bbdf2ddd2b90f10e246cfa87c1fb2298ecfc1dd5e3e9449958",
- "datetime": "2026-08-18T09:59:52.528039+00:00",
- "commit": "10c2997d2a6bbe3ca29991f895fd8b5c20df15a4"
+ "merkle_root": "f5f0aa272574c1dc17f27902c61dd33a26db2e78897311e20aea2f8a5cc11eba",
+ "datetime": "2026-08-20T10:28:47.614559+00:00",
+ "commit": "36048b7890a88b28270e1ecd552092cd2fc7e222"
},
"history": [
{
### python/.changelog.d/7310.added
@@ -0,0 +1 @@
+Stellar: Add asset hints for SEP-41 token invocations of Stellar Asset Contracts.
### python/src/trezorlib/cli/stellar.py
@@ -30,6 +30,7 @@
try:
from stellar_sdk import (
+ Asset,
FeeBumpTransactionEnvelope,
parse_transaction_envelope_from_xdr,
)
@@ -38,6 +39,24 @@
pass
PATH_HELP = "BIP32 path. Always use hardened paths and the m/44h/148h/ prefix"
+ASSET_HINT_HELP = (
+ "Asset whose Stellar Asset Contract is invoked, in the SEP-11 notation "
+ "CODE:ISSUER, or `native`. Lets the device show the invocation as a token "
+ "operation. Repeatable."
+)
+
+
+def _parse_asset(value: str) -> "Asset":
+ """Parse an asset written the SEP-11 way: `native`, or CODE:ISSUER."""
+ try:
+ if value == "native":
+ return Asset.native()
+ code, _, issuer = value.partition(":")
+ if not issuer:
+ raise ValueError("expected CODE:ISSUER or `native`")
+ return Asset(code, issuer)
+ except Exception as e:
+ raise click.BadParameter(f"invalid asset {value!r}: {e}")
@click.group(name="stellar")
@@ -79,10 +98,15 @@ def get_address(
required=False,
help="Network passphrase (blank for public network).",
)
+@click.option("-a", "--asset-hint", multiple=True, help=ASSET_HINT_HELP)
@click.argument("b64envelope")
@with_session
def sign_transaction(
- session: "Session", b64envelope: str, address: str, network_passphrase: str
+ session: "Session",
+ b64envelope: str,
+ address: str,
+ network_passphrase: str,
+ asset_hint: tuple[str, ...],
) -> bytes:
"""Sign a base64-encoded transaction envelope.
@@ -111,7 +135,8 @@ def sign_transaction(
sys.exit(1)
address_n = tools.parse_path(address)
- tx, operations, tx_ext = stellar.from_envelope(envelope)
+ asset_hints = [_parse_asset(a) for a in asset_hint]
+ tx, operations, tx_ext = stellar.from_envelope(envelope, asset_hints=asset_hints)
resp = stellar.sign_tx(
session, tx, operations, tx_ext, address_n, network_passphrase
)
@@ -142,6 +167,7 @@ def sign_transaction(
help="Override the entry's signature_expiration_ledger "
"(the last ledger sequence at which the authorization is valid).",
)
+@click.option("-a", "--asset-hint", multiple=True, help=ASSET_HINT_HELP)
@click.argument("b64entry")
@with_session
def sign_soroban_authorization(
@@ -150,6 +176,7 @@ def sign_soroban_authorization(
address: str,
network_passphrase: str,
valid_until_ledger: int | None,
+ asset_hint: tuple[str, ...],
) -> bytes:
"""Sign a base64-encoded Soroban authorization entry.
@@ -191,7 +218,10 @@ def sign_soroban_authorization(
sys.exit(1)
address_n = tools.parse_path(address)
- authorization = stellar.from_authorization_entry(entry_xdr)
+ asset_hints = [_parse_asset(a) for a in asset_hint]
+ authorization = stellar.from_authorization_entry(
+ entry_xdr, network_passphrase, asset_hints=asset_hints
+ )
if valid_until_ledger is not None:
authorization.signature_expiration_ledger = valid_until_ledger
resp = stellar.sign_soroban_authorization(
### python/src/trezorlib/messages.py
@@ -8577,6 +8577,7 @@ class StellarInvokeContractArgs(protobuf.MessageType):
1: protobuf.Field("contract_address", "string", repeated=False, required=True),
2: protobuf.Field("function_name", "string", repeated=False, required=True),
3: protobuf.Field("args", "StellarSCVal", repeated=True, required=False, default=None),
+ 4: protobuf.Field("asset_hint", "StellarAsset", repeated=False, required=False, default=None),
}
def __init__(
@@ -8585,10 +8586,12 @@ def __init__(
contract_address: "str",
function_name: "str",
args: Optional[Sequence["StellarSCVal"]] = None,
+ asset_hint: Optional["StellarAsset"] = None,
) -> None:
self.args: Sequence["StellarSCVal"] = args if args is not None else []
self.contract_address = contract_address
self.function_name = function_name
+ self.asset_hint = asset_hint
class StellarSorobanAuthorizedFunction(protobuf.MessageType):
### python/src/trezorlib/stellar.py
@@ -15,7 +15,17 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
from decimal import Decimal
-from typing import TYPE_CHECKING, Any, List, Tuple, Union
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Tuple,
+ Union,
+)
from . import exceptions, messages
from .tools import workflow
@@ -95,13 +105,20 @@
def from_envelope(
envelope: "TransactionEnvelope",
+ asset_hints: Iterable["Asset"] = (),
) -> Tuple[messages.StellarSignTx, List["StellarMessageType"], messages.StellarTxExt]:
"""Parse a transaction envelope into a tuple of:
tx - a StellarSignTx describing the transaction header
operations - a list of protobuf messages, one per operation
tx_ext - a StellarTxExt describing the transaction extension: v=1 carrying
the Soroban data for Soroban transactions, otherwise v=0
+
+ Each asset in `asset_hints` is attached to the contract invocations that
+ target its Stellar Asset Contract, letting the device present them as token
+ operations. Hints matching no invocation are ignored. The device re-derives
+ the address before using a hint and falls back to the raw contract UI if it
+ does not match.
"""
if not HAVE_STELLAR_SDK:
raise RuntimeError("Stellar SDK not available")
@@ -149,6 +166,13 @@ def from_envelope(
operations = [_read_operation(op) for op in parsed_tx.operations]
+ asset_hints = list(asset_hints)
+ if asset_hints:
+ sacs = _sac_addresses(asset_hints, envelope.network_passphrase)
+ for op in operations:
+ for args in _operation_contract_args(op):
+ args.asset_hint = sacs.get(args.contract_address)
+
if parsed_tx.soroban_data:
tx_ext = messages.StellarTxExt(
v=1,
@@ -162,12 +186,17 @@ def from_envelope(
def from_authorization_entry(
entry: "xdr.SorobanAuthorizationEntry",
+ network_passphrase: Optional[str] = None,
+ asset_hints: Iterable["Asset"] = (),
) -> messages.StellarSorobanAuthorizationWithAddress:
"""Translate a Soroban authorization entry into its signing request payload.
The resulting message carries exactly the fields committed into the
entry's authorization payload (the WITH_ADDRESS preimage of Protocol 27).
Only SOROBAN_CREDENTIALS_ADDRESS_V2 entries are supported.
+
+ See `from_envelope` for `asset_hints`. Matching them needs the network the
+ entry is signed for, so `network_passphrase` is required alongside them.
"""
if not HAVE_STELLAR_SDK:
raise RuntimeError("Stellar SDK not available")
@@ -180,14 +209,57 @@ def from_authorization_entry(
)
credentials = entry.credentials.address_v2
assert credentials is not None
+
+ invocation = _read_authorized_invocation(entry.root_invocation)
+ asset_hints = list(asset_hints)
+ if asset_hints:
+ if network_passphrase is None:
+ raise ValueError("network_passphrase is required to match asset hints")
+ sacs = _sac_addresses(asset_hints, network_passphrase)
+ for args in _invocation_contract_args(invocation):
+ args.asset_hint = sacs.get(args.contract_address)
+
return messages.StellarSorobanAuthorizationWithAddress(
nonce=credentials.nonce.int64,
signature_expiration_ledger=credentials.signature_expiration_ledger.uint32,
address=_read_sc_address(credentials.address),
- invocation=_read_authorized_invocation(entry.root_invocation),
+ invocation=invocation,
)
+def _sac_addresses(
+ asset_hints: Iterable["Asset"], network_passphrase: str
+) -> Dict[str, messages.StellarAsset]:
+ """Index the hinted assets by the address of their Stellar Asset Contract."""
+ return {
+ asset.contract_id(network_passphrase): _read_asset(asset)
+ for asset in asset_hints
+ }
+
+
+def _invocation_contract_args(
+ invocation: messages.StellarSorobanAuthorizedInvocation,
+) -> Iterator[messages.StellarInvokeContractArgs]:
+ """Walk the contract invocations of an authorized invocation tree."""
+ if invocation.function.contract_fn is not None:
+ yield invocation.function.contract_fn
+ for sub in invocation.sub_invocations:
+ yield from _invocation_contract_args(sub)
+
+
+def _operation_contract_args(
+ op: "StellarMessageType",
+) -> Iterator[messages.StellarInvokeContractArgs]:
+ """Walk the contract invocations an operation displays: the host function
+ it invokes and the authorization trees it carries."""
+ if not isinstance(op, messages.StellarInvokeHostFunctionOp):
+ return
+ if op.function.invoke_contract is not None:
+ yield op.function.invoke_contract
+ for entry in op.auth:
+ yield from _invocation_contract_args(entry.root_invocation)
+
+
def _read_operation(op: "Operation") -> "StellarMessageType":
# TODO: Let's add muxed account support later.
if op.source:
### python/tests/test_stellar.py
@@ -24,6 +24,7 @@
Address,
Asset,
AuthorizationFlag,
+ InvokeHostFunction,
MuxedAccount,
Network,
TransactionBuilder,
@@ -979,20 +980,38 @@ def test_claim_claimable_balance():
SOROBAN_CONTRACT = "CABQUEIYD4TC2NB3IJEVAV26MVWHG6UBRCHZNHNEVOZLTQGHZ3K5ZIRI"
SOROBAN_DESTINATION = "GBOVKZBEM2YYLOCDCUXJ4IMRKHN4LCJAE7WEAEA2KF562XFAGDBOB64V"
+# A real asset, so the address of its Stellar Asset Contract can be derived
+# (CAP-46-2) and asset hints matched against invocations of that contract.
+SAC_ASSET = Asset("USDC", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5")
+SAC_ADDRESS = SAC_ASSET.contract_id(Network.TESTNET_NETWORK_PASSPHRASE)
+SAC_ASSET_HINT = messages.StellarAsset(
+ type=messages.StellarAssetType.ALPHANUM4,
+ code=SAC_ASSET.code,
+ issuer=SAC_ASSET.issuer,
+)
+UNRELATED_ASSET = Asset(
+ "USDX", "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
+)
+NATIVE_SAC_ADDRESS = Asset.native().contract_id(Network.TESTNET_NETWORK_PASSPHRASE)
+NATIVE_ASSET_HINT = messages.StellarAsset(type=messages.StellarAssetType.NATIVE)
+
skip_if_no_protocol_27 = pytest.mark.skipif(
not stellar.HAVE_STELLAR_SDK_PROTOCOL_27,
reason="requires Stellar SDK with Protocol 27 support",
)
def make_soroban_invocation(
- function_name="transfer", amount=500_111_000, sub_invocations=()
+ function_name="transfer",
+ amount=500_111_000,
+ sub_invocations=(),
+ contract_address=SOROBAN_CONTRACT,
):
return stellar_xdr.SorobanAuthorizedInvocation(
function=stellar_xdr.SorobanAuthorizedFunction(
type=stellar_xdr.SorobanAuthorizedFunctionType.SOROBAN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN,
contract_fn=stellar_xdr.InvokeContractArgs(
- contract_address=Address(SOROBAN_CONTRACT).to_xdr_sc_address(),
+ contract_address=Address(contract_address).to_xdr_sc_address(),
function_name=stellar_xdr.SCSymbol(function_name.encode()),
args=[
scval.to_address(SOROBAN_SOURCE),
@@ -1149,3 +1168,112 @@ def test_from_authorization_entry_legacy_address_credentials():
with pytest.raises(ValueError, match="Unsupported SorobanCredentials type"):
stellar.from_authorization_entry(entry)
+
+
+def make_sac_transfer_tx(auth=()):
+ """A transaction invoking the SAC directly, with optional auth entries."""
+ op = InvokeHostFunction(
+ host_function=stellar_xdr.HostFunction(
+ type=stellar_xdr.HostFunctionType.HOST_FUNCTION_TYPE_INVOKE_CONTRACT,
+ invoke_contract=stellar_xdr.InvokeContractArgs(
+ contract_address=Address(SAC_ADDRESS).to_xdr_sc_address(),
+ function_name=stellar_xdr.SCSymbol(b"transfer"),
+ args=[
+ scval.to_address(SOROBAN_SOURCE),
+ scval.to_address(SOROBAN_DESTINATION),
+ scval.to_int128(500_111_000),
+ ],
+ ),
+ ),
+ auth=list(auth),
+ )
+ return make_default_tx().append_operation(op).build()
+
+
+def test_from_envelope_asset_hints():
+ # the entry authorizes a non-SAC root with a SAC sub-invocation
+ entry = stellar_xdr.SorobanAuthorizationEntry(
+ credentials=stellar_xdr.SorobanCredentials(
+ type=stellar_xdr.SorobanCredentialsType.SOROBAN_CREDENTIALS_SOURCE_ACCOUNT
+ ),
+ root_invocation=make_soroban_invocation(
+ sub_invocations=[make_soroban_invocation(contract_address=SAC_ADDRESS)]
+ ),
+ )
+ envelope = make_sac_transfer_tx(auth=[entry])
+
+ _, operations, _ = stellar.from_envelope(envelope, asset_hints=[SAC_ASSET])
+
+ op = operations[0]
+ assert op.function.invoke_contract.asset_hint == SAC_ASSET_HINT
+ root = op.auth[0].root_invocation
+ assert root.function.contract_fn.asset_hint is None
+ assert root.sub_invocations[0].function.contract_fn.asset_hint == SAC_ASSET_HINT
+
+
+def test_from_envelope_multiple_asset_hints():
+ # hints are matched to invocations by contract address, so their order
+ # does not matter and each lands only on the calls of its own SAC
+ entry = stellar_xdr.SorobanAuthorizationEntry(
+ credentials=stellar_xdr.SorobanCredentials(
+ type=stellar_xdr.SorobanCredentialsType.SOROBAN_CREDENTIALS_SOURCE_ACCOUNT
+ ),
+ root_invocation=make_soroban_invocation(
+ sub_invocations=[
+ make_soroban_invocation(contract_address=SAC_ADDRESS),
+ make_soroban_invocation(contract_address=NATIVE_SAC_ADDRESS),
+ ]
+ ),
+ )
+ envelope = make_sac_transfer_tx(auth=[entry])
+
+ _, operations, _ = stellar.from_envelope(
+ envelope, asset_hints=[Asset.native(), SAC_ASSET]
+ )
+
+ root = operations[0].auth[0].root_invocation
+ assert root.function.contract_fn.asset_hint is None
+ subs = root.sub_invocations
+ assert subs[0].function.contract_fn.asset_hint == SAC_ASSET_HINT
+ assert subs[1].function.contract_fn.asset_hint == NATIVE_ASSET_HINT
+
+
+def test_from_envelope_unmatched_asset_hint():
+ envelope = make_sac_transfer_tx()
+
+ # a hint whose SAC is not invoked anywhere is dropped, not sent
+ _, operations, _ = stellar.from_envelope(envelope, asset_hints=[UNRELATED_ASSET])
+ assert operations[0].function.invoke_contract.asset_hint is None
+
+ # and no hints means no stamping at all
+ _, operations, _ = stellar.from_envelope(envelope)
+ assert operations[0].function.invoke_contract.asset_hint is None
+
+
+@skip_if_no_protocol_27
+def test_from_authorization_entry_asset_hints():
+ entry = make_authorization_entry(
+ SOROBAN_SOURCE,
+ 123456789,
+ 600000,
+ sub_invocations=[make_soroban_invocation(contract_address=SAC_ADDRESS)],
+ )
+
+ authorization = stellar.from_authorization_entry(
+ entry, Network.TESTNET_NETWORK_PASSPHRASE, asset_hints=[SAC_ASSET]
+ )
+
+ assert authorization.invocation.function.contract_fn.asset_hint is None
+ sub = authorization.invocation.sub_invocations[0]
+ assert sub.function.contract_fn.asset_hint == SAC_ASSET_HINT
+
+
+@skip_if_no_protocol_27
+def test_from_authorization_entry_hints_require_network_passphrase():
+ entry = make_authorization_entry(SOROBAN_SOURCE, 123456789, 600000)
+
+ with pytest.raises(ValueError, match="network_passphrase is required"):
+ stellar.from_authorization_entry(entry, asset_hints=[SAC_ASSET])
+
+ # an exhausted iterator carries no hints, so no passphrase is needed
+ stellar.from_authorization_entry(entry, asset_hints=iter(()))
### rust/trezor-client/src/protos/generated/messages_stellar.rs
@@ -8261,6 +8261,8 @@ pub struct StellarInvokeContractArgs {
pub function_name: ::std::option::Option<::std::string::String>,
// @@protoc_insertion_point(field:hw.trezor.messages.stellar.StellarInvokeContractArgs.args)
pub args: ::std::vec::Vec<StellarSCVal>,
+ // @@protoc_insertion_point(field:hw.trezor.messages.stellar.StellarInvokeContractArgs.asset_hint)
+ pub asset_hint: ::protobuf::MessageField<StellarAsset>,
// special fields
// @@protoc_insertion_point(special_field:hw.trezor.messages.stellar.StellarInvokeContractArgs.special_fields)
pub special_fields: ::protobuf::SpecialFields,
@@ -8350,7 +8352,7 @@ impl StellarInvokeContractArgs {
}
fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData {
- let mut fields = ::std::vec::Vec::with_capacity(3);
+ let mut fields = ::std::vec::Vec::with_capacity(4);
let mut oneofs = ::std::vec::Vec::with_capacity(0);
fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>(
"contract_address",
@@ -8367,6 +8369,11 @@ impl StellarInvokeContractArgs {
|m: &StellarInvokeContractArgs| { &m.args },
|m: &mut StellarInvokeContractArgs| { &mut m.args },
));
+ fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, StellarAsset>(
+ "asset_hint",
+ |m: &StellarInvokeContractArgs| { &m.asset_hint },
+ |m: &mut StellarInvokeContractArgs| { &mut m.asset_hint },
+ ));
::protobuf::reflect::GeneratedMessageDescriptorData::new_2::<StellarInvokeContractArgs>(
"StellarInvokeContractArgs",
fields,
@@ -8390,6 +8397,11 @@ impl ::protobuf::Message for StellarInvokeContractArgs {
return false;
}
};
+ for v in &self.asset_hint {
+ if !v.is_initialized() {
+ return false;
+ }
+ };
true
}
@@ -8405,6 +8417,9 @@ impl ::protobuf::Message for StellarInvokeContractArgs {
26 => {
self.args.push(is.read_message()?);
},
+ 34 => {
+ ::protobuf::rt::read_singular_message_into_field(is, &mut self.asset_hint)?;
+ },
tag => {
::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?;
},
@@ -8427,6 +8442,10 @@ impl ::protobuf::Message for StellarInvokeContractArgs {
let len = value.compute_size();
my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len;
};
+ if let Some(v) = self.asset_hint.as_ref() {
+ let len = v.compute_size();
+ my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len;
+ }
my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields());
self.special_fields.cached_size().set(my_size as u32);
my_size
@@ -8442,6 +8461,9 @@ impl ::protobuf::Message for StellarInvokeContractArgs {
for v in &self.args {
::protobuf::rt::write_message_field_with_cached_size(3, v, os)?;
};
+ if let Some(v) = self.asset_hint.as_ref() {
+ ::protobuf::rt::write_message_field_with_cached_size(4, v, os)?;
+ }
os.write_unknown_fields(self.special_fields.unknown_fields())?;
::std::result::Result::Ok(())
}
@@ -8462,6 +8484,7 @@ impl ::protobuf::Message for StellarInvokeContractArgs {
self.contract_address = ::std::option::Option::None;
self.function_name = ::std::option::Option::None;
self.args.clear();
+ self.asset_hint.clear();
self.special_fields.clear();
}
@@ -8470,6 +8493,7 @@ impl ::protobuf::Message for StellarInvokeContractArgs {
contract_address: ::std::option::Option::None,
function_name: ::std::option::Option::None,
args: ::std::vec::Vec::new(),
+ asset_hint: ::protobuf::MessageField::none(),
special_fields: ::protobuf::SpecialFields::new(),
};
&instance
@@ -11292,66 +11316,68 @@ static file_descriptor_proto_data: &'static [u8] = b"\
C\x10\x10\x12\x0b\n\x07SCV_MAP\x10\x11\x12\x0f\n\x0bSCV_ADDRESS\x10\x12\
\"\x04\x08\x02\x10\x02\"\x04\x08\x13\x10\x13\"\x04\x08\x14\x10\x14\"\x04\
\x08\x15\x10\x15J\x04\x08\x03\x10\x04J\x04\x08\x14\x10\x15J\x04\x08\x15\
- \x10\x16\"\xa9\x01\n\x19StellarInvokeContractArgs\x12)\n\x10contract_add\
+ \x10\x16\"\xf2\x01\n\x19StellarInvokeContractArgs\x12)\n\x10contract_add\
ress\x18\x01\x20\x02(\tR\x0fcontractAddress\x12#\n\rfunction_name\x18\
\x02\x20\x02(\tR\x0cfunctionName\x12<\n\x04args\x18\x03\x20\x03(\x0b2(.h\
- w.trezor.messages.stellar.StellarSCValR\x04args\"\xd7\x02\n\x20StellarSo\
- robanAuthorizedFunction\x12u\n\x04type\x18\x01\x20\x02(\x0e2a.hw.trezor.\
- messages.stellar.StellarSorobanAuthorizedFunction.StellarSorobanAuthoriz\
- edFunctionTypeR\x04type\x12V\n\x0bcontract_fn\x18\x02\x20\x01(\x0b25.hw.\
- trezor.messages.stellar.StellarInvokeContractArgsR\ncontractFn\"d\n$Stel\
- larSorobanAuthorizedFunctionType\x120\n,SOROBAN_AUTHORIZED_FUNCTION_TYPE\
- _CONTRACT_FN\x10\0\"\x04\x08\x01\x10\x01\"\x04\x08\x02\x10\x02\"\xe7\x01\
- \n\"StellarSorobanAuthorizedInvocation\x12X\n\x08function\x18\x01\x20\
- \x02(\x0b2<.hw.trezor.messages.stellar.StellarSorobanAuthorizedFunctionR\
- \x08function\x12g\n\x0fsub_invocations\x18\x02\x20\x03(\x0b2>.hw.trezor.\
- messages.stellar.StellarSorobanAuthorizedInvocationR\x0esubInvocations\"\
- \xa7\x02\n\x13StellarHostFunction\x12[\n\x04type\x18\x01\x20\x02(\x0e2G.\
- hw.trezor.messages.stellar.StellarHostFunction.StellarHostFunctionTypeR\
- \x04type\x12^\n\x0finvoke_contract\x18\x02\x20\x01(\x0b25.hw.trezor.mess\
- ages.stellar.StellarInvokeContractArgsR\x0einvokeContract\"S\n\x17Stella\
- rHostFunctionType\x12&\n\"HOST_FUNCTION_TYPE_INVOKE_CONTRACT\x10\0\"\x04\
- \x08\x01\x10\x01\"\x04\x08\x02\x10\x02\"\x04\x08\x03\x10\x03\"\xda\x01\n\
- \x20StellarSorobanAddressCredentials\x12\x18\n\x07address\x18\x01\x20\
- \x02(\tR\x07address\x12\x14\n\x05nonce\x18\x02\x20\x02(\x12R\x05nonce\
- \x12>\n\x1bsignature_expiration_ledger\x18\x03\x20\x02(\rR\x19signatureE\
- xpirationLedger\x12F\n\tsignature\x18\x04\x20\x02(\x0b2(.hw.trezor.messa\
- ges.stellar.StellarSCValR\tsignature\"\xd4\x02\n\x19StellarSorobanCreden\
- tials\x12g\n\x04type\x18\x01\x20\x02(\x0e2S.hw.trezor.messages.stellar.S\
- tellarSorobanCredentials.StellarSorobanCredentialsTypeR\x04type\x12[\n\n\
- address_v2\x18\x02\x20\x01(\x0b2<.hw.trezor.messages.stellar.StellarSoro\
- banAddressCredentialsR\taddressV2\"q\n\x1dStellarSorobanCredentialsType\
- \x12&\n\"SOROBAN_CREDENTIALS_SOURCE_ACCOUNT\x10\0\x12\"\n\x1eSOROBAN_CRE\
- DENTIALS_ADDRESS_V2\x10\x02\"\x04\x08\x01\x10\x01\"\xe4\x01\n\x20Stellar\
- SorobanAuthorizationEntry\x12W\n\x0bcredentials\x18\x01\x20\x02(\x0b25.h\
- w.trezor.messages.stellar.StellarSorobanCredentialsR\x0bcredentials\x12g\
- \n\x0froot_invocation\x18\x02\x20\x02(\x0b2>.hw.trezor.messages.stellar.\
- StellarSorobanAuthorizedInvocationR\x0erootInvocation\"\xe3\x01\n\x1bSte\
- llarInvokeHostFunctionOp\x12%\n\x0esource_account\x18\x01\x20\x01(\tR\rs\
- ourceAccount\x12K\n\x08function\x18\x02\x20\x02(\x0b2/.hw.trezor.message\
- s.stellar.StellarHostFunctionR\x08function\x12P\n\x04auth\x18\x03\x20\
- \x03(\x0b2<.hw.trezor.messages.stellar.StellarSorobanAuthorizationEntryR\
- \x04auth\"\x86\x06\n\x1fStellarSignSorobanAuthorization\x12\x1b\n\taddre\
- ss_n\x18\x01\x20\x03(\rR\x08addressN\x12-\n\x12network_passphrase\x18\
- \x02\x20\x02(\tR\x11networkPassphrase\x12\x88\x01\n\renvelope_type\x18\
- \x03\x20\x02(\x0e2c.hw.trezor.messages.stellar.StellarSignSorobanAuthori\
- zation.StellarSorobanAuthorizationEnvelopeTypeR\x0cenvelopeType\x12\xaf\
- \x01\n\"soroban_authorization_with_address\x18\x04\x20\x01(\x0b2b.hw.tre\
- zor.messages.stellar.StellarSignSorobanAuthorization.StellarSorobanAutho\
- rizationWithAddressR\x1fsorobanAuthorizationWithAddress\x1a\xf8\x01\n&St\
- ellarSorobanAuthorizationWithAddress\x12\x14\n\x05nonce\x18\x01\x20\x02(\
- \x12R\x05nonce\x12>\n\x1bsignature_expiration_ledger\x18\x02\x20\x02(\rR\
- \x19signatureExpirationLedger\x12\x18\n\x07address\x18\x03\x20\x02(\tR\
- \x07address\x12^\n\ninvocation\x18\x04\x20\x02(\x0b2>.hw.trezor.messages\
- .stellar.StellarSorobanAuthorizedInvocationR\ninvocation\"_\n'StellarSor\
- obanAuthorizationEnvelopeType\x124\n0ENVELOPE_TYPE_SOROBAN_AUTHORIZATION\
- _WITH_ADDRESS\x10\n\"c\n$StellarSorobanAuthorizationSignature\x12\x1d\n\
- \npublic_key\x18\x01\x20\x02(\x0cR\tpublicKey\x12\x1c\n\tsignature\x18\
- \x02\x20\x02(\x0cR\tsignature\"\x15\n\x13StellarTxExtRequest\"?\n\x0cSte\
- llarTxExt\x12\x0c\n\x01v\x18\x01\x20\x02(\x11R\x01v\x12!\n\x0csoroban_da\
- ta\x18\x02\x20\x01(\x0cR\x0bsorobanData*=\n\x10StellarAssetType\x12\n\n\
- \x06NATIVE\x10\0\x12\r\n\tALPHANUM4\x10\x01\x12\x0e\n\nALPHANUM12\x10\
- \x02B;\n#com.satoshilabs.trezor.lib.protobufB\x14TrezorMessageStellar\
+ w.trezor.messages.stellar.StellarSCValR\x04args\x12G\n\nasset_hint\x18\
+ \x04\x20\x01(\x0b2(.hw.trezor.messages.stellar.StellarAssetR\tassetHint\
+ \"\xd7\x02\n\x20StellarSorobanAuthorizedFunction\x12u\n\x04type\x18\x01\
+ \x20\x02(\x0e2a.hw.trezor.messages.stellar.StellarSorobanAuthorizedFunct\
+ ion.StellarSorobanAuthorizedFunctionTypeR\x04type\x12V\n\x0bcontract_fn\
+ \x18\x02\x20\x01(\x0b25.hw.trezor.messages.stellar.StellarInvokeContract\
+ ArgsR\ncontractFn\"d\n$StellarSorobanAuthorizedFunctionType\x120\n,SOROB\
+ AN_AUTHORIZED_FUNCTION_TYPE_CONTRACT_FN\x10\0\"\x04\x08\x01\x10\x01\"\
+ \x04\x08\x02\x10\x02\"\xe7\x01\n\"StellarSorobanAuthorizedInvocation\x12\
+ X\n\x08function\x18\x01\x20\x02(\x0b2<.hw.trezor.messages.stellar.Stella\
+ rSorobanAuthorizedFunctionR\x08function\x12g\n\x0fsub_invocations\x18\
+ \x02\x20\x03(\x0b2>.hw.trezor.messages.stellar.StellarSorobanAuthorizedI\
+ nvocationR\x0esubInvocations\"\xa7\x02\n\x13StellarHostFunction\x12[\n\
+ \x04type\x18\x01\x20\x02(\x0e2G.hw.trezor.messages.stellar.StellarHostFu\
+ nction.StellarHostFunctionTypeR\x04type\x12^\n\x0finvoke_contract\x18\
+ \x02\x20\x01(\x0b25.hw.trezor.messages.stellar.StellarInvokeContractArgs\
+ R\x0einvokeContract\"S\n\x17StellarHostFunctionType\x12&\n\"HOST_FUNCTIO\
+ N_TYPE_INVOKE_CONTRACT\x10\0\"\x04\x08\x01\x10\x01\"\x04\x08\x02\x10\x02\
+ \"\x04\x08\x03\x10\x03\"\xda\x01\n\x20StellarSorobanAddressCredentials\
+ \x12\x18\n\x07address\x18\x01\x20\x02(\tR\x07address\x12\x14\n\x05nonce\
+ \x18\x02\x20\x02(\x12R\x05nonce\x12>\n\x1bsignature_expiration_ledger\
+ \x18\x03\x20\x02(\rR\x19signatureExpirationLedger\x12F\n\tsignature\x18\
+ \x04\x20\x02(\x0b2(.hw.trezor.messages.stellar.StellarSCValR\tsignature\
+ \"\xd4\x02\n\x19StellarSorobanCredentials\x12g\n\x04type\x18\x01\x20\x02\
+ (\x0e2S.hw.trezor.messages.stellar.StellarSorobanCredentials.StellarSoro\
+ banCredentialsTypeR\x04type\x12[\n\naddress_v2\x18\x02\x20\x01(\x0b2<.hw\
+ .trezor.messages.stellar.StellarSorobanAddressCredentialsR\taddressV2\"q\
+ \n\x1dStellarSorobanCredentialsType\x12&\n\"SOROBAN_CREDENTIALS_SOURCE_A\
+ CCOUNT\x10\0\x12\"\n\x1eSOROBAN_CREDENTIALS_ADDRESS_V2\x10\x02\"\x04\x08\
+ \x01\x10\x01\"\xe4\x01\n\x20StellarSorobanAuthorizationEntry\x12W\n\x0bc\
+ redentials\x18\x01\x20\x02(\x0b25.hw.trezor.messages.stellar.StellarSoro\
+ banCredentialsR\x0bcredentials\x12g\n\x0froot_invocation\x18\x02\x20\x02\
+ (\x0b2>.hw.trezor.messages.stellar.StellarSorobanAuthorizedInvocationR\
+ \x0erootInvocation\"\xe3\x01\n\x1bStellarInvokeHostFunctionOp\x12%\n\x0e\
+ source_account\x18\x01\x20\x01(\tR\rsourceAccount\x12K\n\x08function\x18\
+ \x02\x20\x02(\x0b2/.hw.trezor.messages.stellar.StellarHostFunctionR\x08f\
+ unction\x12P\n\x04auth\x18\x03\x20\x03(\x0b2<.hw.trezor.messages.stellar\
+ .StellarSorobanAuthorizationEntryR\x04auth\"\x86\x06\n\x1fStellarSignSor\
+ obanAuthorization\x12\x1b\n\taddress_n\x18\x01\x20\x03(\rR\x08addressN\
+ \x12-\n\x12network_passphrase\x18\x02\x20\x02(\tR\x11networkPassphrase\
+ \x12\x88\x01\n\renvelope_type\x18\x03\x20\x02(\x0e2c.hw.trezor.messages.\
+ stellar.StellarSignSorobanAuthorization.StellarSorobanAuthorizationEnvel\
+ opeTypeR\x0cenvelopeType\x12\xaf\x01\n\"soroban_authorization_with_addre\
+ ss\x18\x04\x20\x01(\x0b2b.hw.trezor.messages.stellar.StellarSignSorobanA\
+ uthorization.StellarSorobanAuthorizationWithAddressR\x1fsorobanAuthoriza\
+ tionWithAddress\x1a\xf8\x01\n&StellarSorobanAuthorizationWithAddress\x12\
+ \x14\n\x05nonce\x18\x01\x20\x02(\x12R\x05nonce\x12>\n\x1bsignature_expir\
+ ation_ledger\x18\x02\x20\x02(\rR\x19signatureExpirationLedger\x12\x18\n\
+ \x07address\x18\x03\x20\x02(\tR\x07address\x12^\n\ninvocation\x18\x04\
+ \x20\x02(\x0b2>.hw.trezor.messages.stellar.StellarSorobanAuthorizedInvoc\
+ ationR\ninvocation\"_\n'StellarSorobanAuthorizationEnvelopeType\x124\n0E\
+ NVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS\x10\n\"c\n$StellarSoroba\
+ nAuthorizationSignature\x12\x1d\n\npublic_key\x18\x01\x20\x02(\x0cR\tpub\
+ licKey\x12\x1c\n\tsignature\x18\x02\x20\x02(\x0cR\tsignature\"\x15\n\x13\
+ StellarTxExtRequest\"?\n\x0cStellarTxExt\x12\x0c\n\x01v\x18\x01\x20\x02(\
+ \x11R\x01v\x12!\n\x0csoroban_data\x18\x02\x20\x01(\x0cR\x0bsorobanData*=\
+ \n\x10StellarAssetType\x12\n\n\x06NATIVE\x10\0\x12\r\n\tALPHANUM4\x10\
+ \x01\x12\x0e\n\nALPHANUM12\x10\x02B;\n#com.satoshilabs.trezor.lib.protob\
+ ufB\x14TrezorMessageStellar\
";
/// `FileDescriptorProto` object which was a source for this generated file
### tests/device_tests/stellar/test_stellar.py
@@ -50,6 +50,7 @@
# 9. Scroll down to the bottom and look at the "signatures" section. The Trezor should generate the same signature
#
+import json
from base64 import b64decode, b64encode
import pytest
@@ -59,7 +60,7 @@
from trezorlib.exceptions import TrezorFailure
from trezorlib.tools import parse_path
-from ...common import parametrize_using_common_fixtures
+from ...common import COMMON_FIXTURES_DIR, parametrize_using_common_fixtures
from ...input_flows import InputFlowShowAddressQRCode
from ..payment_req import (
CoinPurchaseMemo,
@@ -71,6 +72,16 @@
pytestmark = [pytest.mark.altcoin, pytest.mark.stellar]
+def parse_asset_hints(parameters):
+ """The fixture's asset hints as SDK objects (SEP-11: `native` or CODE:ISSUER)."""
+ from stellar_sdk import Asset
+
+ return [
+ Asset.native() if hint == "native" else Asset(*hint.split(":"))
+ for hint in parameters.get("asset_hints", ())
+ ]
+
+
def parameters_to_proto(session, parameters):
tx_data = parameters["tx"]
ops_data = parameters["operations"]
@@ -147,7 +158,9 @@ def test_sign_tx(session: Session, parameters, result):
envelope = TransactionEnvelope.from_xdr(
parameters["xdr"], parameters["network_passphrase"]
)
- tx_parsed, operations_parsed, ext_parsed = stellar.from_envelope(envelope)
+ tx_parsed, operations_parsed, ext_parsed = stellar.from_envelope(
+ envelope, asset_hints=parse_asset_hints(parameters)
+ )
tx_parsed.address_n = parse_path(parameters["address_n"])
# payment requests are not encoded in XDR
tx_parsed.payment_req = tx.payment_req
@@ -187,6 +200,37 @@ def test_sign_tx(session: Session, parameters, result):
assert False, "Invalid expected result"
+@pytest.mark.models("core")
+def test_sign_tx_asset_hint_mismatch(session: Session):
+ """A hint that does not derive to the invoked contract is ignored.
+
+ trezorlib never produces such a hint (it only attaches self-derived
+ matches), so forge one directly in the protobuf of a valid fixture.
+ """
+ fixtures = json.loads(
+ (COMMON_FIXTURES_DIR / "stellar" / "sign_tx.json").read_text()
+ )
+ parameters = next(
+ t
+ for t in fixtures["tests"]
+ if t["name"] == "StellarInvokeHostFunction-sac-transfer"
+ )["parameters"]
+ tx, operations, ext = parameters_to_proto(session, parameters)
+
+ hint = operations[0].function.invoke_contract.asset_hint
+ assert hint is not None
+ hint.code = "USDX"
+
+ stellar.sign_tx(
+ session,
+ tx,
+ operations,
+ ext,
+ tx.address_n,
+ tx.network_passphrase,
+ )
+
+
@pytest.mark.models("core")
@parametrize_using_common_fixtures("stellar/sign_soroban_authorization.json")
def test_sign_soroban_authorization(session: Session, parameters, result):
@@ -201,7 +245,14 @@ def test_sign_soroban_authorization(session: Session, parameters, result):
from stellar_sdk import xdr as stellar_xdr
entry = stellar_xdr.SorobanAuthorizationEntry.from_xdr(parameters["xdr"])
- assert stellar.from_authorization_entry(entry) == authorization
+ assert (
+ stellar.from_authorization_entry(
+ entry,
+ parameters["network_passphrase"],
+ asset_hints=parse_asset_hints(parameters),
+ )
+ == authorization
+ )
if "signature" in result:
assert entry.credentials.address_v2 is not NoneWhy this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.