87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 0 0% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 3.9%;
15 |
16 | --primary: 0 0% 9%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 |
22 | --muted: 0 0% 96.1%;
23 | --muted-foreground: 0 0% 45.1%;
24 |
25 | --accent: 0 0% 96.1%;
26 | --accent-foreground: 0 0% 9%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 0 0% 89.8%;
32 | --input: 0 0% 89.8%;
33 | --ring: 0 0% 3.9%;
34 |
35 | --radius: 0.5rem;
36 |
37 | --chart-1: 12 76% 61%;
38 |
39 | --chart-2: 173 58% 39%;
40 |
41 | --chart-3: 197 37% 24%;
42 |
43 | --chart-4: 43 74% 66%;
44 |
45 | --chart-5: 27 87% 67%;
46 |
47 | --sidebar-background: 0 0% 98%;
48 |
49 | --sidebar-foreground: 240 5.3% 26.1%;
50 |
51 | --sidebar-primary: 240 5.9% 10%;
52 |
53 | --sidebar-primary-foreground: 0 0% 98%;
54 |
55 | --sidebar-accent: 240 4.8% 95.9%;
56 |
57 | --sidebar-accent-foreground: 240 5.9% 10%;
58 |
59 | --sidebar-border: 220 13% 91%;
60 |
61 | --sidebar-ring: 217.2 91.2% 59.8%;
62 | }
63 |
64 | .dark {
65 | --background: 0 0% 3.9%;
66 | --foreground: 0 0% 98%;
67 |
68 | --card: 0 0% 3.9%;
69 | --card-foreground: 0 0% 98%;
70 |
71 | --popover: 0 0% 3.9%;
72 | --popover-foreground: 0 0% 98%;
73 |
74 | --primary: 0 0% 98%;
75 | --primary-foreground: 0 0% 9%;
76 |
77 | --secondary: 0 0% 14.9%;
78 | --secondary-foreground: 0 0% 98%;
79 |
80 | --muted: 0 0% 14.9%;
81 | --muted-foreground: 0 0% 63.9%;
82 |
83 | --accent: 0 0% 14.9%;
84 | --accent-foreground: 0 0% 98%;
85 |
86 | --destructive: 0 62.8% 30.6%;
87 | --destructive-foreground: 0 0% 98%;
88 |
89 | --border: 0 0% 14.9%;
90 | --input: 0 0% 14.9%;
91 | --ring: 0 0% 83.1%;
92 | --chart-1: 220 70% 50%;
93 | --chart-2: 160 60% 45%;
94 | --chart-3: 30 80% 55%;
95 | --chart-4: 280 65% 60%;
96 | --chart-5: 340 75% 55%;
97 | --sidebar-background: 240 5.9% 10%;
98 | --sidebar-foreground: 240 4.8% 95.9%;
99 | --sidebar-primary: 224.3 76.3% 48%;
100 | --sidebar-primary-foreground: 0 0% 100%;
101 | --sidebar-accent: 240 3.7% 15.9%;
102 | --sidebar-accent-foreground: 240 4.8% 95.9%;
103 | --sidebar-border: 240 3.7% 15.9%;
104 | --sidebar-ring: 217.2 91.2% 59.8%;
105 | }
106 | }
107 |
108 | @layer base {
109 | * {
110 | @apply border-border;
111 | }
112 | body {
113 | @apply bg-background text-foreground;
114 | }
115 | }
116 |
117 | :root {
118 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
119 | line-height: 1.5;
120 | font-weight: 400;
121 |
122 | color-scheme: light dark;
123 | color: rgba(255, 255, 255, 0.87);
124 | background-color: #242424;
125 |
126 | font-synthesis: none;
127 | text-rendering: optimizeLegibility;
128 | -webkit-font-smoothing: antialiased;
129 | -moz-osx-font-smoothing: grayscale;
130 | }
131 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/MockTestCase.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { Button } from "@/components/ui/button";
3 | import { AlertCircle } from "lucide-react";
4 | import { ReactNode } from "react";
5 |
6 | /**
7 | * Action type identifiers to prevent typos and improve code maintainability
8 | */
9 | const ACTION_TYPE = {
10 | PROMPT: 'prompt',
11 | CHECK: 'check'
12 | } as const;
13 |
14 | /**
15 | * Mock test case component used for simple prompt/check test cases
16 | * that don't require complex implementation
17 | */
18 | export function MockTestCase(props: BaseTestCaseProps) {
19 | /**
20 | * Renders Yes/No prompt buttons for user confirmation
21 | */
22 | const renderPromptActions = (): ReactNode => (
23 |
24 | props.onStatusChange?.('success')}
28 | className="bg-green-500/10 text-green-500 hover:bg-green-500/20 hover:text-green-600"
29 | >
30 | Yes
31 |
32 | props.onStatusChange?.('failure')}
36 | className="bg-red-500/10 text-red-500 hover:bg-red-500/20 hover:text-red-600"
37 | >
38 | No
39 |
40 |
41 | );
42 |
43 | /**
44 | * Renders retry button and failure message for failed tests
45 | */
46 | const renderRetryAction = (): ReactNode => (
47 |
48 |
49 |
50 |
This test is not yet implemented
51 |
52 |
props.onStatusChange?.('running')}
56 | className="bg-blue-500/10 text-blue-500 hover:bg-blue-500/20 hover:text-blue-600"
57 | >
58 | Retry
59 |
60 |
61 | );
62 |
63 | /**
64 | * Renders start button for check-type tests
65 | */
66 | const renderStartCheckAction = (): ReactNode => (
67 |
68 | props.onStatusChange?.('running')}
72 | className="bg-blue-500/10 text-blue-500 hover:bg-blue-500/20 hover:text-blue-600"
73 | >
74 | Start Check
75 |
76 |
77 | );
78 |
79 | /**
80 | * Determines which action UI to render based on test case state
81 | */
82 | const renderActions = (): ReactNode => {
83 | const { testCase } = props;
84 |
85 | // Prompt-type test in running state - show Yes/No buttons
86 | if (testCase.type === ACTION_TYPE.PROMPT && testCase.status === 'running') {
87 | return renderPromptActions();
88 | }
89 |
90 | // Failed test that can be retried - show retry button
91 | if (testCase.status === 'failure' && testCase.retryable) {
92 | return renderRetryAction();
93 | }
94 |
95 | // Check-type test in pending state - show start button
96 | if (testCase.type === ACTION_TYPE.CHECK && testCase.status === 'pending') {
97 | return renderStartCheckAction();
98 | }
99 |
100 | return null;
101 | };
102 |
103 | return (
104 |
105 | {renderActions()}
106 |
107 | );
108 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/hooks/use-ton-connect.ts:
--------------------------------------------------------------------------------
1 | import {
2 | toUserFriendlyAddress,
3 | useTonAddress,
4 | useTonConnectUI,
5 | CHAIN,
6 | } from "@tonconnect/ui-react";
7 | import { useCallback, useMemo } from "react";
8 |
9 | /**
10 | * Interface for describing supported wallet functions
11 | */
12 | export interface WalletFeatures {
13 | /** Wallet connection status */
14 | isConnected: boolean;
15 | /** User address in user-friendly format */
16 | userAddress: string;
17 | /** Extra Currency support in wallet */
18 | hasExtraCurrencySupport: boolean;
19 | }
20 |
21 | /**
22 | * Interface for transaction sending function with Extra Currency support
23 | */
24 | interface SendTransactionFeature {
25 | /** Function name */
26 | name: "SendTransaction";
27 | /** Maximum number of messages */
28 | maxMessages: number;
29 | /** Extra Currency support flag */
30 | extraCurrencySupported?: boolean;
31 | }
32 |
33 | /**
34 | * Hook for working with TON Connect
35 | * Provides functions for wallet connection and checking Extra Currency support
36 | *
37 | * @returns Object with functions and properties for interacting with the wallet
38 | */
39 | export function useTonConnect() {
40 | const [tonConnectUI] = useTonConnectUI();
41 | const userAddress = useTonAddress();
42 |
43 | /**
44 | * Checks capabilities of the connected wallet
45 | * @returns Object with information about supported wallet functions
46 | */
47 | const checkWalletFeatures = useCallback((): WalletFeatures => {
48 | const wallet = tonConnectUI.wallet;
49 |
50 | if (!wallet) {
51 | return {
52 | isConnected: false,
53 | userAddress: "",
54 | hasExtraCurrencySupport: false,
55 | };
56 | }
57 |
58 | const userFriendlyAddress = toUserFriendlyAddress(
59 | wallet.account.address,
60 | wallet.account.chain === CHAIN.TESTNET
61 | );
62 |
63 | // Check for SendTransaction function with extraCurrencySupported support
64 | const sendTransactionFeature = wallet.device.features.find(
65 | (feature): feature is SendTransactionFeature =>
66 | typeof feature === "object" &&
67 | feature !== null &&
68 | "name" in feature &&
69 | feature.name === "SendTransaction"
70 | );
71 |
72 | return {
73 | isConnected: true,
74 | userAddress: userFriendlyAddress,
75 | hasExtraCurrencySupport: Boolean(
76 | sendTransactionFeature?.extraCurrencySupported
77 | ),
78 | };
79 | }, [tonConnectUI.wallet]);
80 |
81 | /**
82 | * Opens wallet connection modal window
83 | */
84 | const connect = useCallback(() => {
85 | tonConnectUI.openModal();
86 | }, [tonConnectUI]);
87 |
88 | /**
89 | * Disconnects the wallet
90 | */
91 | const disconnect = useCallback(() => {
92 | tonConnectUI.disconnect();
93 | }, [tonConnectUI]);
94 |
95 | /**
96 | * Wallet object with memoization
97 | */
98 | const wallet = useMemo(() => {
99 | return tonConnectUI.wallet;
100 | }, [tonConnectUI.wallet?.account]);
101 |
102 | /**
103 | * Flag indicating successful wallet connection
104 | */
105 | const isConnected = useMemo(() => {
106 | return !!(wallet && userAddress);
107 | }, [wallet, userAddress]);
108 |
109 | /**
110 | * Flag for Extra Currency support in wallet
111 | */
112 | const hasExtraCurrencySupport = useMemo(() => {
113 | if (!wallet) return false;
114 |
115 | return wallet?.device.features.some(
116 | (feature): feature is SendTransactionFeature =>
117 | typeof feature === "object" &&
118 | feature !== null &&
119 | "name" in feature &&
120 | feature.name === "SendTransaction" &&
121 | "extraCurrencySupported" in feature &&
122 | typeof feature.extraCurrencySupported === "boolean"
123 | );
124 | }, [wallet]);
125 |
126 | return {
127 | connect,
128 | disconnect,
129 | wallet,
130 | userAddress,
131 | isConnected,
132 | hasExtraCurrencySupport,
133 | checkWalletFeatures,
134 | };
135 | }
136 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/TestCaseFactory.tsx:
--------------------------------------------------------------------------------
1 | import { TestCase, TestStatus } from "@/types/test-case";
2 | import { ConnectWalletTest } from "./ConnectWalletTest";
3 | import { ExtraCurrencySupportTest } from "./ExtraCurrencySupportTest";
4 | import { EmulationSupportTest } from "./EmulationSupportTest";
5 | import { TonBalanceTest } from "./TonBalanceTest";
6 | import { GetTestEcTest } from "./GetTestEcTest";
7 | import { EmulationDisplayTest } from "./EmulationDisplayTest";
8 | import { EcBalanceCheckTest } from "./EcBalanceCheckTest";
9 | import { EcBalanceVerificationTest } from "./EcBalanceVerificationTest";
10 | import { SendEcViaTonConnectTest } from "./SendEcViaTonConnectTest";
11 | import { SendEcViaNativeTest } from "./SendEcViaNativeTest";
12 | import { EmulationDisplaySendTest } from "./EmulationDisplaySendTest";
13 | import { EmulationDisplayNativeSendTest } from "./EmulationDisplayNativeSendTest";
14 | import { MockTestCase } from "./MockTestCase";
15 | import { useCallback } from "react";
16 |
17 | /**
18 | * Test case identifiers used to map to their respective component implementations
19 | */
20 | const TEST_ID = {
21 | CONNECT_WALLET: 'connect-wallet',
22 | EC_SUPPORT: 'ec-support',
23 | EMULATION_SUPPORT: 'emulation-support',
24 | TON_BALANCE: 'ton-balance',
25 | GET_TEST_EC: 'get-test-ec',
26 | EMULATION_DISPLAY: 'emulation-display',
27 | EC_BALANCE_CHECK: 'ec-balance-check',
28 | EC_BALANCE_VERIFICATION: 'ec-balance-verification',
29 | SEND_EC_TON_CONNECT: 'send-ec-ton-connect',
30 | SEND_EC_NATIVE: 'send-ec-native',
31 | EMULATION_DISPLAY_SEND: 'emulation-display-send',
32 | EMULATION_DISPLAY_NATIVE_SEND: 'emulation-display-native-send'
33 | } as const;
34 |
35 | /**
36 | * Type definition for the test case ID values
37 | */
38 | type TestCaseId = typeof TEST_ID[keyof typeof TEST_ID];
39 |
40 | /**
41 | * Props for the TestCaseFactory component
42 | */
43 | interface TestCaseFactoryProps {
44 | /** The test case configuration */
45 | testCase: TestCase;
46 | /** Unique identifier for the test */
47 | testId: string;
48 | /** Whether the test is locked/disabled */
49 | isLocked?: boolean;
50 | /** Callback for when test status changes */
51 | onStatusChange?: (testId: string, status: TestStatus) => void;
52 | }
53 |
54 | /**
55 | * Props passed to individual test components
56 | */
57 | interface TestCaseProps {
58 | /** The test case configuration */
59 | testCase: TestCase;
60 | /** Whether the test is locked/disabled */
61 | isLocked?: boolean;
62 | /** Callback for when test status changes */
63 | onStatusChange?: (status: TestStatus) => void;
64 | }
65 |
66 | /**
67 | * Factory component that renders the appropriate test component based on test ID
68 | *
69 | * This component serves as a router for different test implementations
70 | * and provides a consistent way to handle status changes across all tests.
71 | */
72 | export function TestCaseFactory({ testCase, onStatusChange, testId, ...props }: TestCaseFactoryProps) {
73 | // Map test case IDs to their respective components
74 | const testComponents: Record> = {
75 | [TEST_ID.CONNECT_WALLET]: ConnectWalletTest,
76 | [TEST_ID.EC_SUPPORT]: ExtraCurrencySupportTest,
77 | [TEST_ID.EMULATION_SUPPORT]: EmulationSupportTest,
78 | [TEST_ID.TON_BALANCE]: TonBalanceTest,
79 | [TEST_ID.GET_TEST_EC]: GetTestEcTest,
80 | [TEST_ID.EMULATION_DISPLAY]: EmulationDisplayTest,
81 | [TEST_ID.EC_BALANCE_CHECK]: EcBalanceCheckTest,
82 | [TEST_ID.EC_BALANCE_VERIFICATION]: EcBalanceVerificationTest,
83 | [TEST_ID.SEND_EC_TON_CONNECT]: SendEcViaTonConnectTest,
84 | [TEST_ID.SEND_EC_NATIVE]: SendEcViaNativeTest,
85 | [TEST_ID.EMULATION_DISPLAY_SEND]: EmulationDisplaySendTest,
86 | [TEST_ID.EMULATION_DISPLAY_NATIVE_SEND]: EmulationDisplayNativeSendTest,
87 | };
88 |
89 | /**
90 | * Wraps the onStatusChange callback to include the test ID
91 | * This allows child components to update the status without knowing their test ID
92 | */
93 | const handleStatusChange = useCallback((status: TestStatus) => {
94 | onStatusChange?.(testId, status);
95 | }, [testId, onStatusChange]);
96 |
97 | // Get the appropriate test component or fallback to MockTestCase if not found
98 | const TestComponent = (testCase.id in testComponents)
99 | ? testComponents[testCase.id as TestCaseId]
100 | : MockTestCase;
101 |
102 | return (
103 |
108 | );
109 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/hooks/use-test-steps.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { TestStep, TestStepStatus } from "@/types/test-steps";
3 | import { TestStatus } from "@/types/test-case";
4 |
5 | /**
6 | * Options for initializing useTestSteps hook
7 | */
8 | interface UseTestStepsOptions {
9 | /** Initial list of test steps */
10 | defaultSteps: TestStep[];
11 | /** Current status of the entire test */
12 | testStatus: TestStatus;
13 | }
14 |
15 | /**
16 | * Information for updating step status
17 | */
18 | interface StepStatusUpdate {
19 | /** Step identifier */
20 | id: string;
21 | /** New step status */
22 | status: TestStepStatus;
23 | }
24 |
25 | /**
26 | * Hook for managing test steps
27 | * Provides functions for updating statuses and content of steps
28 | *
29 | * @param options Initialization options
30 | * @returns Set of functions for working with steps
31 | */
32 | export function useTestSteps({
33 | defaultSteps,
34 | testStatus,
35 | }: UseTestStepsOptions) {
36 | // Initialization of steps with correct statuses
37 | const [steps, setSteps] = useState(() =>
38 | defaultSteps.map((step, index) => ({
39 | ...step,
40 | status:
41 | testStatus === "success"
42 | ? "success"
43 | : index === 0
44 | ? "pending"
45 | : "locked",
46 | }))
47 | );
48 |
49 | // Reset steps when test status changes
50 | useEffect(() => {
51 | if (testStatus === "pending") {
52 | setSteps(
53 | defaultSteps.map((step, index) => ({
54 | ...step,
55 | status: index === 0 ? "pending" : "locked",
56 | }))
57 | );
58 | } else if (testStatus === "success") {
59 | setSteps(
60 | defaultSteps.map((step) => ({
61 | ...step,
62 | status: "success",
63 | }))
64 | );
65 | }
66 | }, [testStatus, defaultSteps]);
67 |
68 | /**
69 | * Updates statuses of multiple steps simultaneously
70 | * @param updates Array of step updates
71 | */
72 | const updateStepsStatus = (updates: StepStatusUpdate[]) => {
73 | setSteps((steps) =>
74 | steps.map((step) => {
75 | const update = updates.find((s) => s.id === step.id);
76 | if (update) {
77 | return { ...step, status: update.status };
78 | }
79 | return step;
80 | })
81 | );
82 | };
83 |
84 | /**
85 | * Updates status of a single step
86 | * @param id Step identifier
87 | * @param status New status
88 | */
89 | const updateStepStatus = (id: string, status: TestStepStatus) => {
90 | updateStepsStatus([{ id, status }]);
91 | };
92 |
93 | /**
94 | * Sets one status for all steps at once
95 | * @param status Status to set for all steps
96 | */
97 | const setAllStepsStatus = (status: TestStepStatus) => {
98 | setSteps((steps) => steps.map((step) => ({ ...step, status })));
99 | };
100 |
101 | /**
102 | * Updates step content
103 | * @param stepId Step identifier
104 | * @param details New content (React nodes)
105 | */
106 | const updateStepDetails = (stepId: string, details: React.ReactNode) => {
107 | setSteps((steps) =>
108 | steps.map((step) => (step.id === stepId ? { ...step, details } : step))
109 | );
110 | };
111 |
112 | // Helper functions for commonly used operations
113 |
114 | /**
115 | * Sets step status to "success"
116 | * @param id Step identifier
117 | */
118 | const setStepSuccess = (id: string) => {
119 | updateStepStatus(id, "success");
120 | };
121 |
122 | /**
123 | * Sets step status to "failure"
124 | * @param id Step identifier
125 | */
126 | const setStepFailure = (id: string) => {
127 | updateStepStatus(id, "failure");
128 | };
129 |
130 | /**
131 | * Sets step status to "pending"
132 | * @param id Step identifier
133 | */
134 | const setStepPending = (id: string) => {
135 | updateStepStatus(id, "pending");
136 | };
137 |
138 | /**
139 | * Sets step status to "running"
140 | * @param id Step identifier
141 | */
142 | const setStepRunning = (id: string) => {
143 | updateStepStatus(id, "running");
144 | };
145 |
146 | /**
147 | * Sets status of all steps to "success"
148 | */
149 | const setAllStepsSuccess = () => {
150 | setAllStepsStatus("success");
151 | };
152 |
153 | return {
154 | steps,
155 | setSteps,
156 | updateStepsStatus,
157 | updateStepStatus,
158 | setAllStepsStatus,
159 | updateStepDetails,
160 | setStepSuccess,
161 | setStepFailure,
162 | setStepPending,
163 | setStepRunning,
164 | setAllStepsSuccess,
165 | };
166 | }
167 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form"
14 |
15 | import { cn } from "@/lib/utils"
16 | import { Label } from "@/components/ui/label"
17 |
18 | const Form = FormProvider
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName
25 | }
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | )
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext)
46 | const itemContext = React.useContext(FormItemContext)
47 | const { getFieldState, formState } = useFormContext()
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState)
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ")
53 | }
54 |
55 | const { id } = itemContext
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | }
65 | }
66 |
67 | type FormItemContextValue = {
68 | id: string
69 | }
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | )
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId()
80 |
81 | return (
82 |
83 |
84 |
85 | )
86 | })
87 | FormItem.displayName = "FormItem"
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField()
94 |
95 | return (
96 |
102 | )
103 | })
104 | FormLabel.displayName = "FormLabel"
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111 |
112 | return (
113 |
124 | )
125 | })
126 | FormControl.displayName = "FormControl"
127 |
128 | const FormDescription = React.forwardRef<
129 | HTMLParagraphElement,
130 | React.HTMLAttributes
131 | >(({ className, ...props }, ref) => {
132 | const { formDescriptionId } = useFormField()
133 |
134 | return (
135 |
141 | )
142 | })
143 | FormDescription.displayName = "FormDescription"
144 |
145 | const FormMessage = React.forwardRef<
146 | HTMLParagraphElement,
147 | React.HTMLAttributes
148 | >(({ className, children, ...props }, ref) => {
149 | const { error, formMessageId } = useFormField()
150 | const body = error ? String(error?.message) : children
151 |
152 | if (!body) {
153 | return null
154 | }
155 |
156 | return (
157 |
163 | {body}
164 |
165 | )
166 | })
167 | FormMessage.displayName = "FormMessage"
168 |
169 | export {
170 | useFormField,
171 | Form,
172 | FormItem,
173 | FormLabel,
174 | FormControl,
175 | FormDescription,
176 | FormMessage,
177 | FormField,
178 | }
179 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | const AlertDialog = AlertDialogPrimitive.Root
8 |
9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10 |
11 | const AlertDialogPortal = AlertDialogPrimitive.Portal
12 |
13 | const AlertDialogOverlay = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, ...props }, ref) => (
17 |
25 | ))
26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27 |
28 | const AlertDialogContent = React.forwardRef<
29 | React.ElementRef,
30 | React.ComponentPropsWithoutRef
31 | >(({ className, ...props }, ref) => (
32 |
33 |
34 |
42 |
43 | ))
44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45 |
46 | const AlertDialogHeader = ({
47 | className,
48 | ...props
49 | }: React.HTMLAttributes) => (
50 |
57 | )
58 | AlertDialogHeader.displayName = "AlertDialogHeader"
59 |
60 | const AlertDialogFooter = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | AlertDialogFooter.displayName = "AlertDialogFooter"
73 |
74 | const AlertDialogTitle = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef
77 | >(({ className, ...props }, ref) => (
78 |
83 | ))
84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85 |
86 | const AlertDialogDescription = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
95 | ))
96 | AlertDialogDescription.displayName =
97 | AlertDialogPrimitive.Description.displayName
98 |
99 | const AlertDialogAction = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110 |
111 | const AlertDialogCancel = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
124 | ))
125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126 |
127 | export {
128 | AlertDialog,
129 | AlertDialogPortal,
130 | AlertDialogOverlay,
131 | AlertDialogTrigger,
132 | AlertDialogContent,
133 | AlertDialogHeader,
134 | AlertDialogFooter,
135 | AlertDialogTitle,
136 | AlertDialogDescription,
137 | AlertDialogAction,
138 | AlertDialogCancel,
139 | }
140 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/data/test-cases.ts:
--------------------------------------------------------------------------------
1 | import { TestGroup } from "@/types/test-case";
2 |
3 | export const initialTestGroups: TestGroup[] = [
4 | {
5 | id: "emulation-settings",
6 | title: "Emulation Settings",
7 | description: "Configure wallet emulation preferences",
8 | cases: [
9 | {
10 | id: "emulation-support",
11 | title: "Emulation Support",
12 | description: "Does your wallet support emulation?",
13 | type: "prompt",
14 | status: "pending",
15 | isOptional: true,
16 | },
17 | ],
18 | },
19 | {
20 | id: "wallet-connection",
21 | title: "Wallet Connection",
22 | description: "Basic wallet connection and feature verification",
23 | cases: [
24 | {
25 | id: "connect-wallet",
26 | title: "Connect Wallet",
27 | description: "Click on Connect Wallet button to connect your wallet",
28 | type: "check",
29 | status: "pending",
30 | retryable: true,
31 | },
32 | {
33 | id: "ec-support",
34 | title: "Extra Currency Support",
35 | description:
36 | "Checking if wallet indicates Extra Currency support in its features",
37 | type: "check",
38 | status: "pending",
39 | dependencies: ["connect-wallet"],
40 | },
41 | ],
42 | },
43 | {
44 | id: "ec-topup",
45 | title: "Extra Currency Top Up",
46 | description: "Getting test Extra Currency",
47 | cases: [
48 | {
49 | id: "ton-balance",
50 | title: "TON Balance Check",
51 | description: "Checking if wallet has at least 3.25 test TON",
52 | type: "check",
53 | status: "pending",
54 | retryable: true,
55 | dependencies: ["connect-wallet"],
56 | },
57 | {
58 | id: "get-test-ec",
59 | title: "Get Test EC",
60 | description: "Sending TON to get test Extra Currency",
61 | type: "check",
62 | status: "pending",
63 | retryable: true,
64 | dependencies: ["ton-balance", "ec-support"],
65 | },
66 | {
67 | id: "emulation-display",
68 | title: "Emulation Display",
69 | description:
70 | "Wallet emulation displayed incoming EC and TON excess correctly",
71 | type: "prompt",
72 | status: "pending",
73 | isEmulationRequired: true,
74 | dependencies: ["get-test-ec", "emulation-support"],
75 | },
76 | ],
77 | },
78 | {
79 | id: "balance-verification",
80 | title: "Balance Verification",
81 | description: "Verifying EC balance display",
82 | cases: [
83 | {
84 | id: "ec-balance-check",
85 | title: "EC Balance Check",
86 | description: "Checking if wallet has Extra Currency",
87 | type: "check",
88 | status: "pending",
89 | retryable: true,
90 | dependencies: ["connect-wallet"],
91 | },
92 | {
93 | id: "ec-balance-verification",
94 | title: "Balance Display",
95 | description: "Verify that your wallet displays the correct EC balance",
96 | type: "prompt",
97 | status: "pending",
98 | dependencies: ["ec-balance-check"],
99 | },
100 | ],
101 | },
102 | {
103 | id: "send-ec-ton-connect",
104 | title: "Send EC via TON Connect",
105 | description: "Testing EC transfer via TON Connect",
106 | cases: [
107 | {
108 | id: "send-ec-ton-connect",
109 | title: "Send via TON Connect",
110 | description: "Sending EC back to swap contract via TON Connect",
111 | type: "check",
112 | status: "pending",
113 | retryable: true,
114 | dependencies: ["ec-balance-check", "ec-support"],
115 | },
116 | {
117 | id: "emulation-display-send",
118 | title: "Emulation Send Display",
119 | description: "Wallet emulation displayed outgoing EC correctly",
120 | type: "prompt",
121 | status: "pending",
122 | isEmulationRequired: true,
123 | dependencies: ["send-ec-ton-connect", "emulation-support"],
124 | },
125 | ],
126 | },
127 | {
128 | id: "send-ec-native",
129 | title: "Send EC via Wallet Interface",
130 | description: "Testing EC transfer via wallet interface",
131 | cases: [
132 | {
133 | id: "send-ec-native",
134 | title: "Send via Wallet Interface",
135 | description: "Send EC to the swap contract using wallet interface",
136 | type: "prompt",
137 | status: "pending",
138 | retryable: true,
139 | dependencies: ["ec-balance-check"],
140 | },
141 | {
142 | id: "emulation-display-native-send",
143 | title: "Emulation Send Display",
144 | description: "Wallet emulation displayed outgoing EC correctly",
145 | type: "prompt",
146 | status: "pending",
147 | isEmulationRequired: true,
148 | dependencies: ["send-ec-native", "emulation-support"],
149 | },
150 | ],
151 | },
152 | ];
153 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/ExtraCurrencySupportTest.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { useTonConnect } from "@/hooks/use-ton-connect";
3 | import { useEffect, ReactNode } from "react";
4 | import { Alert, AlertDescription } from "@/components/ui/alert";
5 | import { Code } from "@/components/ui/code";
6 | import { TestStepsList } from "@/components/test-steps/TestStepsList";
7 | import { useTestSteps } from "@/hooks/use-test-steps";
8 | import { TestStep } from "@/types/test-steps";
9 | import { TestStatus } from "@/types/test-case";
10 |
11 | /**
12 | * Step identifiers used throughout the component
13 | */
14 | const STEP_ID = {
15 | CHECK_SUPPORT: 'check-support'
16 | } as const;
17 |
18 | /**
19 | * Default test steps for Extra Currency support verification
20 | */
21 | const DEFAULT_STEPS: TestStep[] = [
22 | {
23 | id: STEP_ID.CHECK_SUPPORT,
24 | title: 'Extra Currency Support',
25 | description: 'Check if wallet supports Extra Currency feature',
26 | type: 'auto-check',
27 | status: 'pending'
28 | }
29 | ];
30 |
31 | /**
32 | * Component for testing whether wallet supports Extra Currency feature
33 | */
34 | export function ExtraCurrencySupportTest(props: BaseTestCaseProps) {
35 | const { isConnected, hasExtraCurrencySupport, wallet } = useTonConnect();
36 | const {
37 | steps,
38 | updateStepDetails,
39 | setStepSuccess,
40 | setStepFailure,
41 | setStepPending
42 | } = useTestSteps({
43 | defaultSteps: DEFAULT_STEPS,
44 | testStatus: props.testCase.status
45 | });
46 |
47 | /**
48 | * Verifies if the wallet supports Extra Currency functionality
49 | * @returns Test status based on wallet's Extra Currency support
50 | */
51 | const checkExtraCurrencySupport = (): TestStatus => {
52 | if (!isConnected) {
53 | setStepPending(STEP_ID.CHECK_SUPPORT);
54 | return 'pending';
55 | }
56 |
57 | if (hasExtraCurrencySupport) {
58 | setStepSuccess(STEP_ID.CHECK_SUPPORT);
59 | return 'success';
60 | }
61 |
62 | setStepFailure(STEP_ID.CHECK_SUPPORT);
63 | return 'failure';
64 | };
65 |
66 | /**
67 | * Renders instructions for the Extra Currency support check
68 | */
69 | const renderInstructions = (): ReactNode => (
70 |
71 |
72 | Checking if your wallet indicates Extra Currency support in its features.
73 | The wallet should provide extraCurrencySupported flag in SendTransaction feature.
74 |
75 |
76 | );
77 |
78 | /**
79 | * Renders success message when wallet supports Extra Currency
80 | */
81 | const renderSuccessMessage = (): ReactNode => (
82 |
83 | ✓ Wallet correctly indicates Extra Currency support
84 |
85 | );
86 |
87 | /**
88 | * Renders failure message when wallet doesn't support Extra Currency
89 | */
90 | const renderFailureMessage = (): ReactNode => (
91 |
92 |
93 | ✗ Wallet does not indicate Extra Currency support
94 |
95 |
96 | {`{
97 | name: 'SendTransaction',
98 | maxMessages: number,
99 | extraCurrencySupported: boolean
100 | }`}
101 |
102 |
103 | );
104 |
105 | /**
106 | * Renders the main content for the Extra Currency support check
107 | */
108 | const renderSupportCheckContent = (): ReactNode => (
109 |
110 | {renderInstructions()}
111 | {props.testCase.status === 'success' && renderSuccessMessage()}
112 | {props.testCase.status === 'failure' && renderFailureMessage()}
113 |
114 | );
115 |
116 | /**
117 | * Updates test status based on wallet's state
118 | */
119 | useEffect(() => {
120 | if (props.testCase.status === 'running' || props.testCase.status === 'pending') {
121 | const result = checkExtraCurrencySupport();
122 | if (result !== props.testCase.status) {
123 | props.onStatusChange?.(result);
124 | }
125 | }
126 |
127 | if (props.testCase.status === 'failure') {
128 | if (hasExtraCurrencySupport) {
129 | setStepSuccess(STEP_ID.CHECK_SUPPORT);
130 | props.onStatusChange?.('success');
131 | } else if (isConnected) {
132 | setStepFailure(STEP_ID.CHECK_SUPPORT);
133 | props.onStatusChange?.('failure');
134 | }
135 | }
136 | }, [props.testCase.status, wallet, isConnected, hasExtraCurrencySupport, props.onStatusChange]);
137 |
138 | /**
139 | * Updates step content based on current test state
140 | */
141 | useEffect(() => {
142 | updateStepDetails(STEP_ID.CHECK_SUPPORT, renderSupportCheckContent());
143 | }, [props.testCase.status]);
144 |
145 | return (
146 |
147 |
148 |
149 |
150 |
151 | );
152 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/EmulationSupportTest.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { Button } from "@/components/ui/button";
3 | import { Check, X } from "lucide-react";
4 | import { Alert, AlertDescription } from "@/components/ui/alert";
5 | import { TestStepsList } from "@/components/test-steps/TestStepsList";
6 | import { useTestSteps } from "@/hooks/use-test-steps";
7 | import { TestStep } from "@/types/test-steps";
8 | import { useEffect, ReactNode } from "react";
9 |
10 | /**
11 | * Step identifiers used throughout the component
12 | */
13 | const STEP_ID = {
14 | CHECK_EMULATION: 'check-emulation'
15 | } as const;
16 |
17 | /**
18 | * Default test steps for transaction emulation support test
19 | */
20 | const DEFAULT_STEPS: TestStep[] = [
21 | {
22 | id: STEP_ID.CHECK_EMULATION,
23 | title: 'Transaction Emulation Support',
24 | description: 'Optional: Check if your wallet supports transaction preview',
25 | type: 'user-action',
26 | status: 'pending'
27 | }
28 | ];
29 |
30 | /**
31 | * Component for testing whether wallet supports transaction emulation features
32 | */
33 | export function EmulationSupportTest(props: BaseTestCaseProps) {
34 | const {
35 | steps,
36 | updateStepDetails,
37 | setStepSuccess,
38 | setStepFailure
39 | } = useTestSteps({
40 | defaultSteps: DEFAULT_STEPS,
41 | testStatus: props.testCase.status
42 | });
43 |
44 | /**
45 | * Handles user response to emulation support question
46 | * Updates test status based on whether the wallet supports transaction preview
47 | */
48 | const handleResponse = (correct: boolean) => {
49 | if (correct) {
50 | setStepSuccess(STEP_ID.CHECK_EMULATION);
51 | } else {
52 | setStepFailure(STEP_ID.CHECK_EMULATION);
53 | }
54 | props.onStatusChange?.(correct ? 'success' : 'failure');
55 | };
56 |
57 | /**
58 | * Renders emulation feature description and instructions
59 | */
60 | const renderEmulationDescription = (): ReactNode => (
61 |
62 |
63 | Does your wallet support transaction emulation?
64 |
65 | Shows preview of incoming/outgoing EC amounts before confirmation
66 | Displays expected TON excess for swap operations
67 | Allows reviewing transaction details before signing
68 |
69 |
70 | Note: This is an optional feature. If your wallet doesn't support emulation,
71 | the testing stand will use alternative verification methods.
72 |
73 |
74 |
75 | );
76 |
77 | /**
78 | * Renders success message when emulation is supported
79 | */
80 | const renderSuccessMessage = (): ReactNode => (
81 |
82 | ✓ Emulation support confirmed - additional preview tests will be included
83 |
84 | );
85 |
86 | /**
87 | * Renders message for when emulation is not supported
88 | */
89 | const renderFailureMessage = (): ReactNode => (
90 |
91 | Proceeding without emulation - standard verification will be used
92 |
93 | );
94 |
95 | /**
96 | * Renders user verification controls (Yes/No buttons)
97 | */
98 | const renderVerificationControls = (): ReactNode => (
99 |
100 | handleResponse(true)}
104 | className="bg-green-500/10 text-green-500 hover:bg-green-500/20 hover:text-green-600"
105 | >
106 |
107 | Yes
108 |
109 | handleResponse(false)}
113 | className="bg-red-500/10 text-red-500 hover:bg-red-500/20 hover:text-red-600"
114 | >
115 |
116 | No
117 |
118 |
119 | );
120 |
121 | /**
122 | * Renders the main content for the emulation check step
123 | */
124 | const renderEmulationCheckContent = (): ReactNode => {
125 | return (
126 |
127 | {renderEmulationDescription()}
128 | {props.testCase.status === 'success' && renderSuccessMessage()}
129 | {props.testCase.status === 'failure' && renderFailureMessage()}
130 | {props.testCase.status === 'pending' && renderVerificationControls()}
131 |
132 | );
133 | };
134 |
135 | /**
136 | * Updates step UI based on current test state
137 | */
138 | useEffect(() => {
139 | updateStepDetails(STEP_ID.CHECK_EMULATION, renderEmulationCheckContent());
140 | }, [props.testCase.status]);
141 |
142 | return (
143 |
144 |
145 |
146 |
147 |
148 | );
149 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/EmulationDisplayTest.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { Button } from "@/components/ui/button";
3 | import { Check, X } from "lucide-react";
4 | import { Alert, AlertDescription } from "@/components/ui/alert";
5 | import { TestStepsList } from "@/components/test-steps/TestStepsList";
6 | import { useTestSteps } from "@/hooks/use-test-steps";
7 | import { TestStep } from "@/types/test-steps";
8 | import { useEffect, ReactNode } from "react";
9 |
10 | /**
11 | * Step identifiers used throughout the component
12 | */
13 | const STEP_ID = {
14 | CHECK_EMULATION: 'check-emulation'
15 | } as const;
16 |
17 | /**
18 | * Default test steps for Extra Currency emulation display test
19 | */
20 | const DEFAULT_STEPS: TestStep[] = [
21 | {
22 | id: STEP_ID.CHECK_EMULATION,
23 | title: 'Emulation Display',
24 | description: 'Verify that your wallet correctly displays the emulation preview for incoming EC and TON excess return',
25 | type: 'user-action',
26 | status: 'pending'
27 | }
28 | ];
29 |
30 | /**
31 | * Component for testing whether wallet correctly displays emulation preview
32 | * for incoming Extra Currency and TON excess return
33 | */
34 | export function EmulationDisplayTest(props: BaseTestCaseProps) {
35 | const {
36 | steps,
37 | updateStepDetails,
38 | setStepSuccess,
39 | setStepFailure
40 | } = useTestSteps({
41 | defaultSteps: DEFAULT_STEPS,
42 | testStatus: props.testCase.status
43 | });
44 |
45 | /**
46 | * Handles user response to verification question
47 | * Updates test status based on whether the wallet's transaction preview was correct
48 | */
49 | const handleResponse = (correct: boolean) => {
50 | if (correct) {
51 | setStepSuccess(STEP_ID.CHECK_EMULATION);
52 | } else {
53 | setStepFailure(STEP_ID.CHECK_EMULATION);
54 | }
55 | props.onStatusChange?.(correct ? 'success' : 'failure');
56 | };
57 |
58 | /**
59 | * Renders verification instructions for the user
60 | */
61 | const renderVerificationInstructions = (): ReactNode => (
62 |
63 |
64 | Please verify that your wallet's transaction preview correctly shows:
65 |
66 | The exact amount of Extra Currency you will receive
67 | The amount of TON that will be returned as excess
68 |
69 |
70 |
71 | );
72 |
73 | /**
74 | * Renders success message when wallet properly displays emulation preview
75 | */
76 | const renderSuccessMessage = (): ReactNode => (
77 |
78 | ✓ Transaction preview displays correct emulation details
79 |
80 | );
81 |
82 | /**
83 | * Renders failure message when wallet doesn't properly display emulation preview
84 | */
85 | const renderFailureMessage = (): ReactNode => (
86 |
87 |
88 | Transaction preview issues detected:
89 |
90 |
The wallet did not display the transaction preview correctly. The preview should include:
91 |
92 | Expected Extra Currency amount to receive
93 | Expected TON amount to be returned as excess
94 |
95 |
96 |
97 |
98 | );
99 |
100 | /**
101 | * Renders user verification controls (Yes/No buttons)
102 | */
103 | const renderVerificationControls = (): ReactNode => (
104 |
105 | handleResponse(true)}
109 | className="bg-green-500/10 text-green-500 hover:bg-green-500/20 hover:text-green-600"
110 | >
111 |
112 | Yes
113 |
114 | handleResponse(false)}
118 | className="bg-red-500/10 text-red-500 hover:bg-red-500/20 hover:text-red-600"
119 | >
120 |
121 | No
122 |
123 |
124 | );
125 |
126 | /**
127 | * Renders the main content for the emulation check step
128 | */
129 | const renderEmulationCheckContent = (): ReactNode => {
130 | return (
131 |
132 | {renderVerificationInstructions()}
133 | {props.testCase.status === 'success' && renderSuccessMessage()}
134 | {props.testCase.status === 'failure' && renderFailureMessage()}
135 | {props.testCase.status === 'pending' && renderVerificationControls()}
136 |
137 | );
138 | };
139 |
140 | /**
141 | * Updates step UI based on current test state
142 | */
143 | useEffect(() => {
144 | updateStepDetails(STEP_ID.CHECK_EMULATION, renderEmulationCheckContent());
145 | }, [props.testCase.status]);
146 |
147 | return (
148 |
149 |
150 |
151 |
152 |
153 | );
154 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { type DialogProps } from "@radix-ui/react-dialog"
3 | import { Command as CommandPrimitive } from "cmdk"
4 | import { Search } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { Dialog, DialogContent } from "@/components/ui/dialog"
8 |
9 | const Command = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ))
22 | Command.displayName = CommandPrimitive.displayName
23 |
24 | const CommandDialog = ({ children, ...props }: DialogProps) => {
25 | return (
26 |
27 |
28 |
29 | {children}
30 |
31 |
32 |
33 | )
34 | }
35 |
36 | const CommandInput = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, ...props }, ref) => (
40 |
41 |
42 |
50 |
51 | ))
52 |
53 | CommandInput.displayName = CommandPrimitive.Input.displayName
54 |
55 | const CommandList = React.forwardRef<
56 | React.ElementRef,
57 | React.ComponentPropsWithoutRef
58 | >(({ className, ...props }, ref) => (
59 |
64 | ))
65 |
66 | CommandList.displayName = CommandPrimitive.List.displayName
67 |
68 | const CommandEmpty = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >((props, ref) => (
72 |
77 | ))
78 |
79 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
80 |
81 | const CommandGroup = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, ...props }, ref) => (
85 |
93 | ))
94 |
95 | CommandGroup.displayName = CommandPrimitive.Group.displayName
96 |
97 | const CommandSeparator = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
108 |
109 | const CommandItem = React.forwardRef<
110 | React.ElementRef,
111 | React.ComponentPropsWithoutRef
112 | >(({ className, ...props }, ref) => (
113 |
121 | ))
122 |
123 | CommandItem.displayName = CommandPrimitive.Item.displayName
124 |
125 | const CommandShortcut = ({
126 | className,
127 | ...props
128 | }: React.HTMLAttributes) => {
129 | return (
130 |
137 | )
138 | }
139 | CommandShortcut.displayName = "CommandShortcut"
140 |
141 | export {
142 | Command,
143 | CommandDialog,
144 | CommandInput,
145 | CommandList,
146 | CommandEmpty,
147 | CommandGroup,
148 | CommandItem,
149 | CommandShortcut,
150 | CommandSeparator,
151 | }
152 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/EmulationDisplayNativeSendTest.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { Button } from "@/components/ui/button";
3 | import { Check, X } from "lucide-react";
4 | import { Alert, AlertDescription } from "@/components/ui/alert";
5 | import { TestStepsList } from "@/components/test-steps/TestStepsList";
6 | import { useTestSteps } from "@/hooks/use-test-steps";
7 | import { TestStep } from "@/types/test-steps";
8 | import { useEffect, ReactNode } from "react";
9 |
10 | /**
11 | * Step identifiers used throughout the component
12 | */
13 | const STEP_ID = {
14 | CHECK_EMULATION: 'check-emulation'
15 | } as const;
16 |
17 | /**
18 | * Default test steps for native EC transfer emulation display test
19 | */
20 | const DEFAULT_STEPS: TestStep[] = [
21 | {
22 | id: STEP_ID.CHECK_EMULATION,
23 | title: 'Emulation Display',
24 | description: 'Verify that your wallet correctly displays the transaction preview for native EC transfer',
25 | type: 'user-action',
26 | status: 'pending'
27 | }
28 | ];
29 |
30 | /**
31 | * Component for testing whether wallet correctly displays native EC transfer transaction preview
32 | */
33 | export function EmulationDisplayNativeSendTest(props: BaseTestCaseProps) {
34 | const {
35 | steps,
36 | updateStepDetails,
37 | setStepSuccess,
38 | setStepFailure
39 | } = useTestSteps({
40 | defaultSteps: DEFAULT_STEPS,
41 | testStatus: props.testCase.status
42 | });
43 |
44 | /**
45 | * Handles user response to verification question
46 | * Updates test status based on whether the wallet's transaction preview was correct
47 | */
48 | const handleResponse = (correct: boolean) => {
49 | if (correct) {
50 | setStepSuccess(STEP_ID.CHECK_EMULATION);
51 | } else {
52 | setStepFailure(STEP_ID.CHECK_EMULATION);
53 | }
54 | props.onStatusChange?.(correct ? 'success' : 'failure');
55 | };
56 |
57 | /**
58 | * Renders verification instructions for the user
59 | */
60 | const renderVerificationInstructions = (): ReactNode => (
61 |
62 |
63 | Please verify that your wallet's transaction preview correctly shows:
64 |
65 | The exact amount of Extra Currency being sent
66 | The correct recipient address
67 | All transaction details and fees
68 |
69 |
70 |
71 | );
72 |
73 | /**
74 | * Renders success message when wallet properly displays transaction preview
75 | */
76 | const renderSuccessMessage = (): ReactNode => (
77 |
78 | ✓ Transaction preview displays correct native transfer details
79 |
80 | );
81 |
82 | /**
83 | * Renders failure message when wallet doesn't properly display transaction preview
84 | */
85 | const renderFailureMessage = (): ReactNode => (
86 |
87 |
88 | Transaction preview verification failed:
89 |
90 |
The wallet's transaction preview is missing required information. Please ensure it shows:
91 |
92 | The exact amount of Extra Currency to be sent
93 | The complete recipient address
94 | All transaction details including fees
95 |
96 |
97 |
98 |
99 | );
100 |
101 | /**
102 | * Renders user verification controls (Yes/No buttons)
103 | */
104 | const renderVerificationControls = (): ReactNode => (
105 |
106 | handleResponse(true)}
110 | className="bg-green-500/10 text-green-500 hover:bg-green-500/20 hover:text-green-600"
111 | >
112 |
113 | Yes
114 |
115 | handleResponse(false)}
119 | className="bg-red-500/10 text-red-500 hover:bg-red-500/20 hover:text-red-600"
120 | >
121 |
122 | No
123 |
124 |
125 | );
126 |
127 | /**
128 | * Renders the main content for the emulation check step
129 | */
130 | const renderEmulationCheckContent = (): ReactNode => {
131 | return (
132 |
133 | {renderVerificationInstructions()}
134 | {props.testCase.status === 'success' && renderSuccessMessage()}
135 | {props.testCase.status === 'failure' && renderFailureMessage()}
136 | {props.testCase.status === 'pending' && renderVerificationControls()}
137 |
138 | );
139 | };
140 |
141 | /**
142 | * Updates step UI based on current test state
143 | */
144 | useEffect(() => {
145 | updateStepDetails(STEP_ID.CHECK_EMULATION, renderEmulationCheckContent());
146 | }, [props.testCase.status]);
147 |
148 | return (
149 |
150 |
151 |
152 |
153 |
154 | );
155 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/TestCaseCard.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2 | import { Button } from "@/components/ui/button";
3 | import { TestCase } from "@/types/test-case";
4 | import { CheckCircle, XCircle, Loader2, HelpCircle, Lock, Wallet } from "lucide-react";
5 | import { Badge } from "@/components/ui/badge";
6 | import { useTonConnect } from "@/hooks/use-ton-connect";
7 | import { useEffect } from "react";
8 |
9 | interface TestCaseCardProps {
10 | testCase: TestCase;
11 | onRetry?: () => void;
12 | onYes?: () => void;
13 | onNo?: () => void;
14 | isLocked?: boolean;
15 | onStatusChange?: (status: 'running' | 'success' | 'failure') => void;
16 | }
17 |
18 | export function TestCaseCard({
19 | testCase,
20 | onRetry,
21 | onYes,
22 | onNo,
23 | isLocked = false,
24 | onStatusChange
25 | }: TestCaseCardProps) {
26 | const { connect, isConnected, hasExtraCurrencySupport, wallet } = useTonConnect();
27 |
28 | // Handle wallet connection test cases
29 | useEffect(() => {
30 | if (testCase.status !== 'running') return;
31 |
32 | if (testCase.id === 'connect-wallet') {
33 | if (isConnected) {
34 | onStatusChange?.('success');
35 | }
36 | }
37 |
38 | if (testCase.id === 'ec-support') {
39 | if (hasExtraCurrencySupport) {
40 | onStatusChange?.('success');
41 | } else if (isConnected) {
42 | onStatusChange?.('failure');
43 | }
44 | }
45 | }, [testCase.id, testCase.status, wallet, isConnected, hasExtraCurrencySupport, onStatusChange]);
46 |
47 | const StatusIcon = () => {
48 | if (isLocked) return ;
49 |
50 | switch (testCase.status) {
51 | case 'success':
52 | return ;
53 | case 'failure':
54 | return ;
55 | case 'running':
56 | return ;
57 | default:
58 | return ;
59 | }
60 | };
61 |
62 | // Custom action buttons for specific test cases
63 | const renderCustomActions = () => {
64 | if (testCase.id === 'connect-wallet' && !wallet) {
65 | return (
66 | {
70 | connect();
71 | onStatusChange?.('running');
72 | }}
73 | className="bg-blue-500/10 text-blue-500 hover:bg-blue-500/20 hover:text-blue-600"
74 | >
75 |
76 | Connect Wallet
77 |
78 | );
79 | }
80 | return null;
81 | };
82 |
83 | return (
84 |
85 |
86 |
87 |
88 |
89 | {testCase.title}
90 |
91 |
92 | {testCase.isOptional && (
93 | Optional
94 | )}
95 | {testCase.isEmulationRequired && (
96 | Emulation
97 | )}
98 | {testCase.type === 'check' && (
99 | Auto Check
100 | )}
101 | {testCase.type === 'prompt' && (
102 | Manual Check
103 | )}
104 |
105 |
106 |
107 | {testCase.description}
108 |
109 |
110 |
111 |
112 |
113 | {!isLocked && (
114 |
115 | {renderCustomActions()}
116 |
117 | {testCase.type === 'prompt' && testCase.status === 'running' && (
118 |
119 |
125 | Yes
126 |
127 |
133 | No
134 |
135 |
136 | )}
137 |
138 | {testCase.status === 'failure' && testCase.retryable && (
139 |
140 |
146 | Retry
147 |
148 |
149 | )}
150 |
151 | )}
152 |
153 | );
154 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/TestCaseRenderer.tsx:
--------------------------------------------------------------------------------
1 | import { TestCase, TestStatus } from "@/types/test-case";
2 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3 | import { Badge } from "@/components/ui/badge";
4 | import { CheckCircle, XCircle, Loader2, HelpCircle, Lock, ChevronDown } from "lucide-react";
5 | import { useState, useEffect } from "react";
6 |
7 | /**
8 | * Props for the TestCaseRenderer component
9 | */
10 | interface TestCaseRendererProps {
11 | /** The test case to render */
12 | testCase: TestCase;
13 | /** Whether the test case is locked and cannot be interacted with */
14 | isLocked?: boolean;
15 | /** Callback triggered when the test status changes */
16 | onStatusChange?: (status: TestStatus) => void;
17 | /** Content to render inside the expandable section of the test case */
18 | children?: React.ReactNode;
19 | }
20 |
21 | /**
22 | * Component that renders a test case with its status, description, and expandable content
23 | */
24 | export function TestCaseRenderer({
25 | testCase,
26 | isLocked = false,
27 | children
28 | }: TestCaseRendererProps) {
29 | const [isExpanded, setIsExpanded] = useState(true);
30 |
31 | // Derived states for readability
32 | const isCompleted = testCase.status === 'success';
33 | const isFailed = testCase.status === 'failure';
34 | const isRunning = testCase.status === 'running';
35 | const isPending = testCase.status === 'pending';
36 | const isInteractive = isCompleted || isFailed;
37 |
38 | /**
39 | * Handle automatic collapse of successful test cases after a delay
40 | * and ensure expansion for running, pending, or failed test cases
41 | */
42 | useEffect(() => {
43 | if (isCompleted) {
44 | const collapseTimer = setTimeout(() => setIsExpanded(false), 2000);
45 | return () => clearTimeout(collapseTimer);
46 | } else if (isPending || isRunning || isFailed) {
47 | setIsExpanded(true);
48 | }
49 | }, [isCompleted, isPending, isRunning, isFailed]);
50 |
51 | /**
52 | * Toggle the expanded state of the test case card
53 | */
54 | const handleToggleExpand = () => {
55 | if (isInteractive) {
56 | setIsExpanded(!isExpanded);
57 | }
58 | };
59 |
60 | /**
61 | * Renders the appropriate status icon based on test case state
62 | */
63 | const renderStatusIcon = (): React.ReactNode => {
64 | if (isLocked) return ;
65 |
66 | switch (testCase.status) {
67 | case 'success':
68 | return ;
69 | case 'failure':
70 | return ;
71 | case 'running':
72 | return ;
73 | default:
74 | return ;
75 | }
76 | };
77 |
78 | /**
79 | * Renders the badge indicators for test case attributes
80 | */
81 | const renderBadges = (): React.ReactNode => {
82 | return (
83 |
84 | {testCase.isOptional && (
85 | Optional
86 | )}
87 | {testCase.isEmulationRequired && (
88 | Emulation
89 | )}
90 | {testCase.type === 'check' && (
91 | Auto Check
92 | )}
93 | {testCase.type === 'prompt' && (
94 | Manual Check
95 | )}
96 |
97 | );
98 | };
99 |
100 | /**
101 | * Renders the card header with title, description, and status
102 | */
103 | const renderCardHeader = (): React.ReactNode => {
104 | return (
105 |
109 |
110 |
111 |
112 | {testCase.title}
113 |
114 | {renderBadges()}
115 |
116 |
117 | {testCase.description}
118 |
119 |
120 |
121 | {renderStatusIcon()}
122 | {isInteractive && (
123 |
126 | )}
127 |
128 |
129 | );
130 | };
131 |
132 | /**
133 | * Renders the expandable content section
134 | */
135 | const renderExpandableContent = (): React.ReactNode => {
136 | if (isLocked || !children) return null;
137 |
138 | return (
139 |
144 |
145 | {children}
146 |
147 |
148 | );
149 | };
150 |
151 | return (
152 |
153 | {renderCardHeader()}
154 | {renderExpandableContent()}
155 |
156 | );
157 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/EmulationDisplaySendTest.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { Button } from "@/components/ui/button";
3 | import { Check, X } from "lucide-react";
4 | import { Alert, AlertDescription } from "@/components/ui/alert";
5 | import { TestStepsList } from "@/components/test-steps/TestStepsList";
6 | import { useTestSteps } from "@/hooks/use-test-steps";
7 | import { TestStep } from "@/types/test-steps";
8 | import { useEffect, ReactNode } from "react";
9 |
10 | /**
11 | * Step identifiers used throughout the component
12 | */
13 | const STEP_ID = {
14 | CHECK_EMULATION: 'check-emulation'
15 | } as const;
16 |
17 | /**
18 | * Default test steps for EC transfer emulation display test
19 | */
20 | const DEFAULT_STEPS: TestStep[] = [
21 | {
22 | id: STEP_ID.CHECK_EMULATION,
23 | title: 'Emulation Display',
24 | description: 'Verify that your wallet correctly displays the transaction preview for outgoing EC transfer',
25 | type: 'user-action',
26 | status: 'pending'
27 | }
28 | ];
29 |
30 | /**
31 | * Component for testing whether wallet correctly displays EC transfer transaction preview
32 | */
33 | export function EmulationDisplaySendTest(props: BaseTestCaseProps) {
34 | const {
35 | steps,
36 | updateStepDetails,
37 | setStepSuccess,
38 | setStepFailure
39 | } = useTestSteps({
40 | defaultSteps: DEFAULT_STEPS,
41 | testStatus: props.testCase.status
42 | });
43 |
44 | /**
45 | * Handles user response to verification question
46 | * Updates test status based on whether the wallet's transaction preview was correct
47 | */
48 | const handleResponse = (correct: boolean) => {
49 | if (correct) {
50 | setStepSuccess(STEP_ID.CHECK_EMULATION);
51 | } else {
52 | setStepFailure(STEP_ID.CHECK_EMULATION);
53 | }
54 | props.onStatusChange?.(correct ? 'success' : 'failure');
55 | };
56 |
57 | /**
58 | * Renders verification instructions for the user
59 | */
60 | const renderVerificationInstructions = (): ReactNode => (
61 |
62 |
63 | Please verify that your wallet's transaction preview correctly shows:
64 |
65 | The exact amount of Extra Currency being sent
66 | The correct recipient address (EC swap contract)
67 | All transaction details and fees
68 |
69 |
70 | Note: The transaction preview should clearly show all details of the outgoing EC transfer.
71 |
72 |
73 |
74 | );
75 |
76 | /**
77 | * Renders success message when wallet properly displays transaction preview
78 | */
79 | const renderSuccessMessage = (): ReactNode => (
80 |
81 | ✓ Transaction preview displays correct outgoing transfer details
82 |
83 | );
84 |
85 | /**
86 | * Renders failure message when wallet doesn't properly display transaction preview
87 | */
88 | const renderFailureMessage = (): ReactNode => (
89 |
90 |
91 | Transaction preview verification failed:
92 |
93 |
The wallet's transaction preview is missing required information. Please ensure it shows:
94 |
95 | The exact amount of Extra Currency to be sent
96 | The complete recipient address (EC swap contract)
97 | All transaction details including fees and warnings
98 |
99 |
100 |
101 |
102 | );
103 |
104 | /**
105 | * Renders user verification controls (Yes/No buttons)
106 | */
107 | const renderVerificationControls = (): ReactNode => (
108 |
109 | handleResponse(true)}
113 | className="bg-green-500/10 text-green-500 hover:bg-green-500/20 hover:text-green-600"
114 | >
115 |
116 | Yes
117 |
118 | handleResponse(false)}
122 | className="bg-red-500/10 text-red-500 hover:bg-red-500/20 hover:text-red-600"
123 | >
124 |
125 | No
126 |
127 |
128 | );
129 |
130 | /**
131 | * Renders the main content for the emulation check step
132 | */
133 | const renderEmulationCheckContent = (): ReactNode => {
134 | return (
135 |
136 | {renderVerificationInstructions()}
137 | {props.testCase.status === 'success' && renderSuccessMessage()}
138 | {props.testCase.status === 'failure' && renderFailureMessage()}
139 | {props.testCase.status === 'pending' && renderVerificationControls()}
140 |
141 | );
142 | };
143 |
144 | /**
145 | * Updates step UI based on current test state
146 | */
147 | useEffect(() => {
148 | updateStepDetails(STEP_ID.CHECK_EMULATION, renderEmulationCheckContent());
149 | }, [props.testCase.status]);
150 |
151 | return (
152 |
153 |
154 |
155 |
156 |
157 | );
158 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 | ))
134 | SelectItem.displayName = SelectPrimitive.Item.displayName
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ))
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | }
160 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
4 |
5 | /**
6 | * Maximum number of toast notifications displayed simultaneously
7 | */
8 | const TOAST_LIMIT = 1;
9 |
10 | /**
11 | * Delay before removing a closed toast notification (in milliseconds)
12 | */
13 | const TOAST_REMOVE_DELAY = 1000000;
14 |
15 | /**
16 | * Extended interface for toast notifications with additional properties
17 | */
18 | type ToasterToast = ToastProps & {
19 | /** Unique toast identifier */
20 | id: string;
21 | /** Toast title */
22 | title?: React.ReactNode;
23 | /** Toast description/content */
24 | description?: React.ReactNode;
25 | /** Action element (button) */
26 | action?: ToastActionElement;
27 | };
28 |
29 | /**
30 | * Action types for toast management
31 | */
32 | const actionTypes = {
33 | ADD_TOAST: "ADD_TOAST",
34 | UPDATE_TOAST: "UPDATE_TOAST",
35 | DISMISS_TOAST: "DISMISS_TOAST",
36 | REMOVE_TOAST: "REMOVE_TOAST",
37 | } as const;
38 |
39 | // Counter for generating unique identifiers
40 | let count = 0;
41 |
42 | /**
43 | * Generates a unique identifier for a toast
44 | * @returns String identifier
45 | */
46 | function genId() {
47 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
48 | return count.toString();
49 | }
50 |
51 | type ActionType = typeof actionTypes;
52 |
53 | /**
54 | * Action types for the toast reducer
55 | */
56 | type Action =
57 | | {
58 | type: ActionType["ADD_TOAST"];
59 | toast: ToasterToast;
60 | }
61 | | {
62 | type: ActionType["UPDATE_TOAST"];
63 | toast: Partial;
64 | }
65 | | {
66 | type: ActionType["DISMISS_TOAST"];
67 | toastId?: ToasterToast["id"];
68 | }
69 | | {
70 | type: ActionType["REMOVE_TOAST"];
71 | toastId?: ToasterToast["id"];
72 | };
73 |
74 | /**
75 | * Toast state interface
76 | */
77 | interface State {
78 | /** Array of active toasts */
79 | toasts: ToasterToast[];
80 | }
81 |
82 | // Stores timers for delayed toast removal
83 | const toastTimeouts = new Map>();
84 |
85 | /**
86 | * Adds a toast to the removal queue
87 | * @param toastId Toast identifier to remove
88 | */
89 | const addToRemoveQueue = (toastId: string) => {
90 | if (toastTimeouts.has(toastId)) {
91 | return;
92 | }
93 |
94 | const timeout = setTimeout(() => {
95 | toastTimeouts.delete(toastId);
96 | dispatch({
97 | type: "REMOVE_TOAST",
98 | toastId: toastId,
99 | });
100 | }, TOAST_REMOVE_DELAY);
101 |
102 | toastTimeouts.set(toastId, timeout);
103 | };
104 |
105 | /**
106 | * Reducer for managing toast state
107 | * @param state Current state
108 | * @param action Action to process
109 | * @returns New state
110 | */
111 | export const reducer = (state: State, action: Action): State => {
112 | switch (action.type) {
113 | case "ADD_TOAST":
114 | return {
115 | ...state,
116 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
117 | };
118 |
119 | case "UPDATE_TOAST":
120 | return {
121 | ...state,
122 | toasts: state.toasts.map((t) =>
123 | t.id === action.toast.id ? { ...t, ...action.toast } : t
124 | ),
125 | };
126 |
127 | case "DISMISS_TOAST": {
128 | const { toastId } = action;
129 |
130 | // Side effects - adding to the removal queue
131 | if (toastId) {
132 | addToRemoveQueue(toastId);
133 | } else {
134 | state.toasts.forEach((toast) => {
135 | addToRemoveQueue(toast.id);
136 | });
137 | }
138 |
139 | return {
140 | ...state,
141 | toasts: state.toasts.map((t) =>
142 | t.id === toastId || toastId === undefined
143 | ? {
144 | ...t,
145 | open: false,
146 | }
147 | : t
148 | ),
149 | };
150 | }
151 | case "REMOVE_TOAST":
152 | if (action.toastId === undefined) {
153 | return {
154 | ...state,
155 | toasts: [],
156 | };
157 | }
158 | return {
159 | ...state,
160 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
161 | };
162 | }
163 | };
164 |
165 | // Listeners for tracking state changes
166 | const listeners: Array<(state: State) => void> = [];
167 |
168 | // In-memory state
169 | let memoryState: State = { toasts: [] };
170 |
171 | /**
172 | * Function for dispatching actions to the reducer
173 | * @param action Action to process
174 | */
175 | function dispatch(action: Action) {
176 | memoryState = reducer(memoryState, action);
177 | listeners.forEach((listener) => {
178 | listener(memoryState);
179 | });
180 | }
181 |
182 | /**
183 | * Type for creating a toast without an identifier
184 | */
185 | type Toast = Omit;
186 |
187 | /**
188 | * Function for creating a new toast
189 | * @param props Toast properties
190 | * @returns Methods for managing the created toast
191 | */
192 | function toast({ ...props }: Toast) {
193 | const id = genId();
194 |
195 | const update = (props: ToasterToast) =>
196 | dispatch({
197 | type: "UPDATE_TOAST",
198 | toast: { ...props, id },
199 | });
200 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
201 |
202 | dispatch({
203 | type: "ADD_TOAST",
204 | toast: {
205 | ...props,
206 | id,
207 | open: true,
208 | onOpenChange: (open) => {
209 | if (!open) dismiss();
210 | },
211 | },
212 | });
213 |
214 | return {
215 | id: id,
216 | dismiss,
217 | update,
218 | };
219 | }
220 |
221 | /**
222 | * Hook for managing toast notifications
223 | * @returns Methods and state for working with toast notifications
224 | */
225 | function useToast() {
226 | const [state, setState] = React.useState(memoryState);
227 |
228 | React.useEffect(() => {
229 | listeners.push(setState);
230 | return () => {
231 | const index = listeners.indexOf(setState);
232 | if (index > -1) {
233 | listeners.splice(index, 1);
234 | }
235 | };
236 | }, [state]);
237 |
238 | return {
239 | ...state,
240 | toast,
241 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
242 | };
243 | }
244 |
245 | export { useToast, toast };
246 |
--------------------------------------------------------------------------------
/apps/demo-dapp/src/components/TestGroup.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2 | import { TestGroup as TestGroupType, TestCase, TestStatus } from "@/types/test-case";
3 | import { FolderOpen, CheckCircle } from "lucide-react";
4 | import { Progress } from "@/components/ui/progress";
5 | import { TestCaseFactory } from "@/test-cases/TestCaseFactory";
6 | import { cn } from "@/lib/utils";
7 |
8 | interface TestGroupProps {
9 | group: TestGroupType;
10 | allGroups: TestGroupType[];
11 | onTestAction: (testId: string, action: 'retry' | 'yes' | 'no') => void;
12 | onTestStatusChange: (testId: string, status: TestStatus) => void;
13 | }
14 |
15 | export function TestGroup({ group, allGroups, onTestStatusChange }: TestGroupProps) {
16 | const completedTests = group.cases.filter(test => test.status === 'success').length;
17 | const progress = (completedTests / group.cases.length) * 100;
18 |
19 | // Get all test cases from all groups
20 | const allTestCases = allGroups.flatMap(g => g.cases);
21 |
22 | // Check if all previous tests in dependencies are completed and ensure sequential execution
23 | const isTestLocked = (test: TestCase, index: number): boolean => {
24 | // If it's not the first test in the group, check if all previous tests in this group are completed
25 | if (index > 0) {
26 | const previousTests = group.cases.slice(0, index);
27 | const allPreviousCompleted = previousTests.every(t =>
28 | t.status === 'success' ||
29 | (t.isOptional && t.status === 'failure')
30 | );
31 | if (!allPreviousCompleted) return true;
32 | }
33 |
34 | // Then check specific dependencies across all groups
35 | if (!test.dependencies?.length) {
36 | return false;
37 | }
38 |
39 | const allDependenciesMet = test.dependencies.every(depId =>
40 | allTestCases.some(t => t.id === depId && (
41 | t.status === 'success' ||
42 | (t.isOptional && t.status === 'failure')
43 | ))
44 | );
45 |
46 | return !allDependenciesMet;
47 | };
48 |
49 | // Determine the border color based on test statuses
50 | const getBorderColorClass = (): string => {
51 | const failedTestCount = group.cases.filter(test => test.status === 'failure').length;
52 | const runningTestCount = group.cases.filter(test => test.status === 'running').length;
53 | const pendingTestCount = group.cases.filter(test => test.status === 'pending').length;
54 | const lockedTestCount = group.cases.filter((test, index) => isTestLocked(test, index)).length;
55 |
56 | if (completedTests === group.cases.length) {
57 | return "border-t-green-500"; // All tests passed
58 | } else if (failedTestCount > 0) {
59 | return "border-t-red-500"; // Some tests failed
60 | } else if (lockedTestCount === group.cases.length) {
61 | return "border-t-gray-500"; // All tests are locked
62 | } else if (runningTestCount > 0 || pendingTestCount > 0) {
63 | return "border-t-blue-500"; // Tests are running or waiting for user action
64 | } else {
65 | return "border-t-blue-500"; // Default color
66 | }
67 | };
68 |
69 | // Determine the icon color based on test statuses
70 | const getIconColorClass = (): string => {
71 | const failedTestCount = group.cases.filter(test => test.status === 'failure').length;
72 | const runningTestCount = group.cases.filter(test => test.status === 'running').length;
73 | const pendingTestCount = group.cases.filter(test => test.status === 'pending').length;
74 | const lockedTestCount = group.cases.filter((test, index) => isTestLocked(test, index)).length;
75 |
76 | if (completedTests === group.cases.length) {
77 | return "text-green-500"; // All tests passed
78 | } else if (failedTestCount > 0) {
79 | return "text-red-500"; // Some tests failed
80 | } else if (lockedTestCount === group.cases.length) {
81 | return "text-gray-500"; // All tests are locked
82 | } else if (runningTestCount > 0 || pendingTestCount > 0) {
83 | return "text-blue-500"; // Tests are running or waiting for user action
84 | } else {
85 | return "text-blue-500"; // Default color
86 | }
87 | };
88 |
89 | // Determine the progress bar color based on test statuses
90 | const getProgressColorClass = (): string => {
91 | const failedTestCount = group.cases.filter(test => test.status === 'failure').length;
92 | const runningTestCount = group.cases.filter(test => test.status === 'running').length;
93 | const pendingTestCount = group.cases.filter(test => test.status === 'pending').length;
94 | const lockedTestCount = group.cases.filter((test, index) => isTestLocked(test, index)).length;
95 |
96 | if (completedTests === group.cases.length) {
97 | return "bg-green-500"; // All tests passed
98 | } else if (failedTestCount > 0) {
99 | return "bg-red-500"; // Some tests failed
100 | } else if (lockedTestCount === group.cases.length) {
101 | return "bg-gray-500"; // All tests are locked
102 | } else if (runningTestCount > 0 || pendingTestCount > 0) {
103 | return "bg-blue-500"; // Tests are running or waiting for user action
104 | } else {
105 | return "bg-blue-500"; // Default color
106 | }
107 | };
108 |
109 | return (
110 |
111 |
112 |
113 |
114 |
115 | {group.title}
116 |
117 |
118 |
119 | {completedTests} of {group.cases.length} completed
120 |
121 |
122 | {group.description}
123 |
128 |
129 |
130 |
131 | {group.cases.map((testCase, index) => {
132 | const locked = isTestLocked(testCase, index);
133 | return (
134 |
141 | );
142 | })}
143 |
144 |
145 |
146 | );
147 | }
--------------------------------------------------------------------------------
/apps/demo-dapp/src/test-cases/EcBalanceCheckTest.tsx:
--------------------------------------------------------------------------------
1 | import { BaseTestCase, BaseTestCaseProps } from "./BaseTestCase";
2 | import { useTonConnect } from "@/hooks/use-ton-connect";
3 | import { useEffect, useState, ReactNode } from "react";
4 | import { Alert, AlertDescription } from "@/components/ui/alert";
5 | import { Button } from "@/components/ui/button";
6 | import { RotateCw } from "lucide-react";
7 | import { Address } from "@ton/ton";
8 | import { getAccountState } from "@/lib/ton-client";
9 | import { TestStepsList } from "@/components/test-steps/TestStepsList";
10 | import { useTestSteps } from "@/hooks/use-test-steps";
11 | import { TestStep } from "@/types/test-steps";
12 | import { AccountLink } from "@/components/ton/links";
13 | import { ECAmount } from "@/components/ton";
14 | import { EC_DECIMALS } from "@/constants";
15 |
16 | /**
17 | * Constants for EC token configuration
18 | */
19 | const EC_TOKEN_ID = 100;
20 | const MIN_REQUIRED_BALANCE = BigInt(0.01 * 10 ** EC_DECIMALS); // Minimum sent amount for test
21 |
22 | /**
23 | * Step identifiers used throughout the component
24 | */
25 | const STEP_ID = {
26 | CHECK_BALANCE: 'check-balance'
27 | } as const;
28 |
29 | /**
30 | * Default test steps configuration
31 | */
32 | const DEFAULT_STEPS: TestStep[] = [
33 | {
34 | id: STEP_ID.CHECK_BALANCE,
35 | title: 'EC Balance Check',
36 | description: 'Checking if your wallet has enough Extra Currency for testing',
37 | type: 'auto-check',
38 | status: 'pending'
39 | }
40 | ];
41 |
42 | /**
43 | * Component for testing if a wallet has enough Extra Currency for further tests
44 | */
45 | export function EcBalanceCheckTest(props: BaseTestCaseProps) {
46 | const { userAddress } = useTonConnect();
47 | const [ecBalance, setEcBalance] = useState(null);
48 | const [isLoading, setIsLoading] = useState(false);
49 |
50 | const {
51 | steps,
52 | updateStepDetails,
53 | setStepSuccess,
54 | setStepFailure,
55 | setStepRunning,
56 | setAllStepsStatus
57 | } = useTestSteps({
58 | defaultSteps: DEFAULT_STEPS,
59 | testStatus: props.testCase.status
60 | });
61 |
62 | /**
63 | * Checks the user's Extra Currency balance and updates the test status
64 | * Updates test step status based on the balance result
65 | */
66 | const checkEcBalance = async () => {
67 | if (!userAddress) return;
68 |
69 | setIsLoading(true);
70 | setStepRunning(STEP_ID.CHECK_BALANCE);
71 |
72 | try {
73 | const account = await getAccountState(Address.parse(userAddress));
74 | const currentBalance = BigInt(account.account.balance.currencies[EC_TOKEN_ID] || 0n);
75 | setEcBalance(currentBalance);
76 |
77 | const hasEnoughBalance = currentBalance >= MIN_REQUIRED_BALANCE;
78 |
79 | if (hasEnoughBalance) {
80 | setStepSuccess(STEP_ID.CHECK_BALANCE);
81 | props.onStatusChange?.('success');
82 | } else {
83 | setStepFailure(STEP_ID.CHECK_BALANCE);
84 | props.onStatusChange?.('failure');
85 | }
86 | } catch (error) {
87 | console.error('Failed to fetch EC balance:', error);
88 | setStepFailure(STEP_ID.CHECK_BALANCE);
89 | props.onStatusChange?.('failure');
90 | } finally {
91 | setIsLoading(false);
92 | }
93 | };
94 |
95 | /**
96 | * Initialize test and check balance when wallet is connected
97 | */
98 | useEffect(() => {
99 | if (!userAddress) {
100 | setAllStepsStatus('pending');
101 | props.onStatusChange?.('pending');
102 | return;
103 | }
104 |
105 | if (props.testCase.status === 'pending') {
106 | checkEcBalance();
107 | }
108 | }, [userAddress, props.testCase.status]);
109 |
110 | /**
111 | * Renders success message when user has enough EC balance
112 | */
113 | const renderSuccessMessage = (): ReactNode => (
114 |
115 | ✓ Your wallet has enough Extra Currency
116 |
117 | );
118 |
119 | /**
120 | * Renders error message when user doesn't have enough EC balance
121 | */
122 | const renderInsufficientBalanceMessage = (): ReactNode => (
123 |
124 |
125 | Not enough Extra Currency:
126 |
127 |
128 | Your balance:
129 | Minimum needed:
130 | Complete the "Get Test EC" step to get test tokens
131 |
132 |
133 |
134 |
135 | );
136 |
137 | /**
138 | * Renders balance details and status information
139 | */
140 | const renderBalanceDetails = (): ReactNode => {
141 | const hasEnoughBalance = ecBalance !== null && ecBalance >= MIN_REQUIRED_BALANCE;
142 |
143 | return (
144 |
145 |
146 |
Current balance:
147 |
148 |
149 | {hasEnoughBalance ? renderSuccessMessage() : renderInsufficientBalanceMessage()}
150 |
151 |
158 |
159 | Refresh Balance
160 |
161 |
162 |
163 | );
164 | };
165 |
166 | /**
167 | * Renders the content for the balance check step
168 | */
169 | const renderBalanceCheckContent = (): ReactNode => {
170 | return (
171 |
172 |
173 |
174 | Checking your Extra Currency balance:
175 |
176 | Minimum required:
177 | This balance is required to run test operations
178 |
179 |
180 |
181 | {ecBalance !== null && renderBalanceDetails()}
182 |
183 | );
184 | };
185 |
186 | /**
187 | * Update step UI based on current balance state
188 | */
189 | useEffect(() => {
190 | updateStepDetails(STEP_ID.CHECK_BALANCE, renderBalanceCheckContent());
191 | }, [props.testCase.status, ecBalance, userAddress, isLoading]);
192 |
193 | return (
194 |
195 |
196 |
197 |
198 |
199 | );
200 | }
--------------------------------------------------------------------------------