Summary
The transaction history send/received classification misclassifies payments received to the user's muxed (M...) address as sent. This affects classic payments, Soroban token transfers, and Soroban mints. All sites compare an address against the user's base G... publicKey with strict string equality, with no muxed-aware normalization.
This is the same class of bug that freighter-mobile fixed for classic payments in stellar/freighter-mobile#893 (and tracked for its Soroban mapper in stellar/freighter-mobile#902). The extension has it in more places and has no equivalent isSameAccount helper.
Root cause
addressToString decodes a Soroban scAddressTypeMuxedAccount ScAddress to an M... string:
|
export const addressToString = (address: xdr.ScAddress) => { |
|
if (address.switch().name === "scAddressTypeAccount") { |
|
return StrKey.encodeEd25519PublicKey(address.accountId().ed25519()); |
|
} |
|
|
|
return Address.fromScAddress(address).toString(); |
|
}; |
The installed @stellar/stellar-sdk@15.0.1 / @stellar/stellar-base@15.0.0 fully support scAddressTypeMuxedAccount (Protocol 23), so the M... form is reachable. For classic payments, Horizon also supplies to_muxed (an M... string). The user's publicKey is always base G..., so "M..." === "G..." is false and the row is classified as Sent / Minted instead of Received.
Affected sites
All in extension/src/popup/views/AccountHistory/hooks/useGetHistoryData.tsx:
1. Classic payment — picks the muxed form but still compares with ===:
|
const destination = to_muxed || to || ""; |
|
const sender = from || ""; |
|
|
|
// default to Sent if a payment to self |
|
const isReceiving = destination === publicKey && sender !== publicKey; |
2. Soroban token transfer — extractDestinationFromXDR only parses classic payment/createAccount ops, so for invokeHostFunction it falls back to attrs.to (the M... string):
|
const actualDestination = await extractDestinationFromXDR( |
|
txEnvelopeXdr, |
|
networkDetails, |
|
attrs.to || "", |
|
); |
|
|
|
const isReceiving = |
|
actualDestination === publicKey && attrs.from !== publicKey; |
3. Soroban mint — muxed recipient shows "Minted" instead of "Received":
|
const isReceiving = attrs.to === publicKey; |
4. asset_balance_changes (lower priority) — strict ===, but Horizon typically normalizes these to base G..., so likely not hit in practice:
|
const isCredit = change.to === publicKey; |
Reproduction
- Receive a payment (classic or Soroban token transfer) where the sender addresses you by your muxed (
M...) account.
- Open transaction history.
- Expected: row shows
+amount (Received). Actual: row shows -amount (Sent), and Soroban mints show "Minted" rather than "Received".
Suggested fix
Add a muxed-aware comparison helper (resolve M... to its base G... via MuxedAccount.fromAddress(...).accountId() before comparing) and use it at all of sites 1-3 (and 4 if Horizon can return muxed there). This mirrors the isSameAccount helper added in stellar/freighter-mobile#893.
Summary
The transaction history send/received classification misclassifies payments received to the user's muxed (
M...) address as sent. This affects classic payments, Soroban token transfers, and Soroban mints. All sites compare an address against the user's baseG...publicKeywith strict string equality, with no muxed-aware normalization.This is the same class of bug that freighter-mobile fixed for classic payments in stellar/freighter-mobile#893 (and tracked for its Soroban mapper in stellar/freighter-mobile#902). The extension has it in more places and has no equivalent
isSameAccounthelper.Root cause
addressToStringdecodes a SorobanscAddressTypeMuxedAccountScAddressto anM...string:freighter/extension/src/popup/helpers/soroban.ts
Lines 355 to 361 in 21b6e38
The installed
@stellar/stellar-sdk@15.0.1/@stellar/stellar-base@15.0.0fully supportscAddressTypeMuxedAccount(Protocol 23), so theM...form is reachable. For classic payments, Horizon also suppliesto_muxed(anM...string). The user'spublicKeyis always baseG..., so"M..." === "G..."isfalseand the row is classified as Sent / Minted instead of Received.Affected sites
All in
extension/src/popup/views/AccountHistory/hooks/useGetHistoryData.tsx:1. Classic payment — picks the muxed form but still compares with
===:freighter/extension/src/popup/views/AccountHistory/hooks/useGetHistoryData.tsx
Lines 682 to 686 in 21b6e38
2. Soroban token transfer —
extractDestinationFromXDRonly parses classicpayment/createAccountops, so forinvokeHostFunctionit falls back toattrs.to(theM...string):freighter/extension/src/popup/views/AccountHistory/hooks/useGetHistoryData.tsx
Lines 826 to 833 in 21b6e38
3. Soroban mint — muxed recipient shows "Minted" instead of "Received":
freighter/extension/src/popup/views/AccountHistory/hooks/useGetHistoryData.tsx
Line 785 in 21b6e38
4.
asset_balance_changes(lower priority) — strict===, but Horizon typically normalizes these to baseG..., so likely not hit in practice:freighter/extension/src/popup/views/AccountHistory/hooks/useGetHistoryData.tsx
Line 457 in 21b6e38
Reproduction
M...) account.+amount(Received). Actual: row shows-amount(Sent), and Soroban mints show "Minted" rather than "Received".Suggested fix
Add a muxed-aware comparison helper (resolve
M...to its baseG...viaMuxedAccount.fromAddress(...).accountId()before comparing) and use it at all of sites 1-3 (and 4 if Horizon can return muxed there). This mirrors theisSameAccounthelper added in stellar/freighter-mobile#893.