Skip to content

Commit 7bbbfd8

Browse files
authored
[FIX 1.9.24] Disable memo for all M addresses (#602)
* disable memo for all M addresses (classic + soroban transactions) * handle disable memo for pre-cap67 contract addresses (e.g. MP collectibles) * update tests for muxed address scenarios * update tests for muxed address scenarios
1 parent 249f2f5 commit 7bbbfd8

8 files changed

Lines changed: 287 additions & 156 deletions

File tree

__tests__/components/TransactionSettingsBottomSheet.test.tsx

Lines changed: 242 additions & 85 deletions
Large diffs are not rendered by default.

__tests__/helpers/muxedAddress.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe("muxedAddress helpers", () => {
5454
});
5555

5656
describe("getMemoDisabledState", () => {
57-
it("should disable memo for Soroban M addresses (M address with contractId)", async () => {
57+
it("should disable memo for all M addresses (M address with contractId)", async () => {
5858
mockIsMuxedAccount.mockReturnValue(true);
5959

6060
const result = await getMemoDisabledState({
@@ -70,16 +70,18 @@ describe("muxedAddress helpers", () => {
7070
);
7171
});
7272

73-
it("should allow memo for classic M addresses (M address without contractId)", async () => {
73+
it("should disable memo for all M addresses (M address without contractId)", async () => {
7474
mockIsMuxedAccount.mockReturnValue(true);
7575

7676
const result = await getMemoDisabledState({
7777
targetAddress: "M...",
7878
t: mockT,
7979
});
8080

81-
expect(result.isMemoDisabled).toBe(false);
82-
expect(result.memoDisabledMessage).toBeUndefined();
81+
expect(result.isMemoDisabled).toBe(true);
82+
expect(result.memoDisabledMessage).toBe(
83+
"translated:transactionSettings.memoInfo.memoDisabledForTransaction",
84+
);
8385
});
8486

8587
it("should allow memo for classic transactions (no contract)", async () => {
@@ -111,7 +113,7 @@ describe("muxedAddress helpers", () => {
111113
);
112114
});
113115

114-
it("should allow memo when contract does not support muxed but target is G address", async () => {
116+
it("should disable memo when contract does not support muxed and target is G address", async () => {
115117
mockIsMuxedAccount.mockReturnValue(false);
116118
mockIsContractId.mockReturnValue(false);
117119
mockIsValidStellarAddress.mockReturnValue(true);
@@ -124,8 +126,10 @@ describe("muxedAddress helpers", () => {
124126
t: mockT,
125127
});
126128

127-
expect(result.isMemoDisabled).toBe(false);
128-
expect(result.memoDisabledMessage).toBeUndefined();
129+
expect(result.isMemoDisabled).toBe(true);
130+
expect(result.memoDisabledMessage).toBe(
131+
"translated:transactionSettings.memoInfo.memoNotSupportedForOperation",
132+
);
129133
});
130134

131135
it("should disable memo when contract does not support muxed and target is M address", async () => {
@@ -181,7 +185,7 @@ describe("muxedAddress helpers", () => {
181185
);
182186
});
183187

184-
it("should allow memo on error checking contract when target is G address", async () => {
188+
it("should disable memo on error checking contract when target is G address", async () => {
185189
mockIsMuxedAccount.mockReturnValue(false);
186190
mockIsContractId.mockReturnValue(false);
187191
mockIsValidStellarAddress.mockReturnValue(true);
@@ -196,8 +200,10 @@ describe("muxedAddress helpers", () => {
196200
t: mockT,
197201
});
198202

199-
expect(result.isMemoDisabled).toBe(false);
200-
expect(result.memoDisabledMessage).toBeUndefined();
203+
expect(result.isMemoDisabled).toBe(true);
204+
expect(result.memoDisabledMessage).toBe(
205+
"translated:transactionSettings.memoInfo.memoNotSupportedForOperation",
206+
);
201207
});
202208

203209
it("should disable memo on error checking contract when target is M address", async () => {

src/components/TransactionDetailsBottomSheet.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
formatFiatAmount,
2323
stroopToXlm,
2424
} from "helpers/formatAmount";
25-
import { isSorobanTransaction as checkIsSorobanTransaction } from "helpers/soroban";
2625
import { truncateAddress, isMuxedAccount } from "helpers/stellar";
2726
import { getStellarExpertUrl } from "helpers/stellarExpert";
2827
import useAppTranslation from "hooks/useAppTranslation";
@@ -111,18 +110,10 @@ const TransactionDetailsBottomSheet: React.FC<
111110
recipientAddress && isMuxedAccount(recipientAddress),
112111
);
113112

114-
// Check if this is a Soroban transaction (custom token or contract address)
115-
const isSorobanTransaction = checkIsSorobanTransaction(
116-
selectedBalance,
117-
recipientAddress,
118-
);
119-
120-
// Only hide memo for Soroban M addresses (M addresses in Soroban transactions)
121-
// Normal transactions support M address + memo
122-
// Custom tokens to G addresses support memo
113+
// Hide memo for M addresses (memo is encoded in the address)
123114
let memo = "";
124-
// Always extract memo from transaction or use stored memo, unless it's a Soroban M address
125-
if (!(isRecipientMuxed && isSorobanTransaction)) {
115+
// Only extract memo from transaction or use stored memo if recipient is not an M address
116+
if (!isRecipientMuxed) {
126117
if ("memo" in transaction && transaction.memo.value) {
127118
memo = String(transaction.memo.value);
128119
} else if (transactionMemo) {
@@ -319,9 +310,8 @@ const TransactionDetailsBottomSheet: React.FC<
319310
</Text>
320311
),
321312
},
322-
// Hide memo line only for Soroban M addresses (M addresses in Soroban transactions)
323-
// Normal transactions support M address + memo
324-
!(isRecipientMuxed && isSorobanTransaction)
313+
// Hide memo line for M addresses (memo is encoded in the address)
314+
!isRecipientMuxed
325315
? {
326316
icon: (
327317
<Icon.File02

src/components/screens/HistoryScreen/TransactionDetailsBottomSheetCustomContent.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,6 @@ export const TransactionDetailsBottomSheetCustomContent: React.FC<
8585
? isMuxedAccount(destinationAddress)
8686
: false;
8787

88-
// Check if this is a Soroban transaction (custom token or collectible transfer)
89-
// Soroban transactions to M addresses don't support separate memo (memo is encoded in address)
90-
// Soroban transactions to G addresses support memo
91-
// Normal transactions (payments) support M address + memo
92-
const isSorobanTransaction =
93-
transactionDetails.transactionType === TransactionType.CONTRACT_TRANSFER ||
94-
transactionDetails.transactionType === TransactionType.CONTRACT;
95-
9688
const detailItems = useMemo(
9789
() =>
9890
[
@@ -119,10 +111,8 @@ export const TransactionDetailsBottomSheetCustomContent: React.FC<
119111
</Text>
120112
),
121113
},
122-
// Hide memo line only for Soroban transactions (custom tokens or collectibles) to M addresses
123-
// Soroban transactions to G addresses support memo
124-
// Normal transactions support M address + memo
125-
!(isDestinationMuxed && isSorobanTransaction)
114+
// Hide memo line for M addresses (memo is encoded in the address)
115+
!isDestinationMuxed
126116
? {
127117
icon: (
128118
<Icon.File02 size={16} color={themeColors.foreground.primary} />
@@ -199,7 +189,6 @@ export const TransactionDetailsBottomSheetCustomContent: React.FC<
199189
transactionDetails.memo,
200190
transactionDetails.xdr,
201191
isDestinationMuxed,
202-
isSorobanTransaction,
203192
],
204193
) as ListItemProps[];
205194

src/components/screens/SendScreen/components/SendReviewBottomSheet.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { useTransactionSettingsStore } from "ducks/transactionSettings";
1919
import { isLiquidityPool } from "helpers/balances";
2020
import { pxValue } from "helpers/dimensions";
2121
import { formatTokenForDisplay, formatFiatAmount } from "helpers/formatAmount";
22-
import { isSorobanTransaction as checkIsSorobanTransaction } from "helpers/soroban";
2322
import { truncateAddress, isMuxedAccount } from "helpers/stellar";
2423
import useAppTranslation from "hooks/useAppTranslation";
2524
import { useClipboard } from "hooks/useClipboard";
@@ -195,12 +194,6 @@ const SendReviewBottomSheet: React.FC<SendReviewBottomSheetProps> = ({
195194

196195
const isRecipientMuxed = isMuxedAccount(recipientAddress);
197196

198-
// Check if this is a Soroban transaction (custom token or contract address)
199-
const isSorobanTransaction = checkIsSorobanTransaction(
200-
selectedBalance,
201-
recipientAddress,
202-
);
203-
204197
const transactionDetailsList: ListItemProps[] = useMemo(
205198
() =>
206199
[
@@ -223,9 +216,8 @@ const SendReviewBottomSheet: React.FC<SendReviewBottomSheetProps> = ({
223216
</View>
224217
),
225218
},
226-
// Hide memo line only for Soroban M addresses (M addresses in Soroban transactions)
227-
// Normal transactions support M address + memo
228-
!(isRecipientMuxed && isSorobanTransaction)
219+
// Hide memo line for M addresses (memo is encoded in the address)
220+
!isRecipientMuxed
229221
? {
230222
icon: (
231223
<Icon.File02 size={16} color={themeColors.foreground.primary} />
@@ -282,7 +274,6 @@ const SendReviewBottomSheet: React.FC<SendReviewBottomSheetProps> = ({
282274
transactionMemo,
283275
transactionXDR,
284276
isRecipientMuxed,
285-
isSorobanTransaction,
286277
],
287278
);
288279

src/components/screens/SendScreen/screens/TransactionAmountScreen.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,12 @@ const TransactionAmountScreen: React.FC<TransactionAmountScreenProps> = ({
284284
recipientAddress && isMuxedAccount(recipientAddress),
285285
);
286286

287-
// Clear memo only for Soroban M addresses (custom tokens / contract addresses)
288-
// Normal transactions support M address + memo
287+
// Clear memo for all M addresses (memo is encoded in the address)
289288
useEffect(() => {
290-
if (isRecipientMuxed && isCustomToken && transactionMemo) {
289+
if (isRecipientMuxed && transactionMemo) {
291290
saveMemo("");
292291
}
293-
}, [isRecipientMuxed, isCustomToken, transactionMemo, saveMemo]);
292+
}, [isRecipientMuxed, transactionMemo, saveMemo]);
294293

295294
const contractId = useMemo(() => {
296295
if (

src/helpers/muxedAddress.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ export async function getMemoDisabledState(
2929
): Promise<MemoDisabledState> {
3030
const { targetAddress, contractId, networkDetails, t } = params;
3131

32-
// Only disable memo for Soroban M addresses (when there's a contractId AND target is M address)
33-
// Normal transactions support M address + memo
34-
// Custom tokens to G addresses support memo
35-
if (isMuxedAccount(targetAddress) && contractId) {
32+
// Disable memo for all M addresses (memo is encoded in the address)
33+
if (isMuxedAccount(targetAddress)) {
3634
return {
3735
isMemoDisabled: true,
3836
memoDisabledMessage: t(
@@ -63,17 +61,15 @@ export async function getMemoDisabledState(
6361
};
6462
}
6563

66-
// For Soroban transactions (custom tokens), memo is supported for G addresses
67-
// Only disable memo if contract doesn't support muxed AND target is M address
64+
// For Soroban transactions (custom tokens/collectibles) with G addresses:
65+
// Memo is only supported if the contract supports muxed (has to_muxed property)
6866
try {
6967
const contractSupportsMuxed = await checkContractSupportsMuxed({
7068
contractId,
7169
networkDetails,
7270
});
7371

74-
// If contract doesn't support muxed and target is M address, disable memo
75-
// (because we'll need to convert M to G, and memo can't be encoded)
76-
if (!contractSupportsMuxed && isMuxedAccount(targetAddress)) {
72+
if (!contractSupportsMuxed) {
7773
return {
7874
isMemoDisabled: true,
7975
memoDisabledMessage: t(
@@ -82,15 +78,15 @@ export async function getMemoDisabledState(
8278
};
8379
}
8480

85-
// For G addresses in Soroban transactions, memo is always supported
86-
// For M addresses in Soroban transactions with muxed support, memo is disabled (encoded in address)
8781
return { isMemoDisabled: false, memoDisabledMessage: undefined };
8882
} catch (error) {
89-
// On error, only disable memo if target is M address (to be safe)
90-
if (isMuxedAccount(targetAddress)) {
91-
return { isMemoDisabled: true, memoDisabledMessage: undefined };
92-
}
93-
return { isMemoDisabled: false, memoDisabledMessage: undefined };
83+
// On error, disable memo for safety (assume contract doesn't support muxed)
84+
return {
85+
isMemoDisabled: true,
86+
memoDisabledMessage: t(
87+
"transactionSettings.memoInfo.memoNotSupportedForOperation",
88+
),
89+
};
9490
}
9591
}
9692

src/services/transactionService.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
isValidStellarAddress,
3232
isSameAccount,
3333
getBaseAccount,
34+
isMuxedAccount,
3435
} from "helpers/stellar";
3536
import { t } from "i18next";
3637
import { analytics } from "services/analytics";
@@ -375,9 +376,11 @@ export const buildPaymentTransaction = async (
375376

376377
const isToContractAddress = isContractId(recipientAddress);
377378
const shouldUseSorobanTransfer = isToContractAddress || isCustomToken;
378-
// For normal transactions (non-Soroban), allow memo even with M addresses
379+
const isRecipientMuxed = isMuxedAccount(recipientAddress);
380+
// Don't add memo for M addresses (memo is encoded in the address)
381+
// For normal transactions (non-Soroban), only add memo if recipient is not M address
379382
// For Soroban transactions, memo handling is done in buildSorobanTransferOperation
380-
if (memo && !shouldUseSorobanTransfer) {
383+
if (memo && !shouldUseSorobanTransfer && !isRecipientMuxed) {
381384
transactionBuilder.addMemo(new Memo(Memo.text(memo).type, memo));
382385
}
383386

0 commit comments

Comments
 (0)