Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions __tests__/components/screens/WalletKit/DappMessageDisplay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { DappMessageDisplay } from "components/screens/WalletKit/DappMessageDisplay";
import { renderWithProviders } from "helpers/testUtils";
import React from "react";
import { Dimensions, StyleSheet } from "react-native";

jest.mock("hooks/useAppTranslation", () => ({
__esModule: true,
default: () => ({ t: (key: string) => key }),
}));

describe("DappMessageDisplay", () => {
it("renders the message content", () => {
const { getByTestId } = renderWithProviders(
<DappMessageDisplay message="hello world" />,
);

expect(getByTestId("message-display-content")).toHaveTextContent(
"hello world",
);
});

it("pretty-prints JSON messages", () => {
const { getByTestId } = renderWithProviders(
<DappMessageDisplay message='{"a":1}' />,
);

expect(getByTestId("message-display-content").props.children).toBe(
JSON.stringify({ a: 1 }, null, 2),
);
});

it("bounds the message scroll area so long messages cannot push the action buttons off-screen", () => {
const { getByTestId } = renderWithProviders(
<DappMessageDisplay message={"x".repeat(10000)} />,
);

const scrollView = getByTestId("message-display-content-scroll");
const style = StyleSheet.flatten(scrollView.props.style);

expect(style.maxHeight).toBeDefined();
expect(style.maxHeight).toBeLessThanOrEqual(
Dimensions.get("window").height * 0.5,
);
});

it("can shrink below the cap (with a usable floor) when the sheet needs the room", () => {
// With a security banner + stacked warning buttons the sheet's fixed
// content grows; the message box must yield height so the action buttons
// stay on screen, while keeping a scrollable sliver of the message.
const { getByTestId } = renderWithProviders(
<DappMessageDisplay message={"x".repeat(10000)} />,
);

const boxStyle = StyleSheet.flatten(
getByTestId("message-display").props.style,
);
const scrollStyle = StyleSheet.flatten(
getByTestId("message-display-content-scroll").props.style,
);

expect(boxStyle.flexShrink).toBe(1);
expect(scrollStyle.flexShrink).toBe(1);
expect(scrollStyle.minHeight).toBeGreaterThan(0);
});
});
25 changes: 17 additions & 8 deletions __tests__/helpers/walletKitValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,22 @@ describe("validateSignMessageLength", () => {
}
});

it("returns valid for a message exactly at the 1 KB limit", () => {
it("allows a multi-KB JSON payload (limit is 10 KB, not 1 KB)", () => {
// Regression for stellar/freighter-mobile#957: dApps sign JSON payloads
// that routinely exceed 1 KB. SEP-53 imposes no size limit.
expect(SIGN_MESSAGE_MAX_BYTES).toBe(10240);
const message = JSON.stringify({ data: "a".repeat(4000) });
const result = validateSignMessageLength(message);
expect(result.valid).toBe(true);
});

it("returns valid for a message exactly at the limit", () => {
const message = "a".repeat(SIGN_MESSAGE_MAX_BYTES);
const result = validateSignMessageLength(message);
expect(result.valid).toBe(true);
});

it("returns error for a message exceeding 1 KB (ASCII)", () => {
it("returns error for a message exceeding the limit (ASCII)", () => {
const message = "a".repeat(SIGN_MESSAGE_MAX_BYTES + 1);
const result = validateSignMessageLength(message);
expect(result.valid).toBe(false);
Expand All @@ -163,19 +172,19 @@ describe("validateSignMessageLength", () => {
}
});

it("returns error for a message exceeding 1 KB due to multi-byte UTF-8 chars", () => {
// Each emoji is 4 bytes in UTF-8 — 257 emojis = 1028 bytes > 1024
const message = "🚀".repeat(257);
it("returns error for a message exceeding the limit due to multi-byte UTF-8 chars", () => {
// Each emoji is 4 bytes in UTF-8, so this is 4 bytes over the limit
const message = "🚀".repeat(SIGN_MESSAGE_MAX_BYTES / 4 + 1);
const result = validateSignMessageLength(message);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.errorKey).toBe(ValidationErrorKeys.MESSAGE_TOO_LONG);
}
});

it("returns valid for multi-byte chars that stay within 1 KB", () => {
// 256 emojis = 1024 bytes — exactly at the limit
const message = "🚀".repeat(256);
it("returns valid for multi-byte chars that stay within the limit", () => {
// Each emoji is 4 bytes in UTF-8 — exactly at the limit
const message = "🚀".repeat(SIGN_MESSAGE_MAX_BYTES / 4);
const result = validateSignMessageLength(message);
expect(result.valid).toBe(true);
});
Expand Down
26 changes: 22 additions & 4 deletions src/components/screens/WalletKit/DappMessageDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Text } from "components/sds/Typography";
import useColors from "hooks/useColors";
import React from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native";
import { Dimensions, ScrollView, View } from "react-native";

/**
* Props for the DappMessageDisplay component
Expand All @@ -28,7 +28,7 @@ const isJsonString = (str: string): boolean => {
/**
* DappMessageDisplay component for showing SEP-53 messages
* Displays the message with the SEP-53 prefix and handles JSON formatting
* Dynamically increases height based on message length
* Grows with the message up to a capped height, then scrolls
*
* @component
* @param {DappMessageDisplayProps} props - The component props
Expand All @@ -49,7 +49,13 @@ export const DappMessageDisplay: React.FC<DappMessageDisplayProps> = ({
return (
<View
className="rounded-2xl p-4 my-3"
style={{ backgroundColor: themeColors.background.secondary }}
// flexShrink lets this box give up height first when the sheet content
// is taller than the screen (e.g. security banner + stacked warning
// buttons), keeping the action buttons visible.
style={{
backgroundColor: themeColors.background.secondary,
flexShrink: 1,
}}
testID="message-display"
>
<View className="flex-row items-center gap-2 mb-4">
Expand All @@ -63,7 +69,19 @@ export const DappMessageDisplay: React.FC<DappMessageDisplayProps> = ({
{t("common.message")}
</Text>
</View>
<ScrollView testID="message-display-content-scroll">
<ScrollView
// Cap the message area so long messages scroll instead of growing the
// sheet past the screen and pushing the action buttons out of view.
// flexShrink + minHeight let it shrink further when the rest of the
// sheet needs the room, while always leaving a usable scroll window.
style={{
maxHeight: Dimensions.get("window").height * 0.3,
flexShrink: 1,
minHeight: 56,
}}
showsVerticalScrollIndicator={false}
testID="message-display-content-scroll"
>
<Text
sm
primary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ import { WalletKitSessionRequest } from "ducks/walletKit";
import useAppTranslation from "hooks/useAppTranslation";
import useColors from "hooks/useColors";
import React, { useMemo } from "react";
import { View } from "react-native";
import { useWindowDimensions, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

/**
* Vertical space the bottom sheet adds around this content (drag handle plus
* the BottomSheetView paddings — see components/BottomSheet). Used to bound
* the content to the usable window height so the message box can flex-shrink
* instead of pushing the action buttons off-screen.
*/
const SHEET_CHROME_HEIGHT = 80;

interface DappSignMessageBottomSheetContentProps {
requestEvent: WalletKitSessionRequest | null;
Expand Down Expand Up @@ -46,6 +55,10 @@ export const DappSignMessageBottomSheetContent: React.FC<
}) => {
const { themeColors } = useColors();
const { t } = useAppTranslation();
const { height: windowHeight } = useWindowDimensions();
const insets = useSafeAreaInsets();
const maxContentHeight =
windowHeight - insets.top - insets.bottom - SHEET_CHROME_HEIGHT;

const accountList = useMemo(
() => [
Expand Down Expand Up @@ -89,6 +102,7 @@ export const DappSignMessageBottomSheetContent: React.FC<
return (
<View
className="flex-1 justify-center mt-2 gap-[16px]"
style={{ maxHeight: maxContentHeight }}
testID="dapp-request-bottom-sheet"
>
<View className="flex-row items-center gap-[12px] w-full">
Expand All @@ -112,7 +126,9 @@ export const DappSignMessageBottomSheetContent: React.FC<
securityWarningAction={securityWarningAction}
/>

<View className="gap-[12px]">
{/* flexShrink lets the message box inside absorb the squeeze when the
sheet is taller than the screen; the account list keeps its size. */}
<View className="gap-[12px]" style={{ flexShrink: 1 }}>
<DappMessageDisplay message={message} />
<List variant="secondary" items={accountList} />
</View>
Expand Down
11 changes: 8 additions & 3 deletions src/helpers/walletKitValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ export const ValidationErrorKeys = {
AUTH_ENTRY_ADDRESS_MISMATCH: "walletKit.errorAuthEntryAddressMismatch",
} as const;

/** Max UTF-8 byte length for sign_message content per SEP-53. */
export const SIGN_MESSAGE_MAX_BYTES = 1024;
/**
* Max UTF-8 byte length for sign_message content. SEP-53 imposes no size
* limit (and the browser extension enforces none) — this is a sanity cap
* against absurd WalletConnect payloads, sized to comfortably fit the JSON
* payloads dApps actually sign (see stellar/freighter-mobile#957).
*/
export const SIGN_MESSAGE_MAX_BYTES = 10240;

// ─────────────────────────────────────────────────────────────────────────────
// Types
Expand Down Expand Up @@ -46,7 +51,7 @@ export function validateSignMessageContent(
}

/**
* Validates sign_message length: max 1KB UTF-8 bytes per SEP-53.
* Validates sign_message length against SIGN_MESSAGE_MAX_BYTES (UTF-8 bytes).
*/
export function validateSignMessageLength(
message: string,
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@
"errorAuthEntryNetworkMismatch": "Authorization entry is for a different network",
"errorAuthEntryAddressMismatch": "Authorization entry is bound to a different account",
"errorEmptyMessage": "Cannot sign empty message",
"errorMessageTooLong": "Message too long (max 1KB)",
"errorMessageTooLong": "Message too long (max 10KB)",
"errorSubmitting": "Failed to submit transaction",
"errorRespondingRequest": "Failed to respond transaction request",
"errorWrongNetwork": "Wrong Network",
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/locales/pt/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@
"errorAuthEntryNetworkMismatch": "A entrada de autorização é para uma rede diferente",
"errorAuthEntryAddressMismatch": "A entrada de autorização está vinculada a uma conta diferente",
"errorEmptyMessage": "Não é possível assinar mensagem vazia",
"errorMessageTooLong": "Mensagem muito longa (máximo 1KB)",
"errorMessageTooLong": "Mensagem muito longa (máximo 10KB)",
"errorSubmitting": "Erro ao enviar a transação",
"errorRespondingRequest": "Erro ao responder à solicitação de transação",
"errorWrongNetwork": "Rede incorreta",
Expand Down
2 changes: 1 addition & 1 deletion src/providers/WalletKitProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ export const WalletKitProvider: React.FC<WalletKitProviderProps> = ({
return false;
}

// Step 2: Validate message length (1KB limit per SEP-53)
// Step 2: Validate message length (sanity cap; SEP-53 imposes no limit)
const lengthResult = validateSignMessageLength(contentResult.value);
if (!lengthResult.valid) {
showToast({
Expand Down
Loading