Skip to content

Transaction history misclassifies payments/transfers received to a muxed (M...) address as sent #2841

Description

@piyalbasu

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 transferextractDestinationFromXDR 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

  1. Receive a payment (classic or Soroban token transfer) where the sender addresses you by your muxed (M...) account.
  2. Open transaction history.
  3. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions