-
-
- {
- const newEnvironment = e.target.value as Environment
- setEnvironment(newEnvironment)
- setSourceChain("")
- setDestinationChain("")
- handleSourceChange("")
- handleDestinationChange("")
- }}
- >
- Testnet Environment
- Mainnet Environment
-
-
+
+
+
+
+ handleEnvironmentChange(Environment.Testnet)}
+ aria-pressed={state.environment === Environment.Testnet}
+ >
+ 🔧
+ Testnet
+
+ handleEnvironmentChange(Environment.Mainnet)}
+ aria-pressed={state.environment === Environment.Mainnet}
+ >
+ 🌐
+ Mainnet
+
+
-
- {
- setSourceChain(e.target.value)
- handleSourceChange(e.target.value)
- if (e.target.value === destinationChain) {
- setDestinationChain("")
- handleDestinationChange("")
- }
- }}
- >
- Source Blockchain
- {networks.map((network) => (
-
- {network.name}
-
- ))}
-
-
+
+
+
+
-
- {
- setDestinationChain(e.target.value)
- handleDestinationChange(e.target.value)
- }}
- >
- Destination Blockchain
- {networks
- .filter((n) => n.chain !== sourceChain)
- .map((network) => (
-
- {network.name}
-
- ))}
-
+
+
-
-
+
+
)
}
diff --git a/src/components/CCIP/TutorialBlockchainSelector/index.ts b/src/components/CCIP/TutorialBlockchainSelector/index.ts
index 48f75912836..9104980a71c 100644
--- a/src/components/CCIP/TutorialBlockchainSelector/index.ts
+++ b/src/components/CCIP/TutorialBlockchainSelector/index.ts
@@ -4,3 +4,13 @@ export { ContractAddress } from "./ContractAddress"
export { NetworkAddress } from "./NetworkAddress"
export { StoredContractAddress } from "./StoredContractAddress"
export { ChainUpdateBuilder } from "./ChainUpdateBuilder"
+export { ChainUpdateBuilderWrapper } from "./ChainUpdateBuilderWrapper"
+export { PoolConfigVerification } from "./PoolConfigVerification"
+export { DeployTokenStep } from "./DeployTokenStep"
+export { AdminSetupStep } from "./AdminSetupStep"
+export { DeployPoolStep } from "./DeployPoolStep"
+export { SetPoolStep } from "./SetPoolStep"
+export { GrantPrivilegesStep } from "./GrantPrivilegesStep"
+
+// Re-export types from the correct location
+export type { Network } from "@config/data/ccip/types"
diff --git a/src/components/CCIP/TutorialProgress/ChainSelectorDisplay.module.css b/src/components/CCIP/TutorialProgress/ChainSelectorDisplay.module.css
new file mode 100644
index 00000000000..b934dcd0b5e
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/ChainSelectorDisplay.module.css
@@ -0,0 +1,66 @@
+.selectorContainer {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.9em;
+ padding: 4px 8px;
+ border-radius: 6px;
+ transition: all 0.15s ease;
+ position: relative;
+ cursor: help;
+ background: rgba(255, 255, 255, 0.1);
+ isolation: isolate;
+ display: inline-block;
+}
+
+.selectorContainer:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.selectorText {
+ display: inline;
+ white-space: nowrap;
+}
+
+.selectorContainer[data-tooltip] {
+ position: relative;
+}
+
+.selectorContainer[data-tooltip]::before {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 6px 10px;
+ background: rgba(0, 0, 0, 0.85);
+ color: white;
+ font-size: 0.85em;
+ border-radius: 6px;
+ white-space: nowrap;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.15s ease;
+ pointer-events: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ z-index: 100;
+}
+
+.selectorContainer[data-tooltip]::after {
+ content: "";
+ position: absolute;
+ bottom: calc(100% + 2px);
+ left: 50%;
+ transform: translateX(-50%);
+ border: 6px solid transparent;
+ border-top-color: rgba(0, 0, 0, 0.85);
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.15s ease;
+ pointer-events: none;
+ z-index: 100;
+}
+
+.selectorContainer[data-tooltip]:hover::before,
+.selectorContainer[data-tooltip]:hover::after {
+ opacity: 1;
+ visibility: visible;
+}
diff --git a/src/components/CCIP/TutorialProgress/ChainSelectorDisplay.tsx b/src/components/CCIP/TutorialProgress/ChainSelectorDisplay.tsx
new file mode 100644
index 00000000000..5b1d51c3e0e
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/ChainSelectorDisplay.tsx
@@ -0,0 +1,20 @@
+import styles from "./ChainSelectorDisplay.module.css"
+
+interface ChainSelectorDisplayProps {
+ selector: string
+}
+
+export const ChainSelectorDisplay = ({ selector }: ChainSelectorDisplayProps) => {
+ const truncateChainSelector = (selector: string) => {
+ if (!selector) return ""
+ const prefix = selector.slice(0, 4)
+ const suffix = selector.slice(-4)
+ return `${prefix}...${suffix}`
+ }
+
+ return (
+
+ {truncateChainSelector(selector)}
+
+ )
+}
diff --git a/src/components/CCIP/TutorialProgress/ContractAddressDisplay.module.css b/src/components/CCIP/TutorialProgress/ContractAddressDisplay.module.css
new file mode 100644
index 00000000000..0ae273f5315
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/ContractAddressDisplay.module.css
@@ -0,0 +1,66 @@
+.addressContainer {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.9em;
+ padding: 4px 8px;
+ border-radius: 6px;
+ transition: all 0.15s ease;
+ position: relative;
+ cursor: help;
+ background: rgba(255, 255, 255, 0.1);
+ isolation: isolate;
+ display: inline-block;
+}
+
+.addressContainer:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+.addressText {
+ display: inline;
+ white-space: nowrap;
+}
+
+.addressContainer[data-tooltip] {
+ position: relative;
+}
+
+.addressContainer[data-tooltip]::before {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 6px 10px;
+ background: rgba(0, 0, 0, 0.85);
+ color: white;
+ font-size: 0.85em;
+ border-radius: 6px;
+ white-space: nowrap;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.15s ease;
+ pointer-events: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ z-index: 100;
+}
+
+.addressContainer[data-tooltip]::after {
+ content: "";
+ position: absolute;
+ bottom: calc(100% + 2px);
+ left: 50%;
+ transform: translateX(-50%);
+ border: 6px solid transparent;
+ border-top-color: rgba(0, 0, 0, 0.85);
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.15s ease;
+ pointer-events: none;
+ z-index: 100;
+}
+
+.addressContainer[data-tooltip]:hover::before,
+.addressContainer[data-tooltip]:hover::after {
+ opacity: 1;
+ visibility: visible;
+}
diff --git a/src/components/CCIP/TutorialProgress/ContractAddressDisplay.tsx b/src/components/CCIP/TutorialProgress/ContractAddressDisplay.tsx
new file mode 100644
index 00000000000..d9a8299b2f1
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/ContractAddressDisplay.tsx
@@ -0,0 +1,18 @@
+import styles from "./ContractAddressDisplay.module.css"
+
+interface ContractAddressDisplayProps {
+ address: string
+}
+
+export const ContractAddressDisplay = ({ address }: ContractAddressDisplayProps) => {
+ const truncateAddress = (address: string) => {
+ if (!address) return ""
+ return `${address.slice(0, 6)}...${address.slice(-4)}`
+ }
+
+ return (
+
+ {truncateAddress(address)}
+
+ )
+}
diff --git a/src/components/CCIP/TutorialProgress/SectionProgress.module.css b/src/components/CCIP/TutorialProgress/SectionProgress.module.css
new file mode 100644
index 00000000000..d4164997667
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/SectionProgress.module.css
@@ -0,0 +1,101 @@
+.progressContainer {
+ margin-bottom: 2rem;
+ padding: 1.5rem;
+ background: #f8fafc;
+ border-radius: 12px;
+}
+
+.progressHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.sectionTitle {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #1e293b;
+}
+
+.stepCount {
+ color: #64748b;
+ font-size: 0.875rem;
+}
+
+.progressBar {
+ height: 8px;
+ background: transparent;
+ border-radius: 4px;
+ overflow: hidden;
+ display: flex;
+ gap: 2px;
+}
+
+.progressSegment {
+ height: 100%;
+ border: none;
+ padding: 0;
+ margin: 0;
+ background: #e2e8f0;
+ transition: all 0.2s ease;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+}
+
+.progressSegment::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.1);
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.progressSegment:hover::after {
+ opacity: 1;
+}
+
+.progressSegment.completed {
+ background: #375bd2;
+}
+
+.progressSegment.in-progress {
+ background: #eab308;
+}
+
+.progressSegment.not-started {
+ background: #e2e8f0;
+}
+
+.progressSegment.active {
+ transform: scaleY(1.2);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+/* Tooltip styles */
+.progressSegment:hover::before {
+ content: attr(aria-label);
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ white-space: nowrap;
+ pointer-events: none;
+ z-index: 10;
+}
+
+/* Focus styles for accessibility */
+.progressSegment:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #375bd2;
+}
diff --git a/src/components/CCIP/TutorialProgress/SectionProgress.tsx b/src/components/CCIP/TutorialProgress/SectionProgress.tsx
new file mode 100644
index 00000000000..2bbc9b3361d
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/SectionProgress.tsx
@@ -0,0 +1,43 @@
+import styles from "./SectionProgress.module.css"
+import { navigateToStep, type StepId } from "@stores/lanes"
+import { useMemo } from "react"
+
+interface SectionProgressProps {
+ currentStep: number
+ totalSteps: number
+ sectionTitle: string
+ steps: Array<{ id: StepId; stepNumber: number; status: string }>
+}
+
+export const SectionProgress = ({ currentStep, totalSteps, sectionTitle, steps }: SectionProgressProps) => {
+ const segments = useMemo(() => {
+ return steps.map((step) => ({
+ ...step,
+ width: (1 / totalSteps) * 100,
+ isActive: step.stepNumber === currentStep,
+ }))
+ }, [steps, currentStep, totalSteps])
+
+ return (
+
+
+
{sectionTitle}
+
+ Step {currentStep} of {totalSteps}
+
+
+
+ {segments.map((segment) => (
+ navigateToStep(segment.id)}
+ aria-label={`Go to step ${segment.stepNumber}`}
+ aria-current={segment.isActive ? "step" : undefined}
+ />
+ ))}
+
+
+ )
+}
diff --git a/src/components/CCIP/TutorialProgress/StepCheckbox.css b/src/components/CCIP/TutorialProgress/StepCheckbox.css
new file mode 100644
index 00000000000..46a71d4de7b
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/StepCheckbox.css
@@ -0,0 +1,100 @@
+.step-checkbox {
+ margin: 1.5rem 0;
+ padding: 1.25rem 1.5rem;
+ border-radius: 12px;
+ background: #f8fafc;
+ border: 2px solid #e2e8f0;
+ box-shadow: 0 2px 4px rgba(55, 91, 210, 0.05);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+}
+
+.step-checkbox:hover {
+ background: #f1f5f9;
+ border-color: #375bd2;
+ transform: translateY(-2px);
+ box-shadow: 0 6px 12px rgba(55, 91, 210, 0.1);
+}
+
+.step-checkbox label {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ cursor: pointer;
+ font-weight: 600;
+}
+
+.step-checkbox input[type="checkbox"] {
+ appearance: none;
+ width: 24px;
+ height: 24px;
+ border: 2.5px solid #94a3b8;
+ border-radius: 8px;
+ position: relative;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ background: white;
+}
+
+.step-checkbox input[type="checkbox"]:hover {
+ border-color: #375bd2;
+ box-shadow: 0 0 0 4px rgba(55, 91, 210, 0.1);
+}
+
+.step-checkbox input[type="checkbox"]:checked {
+ background: #375bd2;
+ border-color: #375bd2;
+ animation: checkbox-pop 0.3s ease-out;
+}
+
+.step-checkbox input[type="checkbox"]:checked::after {
+ content: "✓";
+ color: white;
+ position: absolute;
+ top: 45%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 1.125rem;
+ font-weight: bold;
+}
+
+.step-checkbox span {
+ font-size: 1.125rem;
+ color: #1e293b;
+ line-height: 1.4;
+}
+
+/* Add completion indicator */
+.step-checkbox:has(input:checked) {
+ border-color: #375bd2;
+ background: linear-gradient(to right, rgba(55, 91, 210, 0.05), transparent);
+}
+
+.step-checkbox:has(input:checked) span {
+ color: #375bd2;
+}
+
+/* Add visual feedback animation */
+@keyframes checkbox-pop {
+ 0% {
+ transform: scale(0.8);
+ }
+ 50% {
+ transform: scale(1.1);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+/* Add completion badge */
+.step-checkbox:has(input:checked)::after {
+ content: "Completed!";
+ position: absolute;
+ top: 0.75rem;
+ right: 1rem;
+ font-size: 0.875rem;
+ color: #375bd2;
+ font-weight: 600;
+ opacity: 0.8;
+}
diff --git a/src/components/CCIP/TutorialProgress/StepCheckbox.tsx b/src/components/CCIP/TutorialProgress/StepCheckbox.tsx
new file mode 100644
index 00000000000..db3d633d4cd
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/StepCheckbox.tsx
@@ -0,0 +1,125 @@
+import { updateStepProgress, type StepId, TUTORIAL_STEPS, subscribeToStepProgress } from "@stores/lanes"
+import { useCallback, memo, useMemo, useEffect, useRef } from "react"
+
+interface StepCheckboxProps
{
+ stepId: T
+ subStepId: T extends "sourceChain"
+ ? "token-deployed" | "admin-claimed" | "admin-accepted" | "pool-deployed" | "pool-registered"
+ : T extends "destinationChain"
+ ? "dest-token-deployed" | "admin-claimed" | "admin-accepted" | "dest-pool-deployed" | "dest-pool-registered"
+ : string
+ label?: string
+ onChange?: (checked: boolean) => void
+}
+
+// Controlled checkbox component that manages its own animation state
+const StepCheckboxBase = ({ stepId, subStepId, label, onChange }: StepCheckboxProps) => {
+ const checkboxRef = useRef(null)
+ const isAnimating = useRef(false)
+ const isUserAction = useRef(false)
+ const currentValue = useRef(false)
+
+ // Subscribe to store changes with proper filtering
+ useEffect(() => {
+ const unsubscribe = subscribeToStepProgress(stepId, (progress) => {
+ const newValue = progress[subStepId] ?? false
+
+ // Skip update if value hasn't changed
+ if (newValue === currentValue.current) {
+ return
+ }
+ currentValue.current = newValue
+
+ // Only update DOM directly if it's not a user action
+ if (!isUserAction.current && checkboxRef.current) {
+ checkboxRef.current.checked = newValue
+ if (newValue && !isAnimating.current) {
+ isAnimating.current = true
+ checkboxRef.current.parentElement?.classList.add("animating")
+ setTimeout(() => {
+ if (checkboxRef.current?.parentElement) {
+ checkboxRef.current.parentElement.classList.remove("animating")
+ isAnimating.current = false
+ }
+ }, 300)
+ }
+ }
+ isUserAction.current = false
+ })
+ return unsubscribe
+ }, [stepId, subStepId])
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const newValue = e.target.checked
+ isUserAction.current = true
+ currentValue.current = newValue
+
+ if (process.env.NODE_ENV === "development") {
+ console.log(`[CheckboxChange] ${stepId}.${subStepId}:`, {
+ newValue,
+ timestamp: new Date().toISOString(),
+ })
+ }
+
+ // Start animation if checked
+ if (newValue) {
+ isAnimating.current = true
+ e.target.parentElement?.classList.add("animating")
+ setTimeout(() => {
+ if (checkboxRef.current?.parentElement) {
+ checkboxRef.current.parentElement.classList.remove("animating")
+ isAnimating.current = false
+ }
+ }, 300)
+ }
+
+ // Update store
+ if (onChange) {
+ onChange(newValue)
+ } else {
+ updateStepProgress(stepId.toString(), subStepId.toString(), newValue)
+ }
+ },
+ [onChange, stepId, subStepId]
+ )
+
+ const ariaLabel = useMemo(
+ () => `Mark ${label || TUTORIAL_STEPS[stepId]?.subSteps?.[subStepId as string] || subStepId} as complete`,
+ [label, stepId, subStepId]
+ )
+
+ const displayLabel = useMemo(
+ () => label || TUTORIAL_STEPS[stepId]?.subSteps?.[subStepId as string] || subStepId,
+ [label, stepId, subStepId]
+ )
+
+ if (process.env.NODE_ENV === "development") {
+ console.log(`[RenderTrack] StepCheckbox-${stepId}-${subStepId} rendered`)
+ }
+
+ return (
+
+
+
+ {displayLabel}
+
+
+ )
+}
+
+// Memoized version with prop comparison
+export const StepCheckbox = memo(StepCheckboxBase, (prev, next) => {
+ return (
+ prev.stepId === next.stepId &&
+ prev.subStepId === next.subStepId &&
+ prev.label === next.label &&
+ prev.onChange === next.onChange
+ )
+})
diff --git a/src/components/CCIP/TutorialProgress/SubStepTracker.tsx b/src/components/CCIP/TutorialProgress/SubStepTracker.tsx
new file mode 100644
index 00000000000..cd82d7fd537
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/SubStepTracker.tsx
@@ -0,0 +1,22 @@
+import { SubStep } from "./types"
+
+interface SubStepTrackerProps {
+ steps: SubStep[]
+ onComplete: (stepId: string, completed: boolean) => void
+}
+
+export const SubStepTracker = ({ steps, onComplete }: SubStepTrackerProps) => {
+ return (
+
+ {steps.map((step) => (
+
+
+ onComplete(step.id, e.target.checked)} />
+ ✓
+
+ {step.title}
+
+ ))}
+
+ )
+}
diff --git a/src/components/CCIP/TutorialProgress/TutorialProgress.module.css b/src/components/CCIP/TutorialProgress/TutorialProgress.module.css
new file mode 100644
index 00000000000..84b1dd1d5c4
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/TutorialProgress.module.css
@@ -0,0 +1,363 @@
+/* Base container */
+.tutorialProgress {
+ position: sticky;
+ top: 0;
+ height: fit-content;
+ padding: var(--doc-padding) 0;
+ min-width: calc(315px - var(--space-16x));
+ isolation: isolate;
+}
+
+/* Override any parent title tooltips */
+.tutorialProgress::before,
+.tutorialProgress::after {
+ display: none !important;
+}
+
+.title {
+ font-family: var(--font-display);
+ font-size: var(--font-size-lg);
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: var(--space-4x);
+ pointer-events: none;
+}
+
+.sectionTitle {
+ font-family: var(--font-display);
+ font-size: var(--font-size-base);
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: var(--space-4x);
+ padding-bottom: var(--space-2x);
+ border-bottom: 1px solid var(--color-border);
+ pointer-events: none;
+}
+
+/* Progress Steps */
+.progressSteps {
+ margin-bottom: 24px;
+}
+
+.steps {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-left: 16px;
+}
+
+.stepContainer {
+ display: flex;
+ flex-direction: column;
+ border-left: 2px solid var(--color-border-primary);
+ margin-left: 15px;
+ padding-left: 15px;
+ transition: all 0.3s ease;
+}
+
+.stepContainer.completed {
+ border-left-color: #16a34a;
+}
+
+.stepContainer.in-progress {
+ border-left-color: #ea580c;
+}
+
+.step {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 12px;
+ background: var(--color-background);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ width: 100%;
+}
+
+.step:hover {
+ transform: translateX(4px);
+ background: var(--color-background-secondary);
+}
+
+.stepIndicator {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ background: var(--color-background-secondary);
+ color: var(--color-text-secondary);
+ border: 2px solid var(--color-border);
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+}
+
+.completed .stepIndicator {
+ background: #dcfce7;
+ border-color: #16a34a;
+ color: #16a34a;
+}
+
+.in-progress .stepIndicator {
+ background: #fef3c7;
+ border-color: #eab308;
+ color: #b45309;
+}
+
+.stepTitle {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+ flex: 1;
+}
+
+.step.in-progress .stepTitle {
+ color: #ea580c;
+}
+
+.chevron {
+ width: 16px;
+ height: 16px;
+ border-right: 2px solid var(--color-text-secondary);
+ border-bottom: 2px solid var(--color-text-secondary);
+ transform: rotate(45deg);
+ transition: transform 0.2s ease;
+ flex-shrink: 0;
+}
+
+.expanded .chevron {
+ transform: rotate(-135deg);
+}
+
+.stepDetails {
+ margin-left: 40px;
+ padding: 8px 0;
+ border-left: 2px solid var(--color-border-primary);
+}
+
+.substep {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+ background: rgba(255, 255, 255, 0.03);
+ margin: 4px 0;
+ width: 100%;
+ text-align: left;
+ border: none;
+ cursor: pointer;
+}
+
+.substep:hover {
+ background: rgba(255, 255, 255, 0.08);
+ transform: translateX(4px);
+}
+
+.substep:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba(55, 91, 210, 0.4);
+}
+
+.substep.completed {
+ background: rgba(22, 163, 74, 0.1);
+ padding-right: 32px;
+ position: relative;
+}
+
+.substep.completed::after {
+ content: "✓";
+ position: absolute;
+ right: 12px;
+ color: #16a34a;
+ font-weight: bold;
+}
+
+.substepTitle {
+ font-size: 0.9rem;
+ color: var(--color-text-secondary);
+ pointer-events: none;
+}
+
+.substep.completed .substepTitle {
+ color: #16a34a;
+ font-weight: 500;
+}
+
+/* Chain section styles */
+.chainBlock {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 12px;
+ padding: 16px;
+ margin-bottom: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ transition: all 0.2s ease;
+}
+
+.chainHeader {
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.chainIdentity {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.chainLogo {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px solid rgba(255, 255, 255, 0.1);
+ transition: all 0.2s ease;
+ background: rgba(255, 255, 255, 0.1);
+ flex-shrink: 0;
+}
+
+.chainLogo img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.chainLogo:not([src]),
+.chainLogo[src=""],
+.chainLogo[src="undefined"] {
+ background: rgba(255, 255, 255, 0.1);
+ border: 2px solid rgba(255, 255, 255, 0.15);
+}
+
+.chainName {
+ font-weight: 600;
+ color: var(--color-text-primary);
+ font-size: 1.1em;
+ pointer-events: none;
+}
+
+.chainConfigs {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+/* Status styles */
+.statusItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 12px;
+ background: rgba(255, 255, 255, 0.03);
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ transition: all 0.2s ease;
+}
+
+.statusLabel {
+ color: var(--color-text-secondary);
+ font-size: 0.9em;
+ font-weight: 500;
+ pointer-events: none;
+}
+
+.statusValue {
+ display: flex;
+ align-items: center;
+ min-width: 0;
+}
+
+.statusValueWithAddress {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ position: relative;
+ max-width: 140px;
+}
+
+.statusCheck {
+ color: #4caf50;
+ font-weight: bold;
+ flex-shrink: 0;
+ pointer-events: none;
+}
+
+.statusPending {
+ color: var(--color-text-secondary);
+ font-size: 0.9em;
+ pointer-events: none;
+}
+
+/* Value container with tooltip */
+.valueContainer {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.9em;
+ padding: 4px 8px;
+ border-radius: 6px;
+ transition: all 0.15s ease;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100px;
+ position: relative;
+ cursor: help;
+ background: rgba(255, 255, 255, 0.1);
+ isolation: isolate;
+}
+
+.valueContainer:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
+
+/* Tooltip styles */
+.valueContainer[data-tooltip] {
+ position: relative;
+}
+
+.valueContainer[data-tooltip]::before {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 6px 10px;
+ background: rgba(0, 0, 0, 0.85);
+ color: white;
+ font-size: 0.85em;
+ border-radius: 6px;
+ white-space: nowrap;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.15s ease;
+ pointer-events: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ z-index: 100;
+}
+
+.valueContainer[data-tooltip]::after {
+ content: "";
+ position: absolute;
+ bottom: calc(100% + 2px);
+ left: 50%;
+ transform: translateX(-50%);
+ border: 6px solid transparent;
+ border-top-color: rgba(0, 0, 0, 0.85);
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.15s ease;
+ pointer-events: none;
+ z-index: 100;
+}
+
+.valueContainer[data-tooltip]:hover::before,
+.valueContainer[data-tooltip]:hover::after {
+ opacity: 1;
+ visibility: visible;
+}
diff --git a/src/components/CCIP/TutorialProgress/TutorialProgress.tsx b/src/components/CCIP/TutorialProgress/TutorialProgress.tsx
new file mode 100644
index 00000000000..fed6e94fe08
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/TutorialProgress.tsx
@@ -0,0 +1,336 @@
+import { useStore } from "@nanostores/react"
+import { useState, useEffect, useMemo, useCallback } from "react"
+import { laneStore, progressStore, TUTORIAL_STEPS, type StepId, type LaneState } from "@stores/lanes"
+import styles from "./TutorialProgress.module.css"
+import { ChainSelectorDisplay } from "./ChainSelectorDisplay"
+import { ContractAddressDisplay } from "./ContractAddressDisplay"
+import { SectionProgress } from "./SectionProgress"
+
+// Helper function to determine current step
+const determineCurrentStep = (state: Omit): number => {
+ if (!state.sourceChain || !state.destinationChain) return 1
+ if (!state.sourceContracts.token || !state.sourceContracts.tokenPool) return 2
+ if (!state.destinationContracts.token || !state.destinationContracts.tokenPool) return 3
+ if (!state.sourceContracts.configured) return 4
+ return 5
+}
+
+// Add navigation helper
+const navigateToSubStep = (stepId: StepId, subStepId: string) => {
+ // First, ensure the parent step is expanded
+ const elementId = `${stepId}-${subStepId}`
+ const element = document.getElementById(elementId)
+
+ if (element) {
+ // Expand the parent step if needed
+ laneStore.set({
+ ...laneStore.get(),
+ currentStep: stepId,
+ })
+
+ // Scroll to the element
+ element.scrollIntoView({ behavior: "smooth", block: "center" })
+
+ // Add a temporary highlight
+ element.style.transition = "background-color 0.3s ease"
+ element.style.backgroundColor = "rgba(55, 91, 210, 0.1)"
+ setTimeout(() => {
+ element.style.backgroundColor = ""
+ }, 1500)
+ }
+}
+
+export const TutorialProgress = () => {
+ const mainState = useStore(laneStore)
+ const progress = useStore(progressStore)
+ const [expandedStep, setExpandedStep] = useState(null)
+ const [forceExpanded, setForceExpanded] = useState(null)
+
+ const steps = useMemo(
+ () => [
+ { id: "setup", title: "Setup", stepNumber: 1 },
+ { id: "sourceChain", title: "Source Chain", stepNumber: 2 },
+ { id: "destinationChain", title: "Destination Chain", stepNumber: 3 },
+ { id: "sourceConfig", title: "Source Configuration", stepNumber: 4 },
+ { id: "destinationConfig", title: "Destination Configuration", stepNumber: 5 },
+ ],
+ []
+ )
+
+ const getStepStatus = useCallback(
+ (stepId: string) => {
+ const stepProgress = progress[stepId] || {}
+ const totalSubSteps = Object.keys(TUTORIAL_STEPS[stepId].subSteps).length
+ const completedSubSteps = Object.values(stepProgress).filter(Boolean).length
+
+ if (completedSubSteps === 0) return "not-started"
+ if (completedSubSteps === totalSubSteps) return "completed"
+ return "in-progress"
+ },
+ [progress]
+ )
+
+ // Calculate the highest completed step
+ const highestCompletedStep = useMemo(() => {
+ return steps.reduce((highest, step) => {
+ const status = getStepStatus(step.id)
+ if (status === "completed") {
+ return Math.max(highest, step.stepNumber)
+ }
+ return highest
+ }, 0)
+ }, [steps, getStepStatus])
+
+ // Determine the displayed step number (should be the lowest incomplete step)
+ const displayedStepNumber = useMemo(() => {
+ const inProgressStep = steps.find((step) => getStepStatus(step.id) === "in-progress")
+ if (inProgressStep) return inProgressStep.stepNumber
+
+ // If no step is in progress, show the next step after highest completed
+ const nextStep = highestCompletedStep + 1
+ return Math.min(nextStep, steps.length)
+ }, [steps, getStepStatus, highestCompletedStep])
+
+ // Use currentStep to determine which step should be expanded by default
+ const currentStepNumber = useMemo(() => determineCurrentStep(mainState), [mainState])
+
+ // Create steps data for progress bar
+ const progressSteps = useMemo(
+ () =>
+ steps.map((step) => ({
+ id: step.id as StepId,
+ stepNumber: step.stepNumber,
+ status: getStepStatus(step.id),
+ })),
+ [steps, getStepStatus]
+ )
+
+ // Auto-expand current step on initial render
+ useEffect(() => {
+ if (!expandedStep) {
+ const currentStep = steps.find((step) => step.stepNumber === currentStepNumber)
+ if (currentStep) {
+ setExpandedStep(currentStep.id)
+ setForceExpanded(currentStep.id)
+ }
+ }
+ }, [currentStepNumber, steps, expandedStep])
+
+ useEffect(() => {
+ if (forceExpanded) {
+ setExpandedStep(forceExpanded)
+ }
+ }, [forceExpanded])
+
+ const toggleStepDetails = useCallback(
+ (stepId: string) => {
+ setForceExpanded(stepId)
+ setExpandedStep(expandedStep === stepId ? null : stepId)
+ },
+ [expandedStep]
+ )
+
+ const getStepProgress = useCallback(
+ (stepId: StepId) => {
+ const stepConfig = TUTORIAL_STEPS[stepId]
+ const stepProgress = progress[stepId] || {}
+ return Object.entries(stepConfig.subSteps).map(([id, title]) => ({
+ id,
+ title,
+ completed: !!stepProgress[id],
+ }))
+ },
+ [progress]
+ )
+
+ // Update the substep rendering to be clickable
+ const renderSubSteps = useCallback(
+ (stepId: StepId) => {
+ const subSteps = getStepProgress(stepId)
+ return subSteps.map(({ id: subStepId, title, completed }) => (
+ navigateToSubStep(stepId, subStepId)}
+ aria-label={`Go to ${title}`}
+ >
+ {title}
+
+ ))
+ },
+ [getStepProgress]
+ )
+
+ return (
+
+ Tutorial Progress
+
+
+
+ {steps.map((step) => {
+ const status = getStepStatus(step.id)
+ const isCurrentStep = step.stepNumber === currentStepNumber
+ return (
+
+
+
+
toggleStepDetails(step.id)}
+ aria-expanded={expandedStep === step.id}
+ aria-controls={`details-${step.id}`}
+ >
+ {status === "completed" ? "✓" : step.stepNumber}
+ {step.title}
+
+
+
+ {expandedStep === step.id && (
+
+
{renderSubSteps(step.id as StepId)}
+
+ )}
+
+ )
+ })}
+
+
+
+
+
Configuration Status
+
+ {/* Source Chain Status */}
+
+
+
+
+ {mainState.sourceNetwork?.logo ? (
+
{
+ e.currentTarget.style.display = "none"
+ }}
+ />
+ ) : null}
+
+
{mainState.sourceNetwork?.name || "Source Chain"}
+
+
+
+
+
Chain Selector
+
+ {mainState.sourceNetwork?.chainSelector ? (
+
+ ✓
+
+
+ ) : (
+
Not Available
+ )}
+
+
+
+
Token
+
+ {mainState.sourceContracts.token ? (
+
+ ✓
+
+
+ ) : (
+
Not Deployed
+ )}
+
+
+
+
Token Pool
+
+ {mainState.sourceContracts.tokenPool ? (
+
+ ✓
+
+
+ ) : (
+
Not Deployed
+ )}
+
+
+
+
+
+ {/* Destination Chain Status */}
+
+
+
+
+ {mainState.destinationNetwork?.logo ? (
+
{
+ e.currentTarget.style.display = "none"
+ }}
+ />
+ ) : null}
+
+
{mainState.destinationNetwork?.name || "Destination Chain"}
+
+
+
+
+
Chain Selector
+
+ {mainState.destinationNetwork?.chainSelector ? (
+
+ ✓
+
+
+ ) : (
+
Not Available
+ )}
+
+
+
+
Token
+
+ {mainState.destinationContracts.token ? (
+
+ ✓
+
+
+ ) : (
+
Not Deployed
+ )}
+
+
+
+
Token Pool
+
+ {mainState.destinationContracts.tokenPool ? (
+
+ ✓
+
+
+ ) : (
+
Not Deployed
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/CCIP/TutorialProgress/types.ts b/src/components/CCIP/TutorialProgress/types.ts
new file mode 100644
index 00000000000..e73804761f9
--- /dev/null
+++ b/src/components/CCIP/TutorialProgress/types.ts
@@ -0,0 +1,17 @@
+export interface SubStep {
+ id: string
+ title: string
+ completed: boolean
+}
+
+export interface StepProgress {
+ setup: {
+ prerequisites: SubStep[]
+ chainSelection: SubStep[]
+ }
+ sourceChain: {
+ deployment: SubStep[]
+ adminSetup: SubStep[]
+ }
+ // ... rest of the interface
+}
diff --git a/src/components/CCIP/TutorialSetup/Callout.module.css b/src/components/CCIP/TutorialSetup/Callout.module.css
new file mode 100644
index 00000000000..d124a47418d
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/Callout.module.css
@@ -0,0 +1,104 @@
+.callout {
+ /* Base variables */
+ --callout-bg: var(--color-background-info);
+ --callout-text: var(--color-text-info);
+ --callout-border: var(--theme-text-light);
+
+ padding: var(--space-4x);
+ gap: var(--space-4x);
+ background-color: var(--callout-bg);
+ border: 2px dotted var(--callout-border);
+ border-radius: var(--border-radius);
+ color: var(--callout-text);
+
+ /* For forced colors mode */
+ outline: 1px solid transparent;
+
+ display: flex;
+}
+
+/* Type-specific styles */
+.caution {
+ --callout-bg: var(--color-background-warning);
+}
+
+.danger {
+ --callout-bg: var(--color-background-error);
+}
+
+.icon {
+ flex-shrink: 0;
+ width: 1.5em;
+ height: 1.5em;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.iconImage {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.content {
+ /* Typography */
+ --content-font-size: 14px;
+ --content-line-height: 1.5;
+ --content-gap: var(--space-2x);
+
+ flex: 1;
+}
+
+.title {
+ font-weight: 600;
+ text-transform: uppercase;
+ color: var(--theme-text);
+ margin-bottom: var(--space-2x);
+}
+
+.message {
+ color: var(--theme-text-light);
+}
+
+/* Content styling */
+.message p,
+.message li {
+ color: var(--theme-text-light);
+ font-size: var(--content-font-size);
+ line-height: var(--content-line-height);
+}
+
+.message p {
+ margin-bottom: 0;
+}
+
+.message p + p {
+ margin-top: var(--content-gap);
+}
+
+.message p + :is(ul, ol) {
+ margin-top: var(--space-1x);
+}
+
+.message li {
+ margin-top: var(--space-1x);
+}
+
+/* Link styling */
+.message :global(a),
+.message :global(a > code:not([class*="language"])) {
+ color: var(--color-text-link);
+ text-decoration: underline;
+}
+
+/* Dark theme support */
+:global(.theme-dark) .message :global(code:not([class*="language"])) {
+ color: var(--theme-code-text);
+}
+
+/* Focus handling */
+.callout:focus-within {
+ outline: 2px solid var(--color-focus);
+ outline-offset: 2px;
+}
diff --git a/src/components/CCIP/TutorialSetup/Callout.tsx b/src/components/CCIP/TutorialSetup/Callout.tsx
new file mode 100644
index 00000000000..9430f918847
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/Callout.tsx
@@ -0,0 +1,47 @@
+import { ReactNode } from "react"
+import infoIcon from "@components/Alert/Assets/info-icon.svg"
+import alertIcon from "@components/Alert/Assets/alert-icon.svg"
+import dangerIcon from "@components/Alert/Assets/danger-icon.svg"
+import styles from "./Callout.module.css"
+
+interface CalloutProps {
+ type?: "note" | "tip" | "caution" | "danger"
+ title?: string
+ children: ReactNode
+ className?: string
+}
+
+const CALLOUT_ICONS = {
+ note: infoIcon,
+ tip: infoIcon,
+ caution: alertIcon,
+ danger: dangerIcon,
+} as const
+
+export const Callout = ({ type = "note", title, children, className }: CalloutProps) => {
+ // Debug log
+ console.log("Callout render:", {
+ type,
+ icon: CALLOUT_ICONS[type],
+ iconSrc: CALLOUT_ICONS[type]?.src,
+ })
+
+ return (
+
+
+
+
+
+
+ {title || type.toUpperCase()}
+
+
{children}
+
+
+ )
+}
diff --git a/src/components/CCIP/TutorialSetup/ContractsImportCard.module.css b/src/components/CCIP/TutorialSetup/ContractsImportCard.module.css
new file mode 100644
index 00000000000..f5d36701995
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/ContractsImportCard.module.css
@@ -0,0 +1,52 @@
+.requirements {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.stepsList {
+ counter-reset: step;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.stepsList li {
+ counter-increment: step;
+ padding-left: 36px;
+ position: relative;
+ margin-bottom: 20px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--color-text-secondary);
+}
+
+.stepsList li:last-child {
+ margin-bottom: 0;
+}
+
+.stepsList li::before {
+ content: counter(step);
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 24px;
+ height: 24px;
+ background: var(--color-accent);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-background);
+ opacity: 0.9;
+}
+
+.codeContainer {
+ margin: 16px 0;
+ padding: 16px;
+ background: var(--color-background-secondary);
+ border-radius: 8px;
+ border: 1px solid var(--color-border);
+}
diff --git a/src/components/CCIP/TutorialSetup/ContractsImportCard.tsx b/src/components/CCIP/TutorialSetup/ContractsImportCard.tsx
new file mode 100644
index 00000000000..4468fa70ba7
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/ContractsImportCard.tsx
@@ -0,0 +1,41 @@
+import { StepCheckbox } from "@components/CCIP/TutorialProgress/StepCheckbox"
+import { CodeSampleReact } from "@components/CodeSample/CodeSampleReact"
+import styles from "./ContractsImportCard.module.css"
+import { TutorialCard } from "./TutorialCard"
+import { SetupSection } from "./SetupSection"
+import type { StepId, SubStepId } from "@stores/lanes"
+
+export const ContractsImportCard = () => {
+ // Generate unique ID for navigation
+ const stepId = "setup-contracts-imported"
+
+ return (
+
+
+
,
+ }}
+ >
+
+
+ Open the pre-configured token contract in Remix:
+
+
+
+
+ Wait a few seconds for Remix to automatically compile all contracts.
+
+
+
+
+ )
+}
diff --git a/src/components/CCIP/TutorialSetup/NetworkCheck.module.css b/src/components/CCIP/TutorialSetup/NetworkCheck.module.css
new file mode 100644
index 00000000000..7d3ea66cfcb
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/NetworkCheck.module.css
@@ -0,0 +1,19 @@
+.networkCheck {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2x);
+ padding: var(--space-4x);
+ margin-bottom: var(--space-4x);
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+}
+
+.networkCheck img {
+ width: 24px;
+ height: 24px;
+}
+
+.networkCheck strong {
+ color: var(--color-text-primary);
+}
diff --git a/src/components/CCIP/TutorialSetup/NetworkCheck.tsx b/src/components/CCIP/TutorialSetup/NetworkCheck.tsx
new file mode 100644
index 00000000000..45eddcdd7da
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/NetworkCheck.tsx
@@ -0,0 +1,17 @@
+import styles from "./NetworkCheck.module.css"
+
+interface NetworkCheckProps {
+ network: {
+ name: string
+ logo?: string
+ }
+}
+
+export const NetworkCheck = ({ network }: NetworkCheckProps) => (
+
+ {network?.logo &&
}
+
+ Ensure MetaMask is connected to {network?.name || "loading..."}
+
+
+)
diff --git a/src/components/CCIP/TutorialSetup/PrerequisitesCard.module.css b/src/components/CCIP/TutorialSetup/PrerequisitesCard.module.css
new file mode 100644
index 00000000000..b52a3754efc
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/PrerequisitesCard.module.css
@@ -0,0 +1,182 @@
+.card {
+ background: var(--color-background);
+ border-radius: 12px;
+ padding: 32px;
+ margin: 16px 0;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
+}
+
+.title {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 24px;
+}
+
+.requirements {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.step {
+ position: relative;
+ background: var(--color-background);
+ border-radius: 12px;
+ transition: all 0.3s ease;
+}
+
+.step:target {
+ background-color: rgba(55, 91, 210, 0.1);
+ border-color: rgba(55, 91, 210, 0.3);
+ box-shadow: 0 0 0 2px rgba(55, 91, 210, 0.2);
+}
+
+.step.active {
+ border-color: var(--color-primary);
+}
+
+.stepContent {
+ position: relative;
+ padding: 16px 0;
+}
+
+.expandButton {
+ position: absolute;
+ top: -40px;
+ right: 8px;
+ background: none;
+ border: none;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ padding: 8px;
+ transition: all 0.2s ease;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 4px;
+}
+
+.expandButton:hover {
+ color: var(--color-text-primary);
+ background: var(--color-background-secondary);
+}
+
+.optionsGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 20px;
+ margin-top: 16px;
+ animation: slideDown 0.3s ease;
+}
+
+.optionCard {
+ background: var(--color-background-secondary);
+ border-radius: 8px;
+ padding: 20px;
+ border: 1px solid var(--color-border);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+ transition: all 0.2s ease;
+}
+
+.optionCard:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+}
+
+.optionCard h4 {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 16px;
+}
+
+.stepsList {
+ counter-reset: step;
+ list-style: none;
+ padding: 0;
+ margin: 0 0 20px;
+}
+
+.stepsList li {
+ counter-increment: step;
+ padding-left: 36px;
+ position: relative;
+ margin-bottom: 12px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--color-text-secondary);
+}
+
+.stepsList li:last-child {
+ margin-bottom: 0;
+}
+
+.stepsList li::before {
+ content: counter(step);
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 24px;
+ height: 24px;
+ background: var(--color-accent);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-background);
+ opacity: 0.9;
+}
+
+.actionButton {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 16px;
+ background: var(--color-accent);
+ color: var(--color-background) !important;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: all 0.2s ease;
+ margin-top: 16px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.actionButton:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(var(--color-accent-rgb), 0.2);
+ filter: brightness(1.05);
+}
+
+.linkArrow {
+ font-size: 18px;
+ transition: transform 0.2s ease;
+}
+
+.actionButton:hover .linkArrow {
+ transform: translateX(4px);
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 768px) {
+ .optionsGrid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/components/CCIP/TutorialSetup/PrerequisitesCard.tsx b/src/components/CCIP/TutorialSetup/PrerequisitesCard.tsx
new file mode 100644
index 00000000000..4f1c7019832
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/PrerequisitesCard.tsx
@@ -0,0 +1,134 @@
+import { useState } from "react"
+import styles from "./PrerequisitesCard.module.css"
+import { type StepId, type SubStepId } from "@stores/lanes"
+import { StepCheckbox } from "@components/CCIP/TutorialProgress/StepCheckbox"
+import { SetupSection } from "./SetupSection"
+import { TutorialCard } from "./TutorialCard"
+
+interface PrerequisiteStep {
+ id: string
+ title: string
+ description: string
+ checkboxId: SubStepId
+ defaultOpen?: boolean
+ options?: {
+ title: string
+ steps: string[]
+ link?: {
+ text: string
+ url: string
+ }
+ }[]
+}
+
+export const PrerequisitesCard = () => {
+ const [activeStep, setActiveStep] = useState("browser-setup")
+
+ const getSubStepId = (subStepId: string) => `setup-${subStepId}`
+
+ const prerequisites: PrerequisiteStep[] = [
+ {
+ id: "browser-setup",
+ checkboxId: "browser-setup" as SubStepId,
+ title: "1. Web Browser Setup",
+ description: "Configure your browser with the required extensions and networks",
+ defaultOpen: true,
+ options: [
+ {
+ title: "Using Chainlist (Recommended)",
+ steps: [
+ "Visit Chainlist",
+ "Search for your desired blockchains",
+ 'Click "Add to MetaMask" for each blockchain',
+ ],
+ link: {
+ text: "Open Chainlist",
+ url: "https://chainlist.org",
+ },
+ },
+ {
+ title: "Manual Configuration",
+ steps: ["Open MetaMask Settings", "Select Networks", "Add Network manually"],
+ link: {
+ text: "View Guide",
+ url: "https://support.metamask.io/networks-and-sidechains/managing-networks/how-to-add-a-custom-network-rpc/",
+ },
+ },
+ ],
+ },
+ {
+ id: "gas-tokens",
+ checkboxId: "gas-tokens" as SubStepId,
+ title: "2. Native Gas Tokens",
+ description: "Acquire tokens for transaction fees",
+ options: [
+ {
+ title: "Testnet Setup",
+ steps: ["Visit blockchain-specific faucets", "Request test tokens"],
+ },
+ {
+ title: "Mainnet Setup",
+ steps: ["Acquire tokens through an exchange"],
+ },
+ ],
+ },
+ ]
+
+ return (
+
+
+ {prerequisites.map((step) => (
+
+
+
+
setActiveStep(activeStep === step.id ? null : step.id)}
+ aria-label={activeStep === step.id ? "Collapse section" : "Expand section"}
+ >
+ {activeStep === step.id ? "▼" : "▶"}
+
+ {activeStep === step.id && step.options && (
+
+ {step.options.map((option, idx) => (
+
+
{option.title}
+
+ {option.steps.map((stepText, stepIdx) => (
+ {stepText}
+ ))}
+
+ {option.link && (
+
+ {option.link.text}
+ →
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/CCIP/TutorialSetup/SetupSection.module.css b/src/components/CCIP/TutorialSetup/SetupSection.module.css
new file mode 100644
index 00000000000..26f601f2845
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/SetupSection.module.css
@@ -0,0 +1,41 @@
+.section {
+ background: var(--color-background);
+ border-radius: 12px;
+ padding: 24px;
+ margin: 16px 0;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 20px;
+}
+
+.headerContent {
+ flex: 1;
+}
+
+.headerActions {
+ margin-left: 16px;
+ padding-top: 4px;
+}
+
+.header h4 {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 8px;
+}
+
+.description {
+ font-size: 14px;
+ color: var(--color-text-secondary);
+ line-height: 1.5;
+}
+
+.content {
+ /* Maintain consistent spacing with PrerequisitesCard */
+ --section-spacing: 32px;
+}
diff --git a/src/components/CCIP/TutorialSetup/SetupSection.tsx b/src/components/CCIP/TutorialSetup/SetupSection.tsx
new file mode 100644
index 00000000000..cc445e4f500
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/SetupSection.tsx
@@ -0,0 +1,33 @@
+import { ReactNode } from "react"
+import styles from "./SetupSection.module.css"
+import { StepCheckbox } from "../TutorialProgress/StepCheckbox"
+import type { StepId, SubStepId } from "@stores/lanes"
+
+interface SetupSectionProps {
+ title: string
+ description?: string
+ children: ReactNode
+ checkbox?: {
+ stepId: StepId
+ subStepId: SubStepId
+ }
+}
+
+export const SetupSection = ({ title, description, children, checkbox }: SetupSectionProps) => {
+ return (
+
+
+
+
{title}
+ {description &&
{description}
}
+
+ {checkbox && (
+
+
+
+ )}
+
+
{children}
+
+ )
+}
diff --git a/src/components/CCIP/TutorialSetup/SolidityParam.module.css b/src/components/CCIP/TutorialSetup/SolidityParam.module.css
new file mode 100644
index 00000000000..8f06cb78cb1
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/SolidityParam.module.css
@@ -0,0 +1,90 @@
+.parameter {
+ display: grid;
+ gap: var(--space-3x);
+ align-items: start;
+
+ /* Default mobile-first layout: stacked */
+ grid-template-columns: 1fr;
+
+ /* Tablet (768px and up) */
+ @media (min-width: 768px) {
+ grid-template-columns: 120px 1fr;
+ grid-template-areas:
+ "name info"
+ "type info";
+ }
+
+ /* Desktop (1024px and up) */
+ @media (min-width: 1024px) {
+ grid-template-columns: 120px 80px 1fr;
+ grid-template-areas: "name type info";
+ }
+}
+
+.name {
+ font-size: var(--font-size-sm);
+ padding: var(--space-1x) var(--space-2x);
+ border-radius: 4px;
+ grid-area: name;
+}
+
+.type {
+ font-size: var(--font-size-sm);
+ grid-area: type;
+
+ /* On mobile/tablet, align with name */
+ @media (max-width: 1023px) {
+ margin-top: calc(-1 * var(--space-2x));
+ }
+}
+
+.info {
+ font-size: var(--font-size-sm);
+ grid-area: info;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1x);
+}
+
+/* Parent container adjustments */
+.parameters {
+ padding: var(--space-2x);
+ font-size: var(--font-size-sm);
+
+ @media (min-width: 768px) {
+ padding: var(--space-3x);
+ }
+
+ @media (min-width: 1024px) {
+ padding: var(--space-4x);
+ }
+}
+
+.parameterHelp {
+ /* Stack help items on mobile */
+ @media (max-width: 767px) {
+ .helpItem {
+ flex-direction: column;
+ gap: var(--space-1x);
+ }
+ }
+}
+
+/* Ensure text remains readable */
+.description {
+ line-height: var(--line-height-base);
+ font-size: var(--font-size-sm);
+
+ @media (min-width: 768px) {
+ font-size: var(--font-size-base);
+ }
+}
+
+/* Adjust spacing for different screen sizes */
+.example {
+ margin-top: var(--space-1x);
+
+ @media (min-width: 768px) {
+ margin-top: var(--space-2x);
+ }
+}
diff --git a/src/components/CCIP/TutorialSetup/SolidityParam.tsx b/src/components/CCIP/TutorialSetup/SolidityParam.tsx
new file mode 100644
index 00000000000..91382c78027
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/SolidityParam.tsx
@@ -0,0 +1,23 @@
+import { type ReactNode } from "react"
+import styles from "./SolidityParam.module.css"
+import { ReactCopyText } from "@components/ReactCopyText"
+
+interface SolidityParamProps {
+ name: string
+ type: string
+ description: string
+ example?: string | ReactNode
+ children?: ReactNode
+}
+
+export const SolidityParam = ({ name, type, description, example, children }: SolidityParamProps) => (
+
+
{name}
+
{type}
+
+
{description}
+ {example && (typeof example === "string" ?
: example)}
+
+ {children &&
{children}
}
+
+)
diff --git a/src/components/CCIP/TutorialSetup/TutorialCard.module.css b/src/components/CCIP/TutorialSetup/TutorialCard.module.css
new file mode 100644
index 00000000000..c2846a8e9c6
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/TutorialCard.module.css
@@ -0,0 +1,52 @@
+.card {
+ background: var(--color-background);
+ border-radius: 12px;
+ padding: 24px;
+ margin: 16px 0;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--color-border);
+ transition: all 0.2s ease-in-out;
+ position: relative;
+}
+
+.card:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 6px 16px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.04);
+}
+
+.card::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 3px;
+ height: 100%;
+ background: var(--color-accent);
+ border-radius: 3px 0 0 3px;
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+}
+
+.card:hover::before {
+ opacity: 1;
+}
+
+.title {
+ font-family: var(--font-display);
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: 8px;
+}
+
+.description {
+ font-family: var(--font-body);
+ color: var(--color-text-secondary);
+ font-size: 14px;
+ line-height: 1.5;
+ margin-bottom: 16px;
+}
+
+.content {
+ margin-top: var(--space-4x);
+}
diff --git a/src/components/CCIP/TutorialSetup/TutorialCard.tsx b/src/components/CCIP/TutorialSetup/TutorialCard.tsx
new file mode 100644
index 00000000000..45121ab96ef
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/TutorialCard.tsx
@@ -0,0 +1,17 @@
+import styles from "./TutorialCard.module.css"
+
+interface TutorialCardProps {
+ title: string
+ description?: string
+ children: React.ReactNode
+}
+
+export const TutorialCard = ({ title, description, children }: TutorialCardProps) => {
+ return (
+
+
{title}
+ {description &&
{description}
}
+
{children}
+
+ )
+}
diff --git a/src/components/CCIP/TutorialSetup/TutorialStep.module.css b/src/components/CCIP/TutorialSetup/TutorialStep.module.css
new file mode 100644
index 00000000000..8b4db9def53
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/TutorialStep.module.css
@@ -0,0 +1,37 @@
+.step {
+ position: relative;
+ padding: 1.5rem;
+ border-radius: 12px;
+ background: #fff;
+ margin-bottom: 1rem;
+ border: 1px solid #e2e8f0;
+ transition: all 0.2s ease;
+}
+
+.step:hover {
+ border-color: #375bd2;
+ box-shadow: 0 4px 12px rgba(55, 91, 210, 0.08);
+}
+
+.stepHeader {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.stepTitle {
+ flex: 1;
+ font-weight: 600;
+ color: #1e293b;
+ font-size: 1.125rem;
+}
+
+.checkbox {
+ margin-left: auto;
+}
+
+.stepContent {
+ color: #475569;
+ line-height: 1.6;
+}
diff --git a/src/components/CCIP/TutorialSetup/TutorialStep.tsx b/src/components/CCIP/TutorialSetup/TutorialStep.tsx
new file mode 100644
index 00000000000..82250f736b9
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/TutorialStep.tsx
@@ -0,0 +1,18 @@
+import styles from "./TutorialStep.module.css"
+
+interface TutorialStepProps {
+ id?: string
+ title: string
+ children: React.ReactNode
+ checkbox?: React.ReactNode
+}
+
+export const TutorialStep = ({ id, title, children, checkbox }: TutorialStepProps) => (
+
+
+
{title}
+ {checkbox &&
{checkbox}
}
+
+ {children}
+
+)
diff --git a/src/components/CCIP/TutorialSetup/index.ts b/src/components/CCIP/TutorialSetup/index.ts
new file mode 100644
index 00000000000..31e3a864579
--- /dev/null
+++ b/src/components/CCIP/TutorialSetup/index.ts
@@ -0,0 +1,7 @@
+export { PrerequisitesCard } from "./PrerequisitesCard"
+export { SetupSection } from "./SetupSection"
+export { TutorialCard } from "./TutorialCard"
+export { TutorialStep } from "./TutorialStep"
+export { ContractsImportCard } from "./ContractsImportCard"
+export * from "./SolidityParam"
+export * from "./NetworkCheck"
diff --git a/src/components/CodeSample/CodeSampleReact.tsx b/src/components/CodeSample/CodeSampleReact.tsx
new file mode 100644
index 00000000000..ae8fcc81115
--- /dev/null
+++ b/src/components/CodeSample/CodeSampleReact.tsx
@@ -0,0 +1,27 @@
+import React from "react"
+import { useRemixUrl } from "src/hooks/useRemixUrl"
+
+interface CodeSampleReactProps {
+ src: string
+ showButtonOnly?: boolean
+ optimize?: boolean
+ runs?: number
+}
+
+export const CodeSampleReact: React.FC = ({ src, showButtonOnly = false, optimize, runs }) => {
+ const remixUrl = useRemixUrl(src, { optimize, runs })
+
+ const isSolidityFile = src.match(/\.sol/)
+ const isSample = isSolidityFile && (src.indexOf("samples/") === 0 || src.indexOf("/samples/") === 0)
+
+ if (!isSample || !showButtonOnly || !remixUrl) return null
+
+ return (
+
+ )
+}
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
new file mode 100644
index 00000000000..6e2c2f733bb
--- /dev/null
+++ b/src/components/ErrorBoundary.tsx
@@ -0,0 +1,36 @@
+import React, { Component, ErrorInfo, ReactNode } from "react"
+
+interface Props {
+ children: ReactNode
+ fallback?: ReactNode
+ onError?: (error: Error, errorInfo: ErrorInfo) => void
+}
+
+interface State {
+ hasError: boolean
+ error: Error | null
+}
+
+export class ErrorBoundary extends Component {
+ public state: State = {
+ hasError: false,
+ error: null,
+ }
+
+ public static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error }
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error("Error caught by boundary:", error, errorInfo)
+ this.props.onError?.(error, errorInfo)
+ }
+
+ public render() {
+ if (this.state.hasError) {
+ return this.props.fallback || Something went wrong. Please try again.
+ }
+
+ return this.props.children
+ }
+}
diff --git a/src/components/ReactCopyText.tsx b/src/components/ReactCopyText.tsx
index 349519460e0..d02f52b7cde 100644
--- a/src/components/ReactCopyText.tsx
+++ b/src/components/ReactCopyText.tsx
@@ -1,7 +1,7 @@
import { clsx } from "../lib"
import "./ReactCopyText.css"
-export type Props = {
+interface ReactCopyTextProps {
text: string
code?: boolean
format?: boolean
@@ -18,7 +18,14 @@ interface Window {
declare const window: Window
-export const ReactCopyText = ({ text, code, format, formatType, eventName, additionalInfo = {} }: Props) => {
+export const ReactCopyText = ({
+ text,
+ code,
+ format,
+ formatType,
+ eventName,
+ additionalInfo = {},
+}: ReactCopyTextProps) => {
const formatText = (text: string, type: string | undefined) => {
if (type === "bytes32" && text.length > 10) {
return text.slice(0, 6) + "..." + text.slice(-4)
diff --git a/src/components/RightSidebar/RightSidebar.astro b/src/components/RightSidebar/RightSidebar.astro
index 11a76ddd9e2..d8aab60c29c 100644
--- a/src/components/RightSidebar/RightSidebar.astro
+++ b/src/components/RightSidebar/RightSidebar.astro
@@ -26,10 +26,9 @@ const { githubEditUrl, headings } = Astro.props
diff --git a/src/pages/ccip/tutorials/[...slug].astro b/src/pages/ccip/tutorials/[...slug].astro
new file mode 100644
index 00000000000..7d643c37303
--- /dev/null
+++ b/src/pages/ccip/tutorials/[...slug].astro
@@ -0,0 +1,37 @@
+---
+import { CollectionEntry, getCollection } from "astro:content"
+import TutorialLayout from "../../../layouts/TutorialLayout.astro"
+import DocsLayout from "../../../layouts/DocsLayout.astro"
+
+type Props = {
+ entry: CollectionEntry<"ccip">
+}
+
+export async function getStaticPaths() {
+ const entries = await getCollection("ccip", (entry) => {
+ return entry.slug.startsWith("tutorials/")
+ })
+
+ const paths: { params: { slug: string }; props: Props }[] = []
+
+ entries.forEach((entry) => {
+ const tutorialSlug = entry.slug.replace("tutorials/", "")
+ paths.push({
+ params: { slug: tutorialSlug },
+ props: { entry },
+ })
+ })
+
+ return paths
+}
+
+const { entry } = Astro.props
+const { Content, headings } = await entry.render()
+
+// Only use TutorialLayout for specific interactive tutorials
+const Layout = entry.id === "tutorials/cross-chain-tokens/register-from-eoa-remix.mdx" ? TutorialLayout : DocsLayout
+---
+
+
+
+
diff --git a/src/stores/lanes/index.ts b/src/stores/lanes/index.ts
index 42b8d456208..687be9d865a 100644
--- a/src/stores/lanes/index.ts
+++ b/src/stores/lanes/index.ts
@@ -1,46 +1,605 @@
-import { atom } from "nanostores"
+import { atom, computed } from "nanostores"
import { Environment } from "@config/data/ccip"
+import type { Network } from "@config/data/ccip/types"
+import { utils } from "ethers"
export type DeployedContracts = {
token?: string
tokenPool?: string
+ tokenPools?: string[]
+ registered?: boolean
+ configured?: boolean
+ poolType?: "lock" | "burn"
+}
+
+export interface TokenBucketState {
+ tokens: string
+ lastUpdated: number
+ isEnabled: boolean
+ capacity: string
+ rate: string
+}
+
+export const TUTORIAL_STEPS = {
+ setup: {
+ id: "setup",
+ title: "Setup",
+ subSteps: {
+ "browser-setup": "Web Browser Setup",
+ "gas-tokens": "native Gas Tokens Ready",
+ "blockchains-selected": "Blockchains Selected",
+ "contracts-imported": "Contracts Imported",
+ },
+ },
+ sourceChain: {
+ id: "sourceChain",
+ title: "Source Chain",
+ subSteps: {
+ "token-deployed": "Token Deployed",
+ "admin-claimed": "Admin Role Claimed",
+ "admin-accepted": "Admin Role Accepted",
+ "pool-deployed": "Pool Deployed",
+ "pool-registered": "Pool Registered",
+ },
+ },
+ destinationChain: {
+ id: "destinationChain",
+ title: "Destination Chain",
+ subSteps: {
+ "dest-token-deployed": "Token Deployed",
+ "admin-claimed": "Admin Role Claimed",
+ "admin-accepted": "Admin Role Accepted",
+ "dest-pool-deployed": "Pool Deployed",
+ "dest-pool-registered": "Pool Registered",
+ },
+ },
+ sourceConfig: {
+ id: "sourceConfig",
+ title: "Source Configuration",
+ subSteps: {
+ "source-privileges": "Grant Burn and Mint Privileges",
+ "source-pool-config": "Configure Pool",
+ "source-verification": "Verify Configuration",
+ },
+ },
+ destinationConfig: {
+ id: "destinationConfig",
+ title: "Destination Configuration",
+ subSteps: {
+ "dest-privileges": "Grant Burn and Mint Privileges",
+ "dest-pool-config": "Configure Pool",
+ "dest-verification": "Verify Configuration",
+ },
+ },
+} as const
+
+export type StepId = keyof typeof TUTORIAL_STEPS
+export type SubStepId = keyof (typeof TUTORIAL_STEPS)[T]["subSteps"]
+
+export interface RateLimiterConfig {
+ enabled: boolean
+ capacity: string
+ rate: string
+}
+
+export type RateLimits = {
+ inbound: RateLimiterConfig
+ outbound: RateLimiterConfig
}
export type LaneState = {
sourceChain: string
destinationChain: string
environment: Environment
+ sourceNetwork: Network | null
+ destinationNetwork: Network | null
sourceContracts: DeployedContracts
destinationContracts: DeployedContracts
+ progress: Record>
+ inboundRateLimiter: TokenBucketState | null
+ outboundRateLimiter: TokenBucketState | null
+ sourceRateLimits: RateLimits | null
+ destinationRateLimits: RateLimits | null
+ currentStep?: StepId
}
-export const laneStore = atom({
+// Add performance monitoring
+const monitorStoreUpdate = (action: string, details: Record) => {
+ if (process.env.NODE_ENV === "development") {
+ console.log(`[StoreAction] ${action}:`, {
+ ...details,
+ timestamp: new Date().toISOString(),
+ })
+ }
+}
+
+// Define conditions at the top level
+const conditions = [
+ // Prerequisites conditions
+ {
+ stepId: "setup" as StepId,
+ subStepId: "browser-setup",
+ check: (state: LaneState) => state.progress.setup?.["browser-setup"] === true,
+ dependencies: [] as StepId[],
+ },
+ {
+ stepId: "setup" as StepId,
+ subStepId: "gas-tokens",
+ check: (state: LaneState) => state.progress.setup?.["gas-tokens"] === true,
+ dependencies: ["setup"] as StepId[],
+ },
+ {
+ stepId: "setup" as StepId,
+ subStepId: "blockchains-selected",
+ check: (state: LaneState) => {
+ const sourceSelected = Boolean(state.sourceChain && state.sourceNetwork)
+ const destSelected = Boolean(state.destinationChain && state.destinationNetwork)
+ return sourceSelected && destSelected
+ },
+ dependencies: ["setup"] as StepId[],
+ },
+ {
+ stepId: "setup" as StepId,
+ subStepId: "contracts-imported",
+ check: (state: LaneState) => state.progress.setup?.["contracts-imported"] === true,
+ dependencies: ["setup"] as StepId[],
+ },
+ // Token deployment conditions
+ {
+ stepId: "sourceChain" as StepId,
+ subStepId: "token-deployed",
+ check: (state: LaneState) => {
+ const hasToken = !!state.sourceContracts.token && utils.isAddress(state.sourceContracts.token)
+ return hasToken
+ },
+ dependencies: ["setup"] as StepId[],
+ },
+ {
+ stepId: "sourceChain" as StepId,
+ subStepId: "pool-deployed",
+ check: (state: LaneState) => {
+ const hasPool = !!state.sourceContracts.tokenPool && utils.isAddress(state.sourceContracts.tokenPool)
+ return hasPool
+ },
+ dependencies: ["sourceChain"] as StepId[],
+ },
+ {
+ stepId: "sourceChain" as StepId,
+ subStepId: "pool-registered",
+ check: (state: LaneState) => !!state.sourceContracts.registered,
+ dependencies: ["sourceChain"] as StepId[],
+ },
+ {
+ stepId: "destinationChain" as StepId,
+ subStepId: "dest-token-deployed",
+ check: (state: LaneState) => !!state.destinationContracts.token,
+ dependencies: ["setup"] as StepId[],
+ },
+ {
+ stepId: "destinationChain" as StepId,
+ subStepId: "dest-pool-deployed",
+ check: (state: LaneState) => {
+ const hasPool = !!state.destinationContracts.tokenPool && utils.isAddress(state.destinationContracts.tokenPool)
+ return hasPool
+ },
+ dependencies: ["destinationChain"] as StepId[],
+ },
+ {
+ stepId: "destinationChain" as StepId,
+ subStepId: "dest-pool-registered",
+ check: (state: LaneState) => !!state.destinationContracts.registered,
+ dependencies: ["destinationChain"] as StepId[],
+ },
+ {
+ stepId: "sourceChain" as StepId,
+ subStepId: "admin-claimed",
+ check: (state: LaneState) => state.progress.sourceChain?.["admin-claimed"] === true,
+ dependencies: ["sourceChain"] as StepId[],
+ },
+ {
+ stepId: "sourceChain" as StepId,
+ subStepId: "admin-accepted",
+ check: (state: LaneState) => state.progress.sourceChain?.["admin-accepted"] === true,
+ dependencies: ["sourceChain"] as StepId[],
+ },
+ {
+ stepId: "destinationChain" as StepId,
+ subStepId: "admin-claimed",
+ check: (state: LaneState) => state.progress.destinationChain?.["admin-claimed"] === true,
+ dependencies: ["destinationChain"] as StepId[],
+ },
+ {
+ stepId: "destinationChain" as StepId,
+ subStepId: "admin-accepted",
+ check: (state: LaneState) => state.progress.destinationChain?.["admin-accepted"] === true,
+ dependencies: ["destinationChain"] as StepId[],
+ },
+ {
+ stepId: "sourceConfig" as StepId,
+ subStepId: "source-privileges",
+ check: (state: LaneState) => state.progress.sourceConfig?.["source-privileges"] === true,
+ dependencies: ["sourceChain"],
+ },
+ {
+ stepId: "sourceConfig" as StepId,
+ subStepId: "source-pool-config",
+ check: (state: LaneState) => state.progress.sourceConfig?.["source-pool-config"] === true,
+ dependencies: ["sourceChain"],
+ },
+ {
+ stepId: "sourceConfig" as StepId,
+ subStepId: "source-verification",
+ check: (state: LaneState) => state.progress.sourceConfig?.["source-verification"] === true,
+ dependencies: ["sourceChain"],
+ },
+ {
+ stepId: "destinationConfig" as StepId,
+ subStepId: "dest-privileges",
+ check: (state: LaneState) => state.progress.destinationConfig?.["dest-privileges"] === true,
+ dependencies: ["destinationChain"],
+ },
+ {
+ stepId: "destinationConfig" as StepId,
+ subStepId: "dest-pool-config",
+ check: (state: LaneState) => state.progress.destinationConfig?.["dest-pool-config"] === true,
+ dependencies: ["destinationChain"],
+ },
+ {
+ stepId: "destinationConfig" as StepId,
+ subStepId: "dest-verification",
+ check: (state: LaneState) => state.progress.destinationConfig?.["dest-verification"] === true,
+ dependencies: ["destinationChain"],
+ },
+]
+
+// Helper function to check if prerequisites are complete
+export const arePrerequisitesComplete = (state: LaneState): boolean => {
+ return (
+ state.progress.setup?.["browser-setup"] === true &&
+ state.progress.setup?.["gas-tokens"] === true &&
+ state.progress.setup?.["blockchains-selected"] === true &&
+ state.progress.setup?.["contracts-imported"] === true
+ )
+}
+
+// Create individual atoms for each step's progress
+export const setupProgressStore = atom>({})
+export const sourceChainProgressStore = atom>({})
+export const destinationChainProgressStore = atom>({})
+export const sourceConfigProgressStore = atom>({})
+export const destinationConfigProgressStore = atom>({})
+
+// Computed store that combines all progress
+export const progressStore = computed(
+ [
+ setupProgressStore,
+ sourceChainProgressStore,
+ destinationChainProgressStore,
+ sourceConfigProgressStore,
+ destinationConfigProgressStore,
+ ],
+ (setup, sourceChain, destinationChain, sourceConfig, destinationConfig) => ({
+ setup,
+ sourceChain,
+ destinationChain,
+ sourceConfig,
+ destinationConfig,
+ })
+)
+
+// Main store without progress
+export const laneStore = atom>({
sourceChain: "",
destinationChain: "",
environment: Environment.Testnet,
+ sourceNetwork: null,
+ destinationNetwork: null,
sourceContracts: {},
destinationContracts: {},
+ inboundRateLimiter: null,
+ outboundRateLimiter: null,
+ sourceRateLimits: {
+ inbound: { enabled: false, capacity: "", rate: "" },
+ outbound: { enabled: false, capacity: "", rate: "" },
+ },
+ destinationRateLimits: {
+ inbound: { enabled: false, capacity: "", rate: "" },
+ outbound: { enabled: false, capacity: "", rate: "" },
+ },
})
-// Helper functions to update contract addresses
-export const setSourceContract = (type: keyof DeployedContracts, address: string) => {
- const current = laneStore.get()
- laneStore.set({
+// Helper to get the correct store for a step with type safety
+const getStoreForStep = (stepId: StepId) => {
+ switch (stepId) {
+ case "setup":
+ return setupProgressStore
+ case "sourceChain":
+ return sourceChainProgressStore
+ case "destinationChain":
+ return destinationChainProgressStore
+ case "sourceConfig":
+ return sourceConfigProgressStore
+ case "destinationConfig":
+ return destinationConfigProgressStore
+ default:
+ throw new Error(`Invalid step ID: ${stepId}`)
+ }
+}
+
+// Standard progress update function for all checkboxes
+export const updateStepProgress = (stepId: string, subStepId: string, completed: boolean) => {
+ const startTime = Date.now()
+ const store = getStoreForStep(stepId as StepId)
+ const current = store.get()
+
+ if (current[subStepId] === completed) {
+ monitorStoreUpdate("SkippedUpdate", {
+ stepId,
+ subStepId,
+ reason: "No change needed",
+ duration: Date.now() - startTime,
+ })
+ return
+ }
+
+ monitorStoreUpdate("StartUpdate", {
+ stepId,
+ subStepId,
+ completed,
+ currentState: current,
+ })
+
+ // Batch updates to minimize renders
+ const updates = new Map()
+
+ // Update progress store
+ updates.set(store, {
...current,
+ [subStepId]: completed,
+ })
+
+ // For pool registration, also update contract state
+ if (
+ (stepId === "sourceChain" && subStepId === "pool-registered") ||
+ (stepId === "destinationChain" && subStepId === "dest-pool-registered")
+ ) {
+ const chain = stepId === "sourceChain" ? "source" : "destination"
+ const contractsKey = `${chain}Contracts` as const
+ const currentState = laneStore.get()
+
+ updates.set(laneStore, {
+ ...currentState,
+ [contractsKey]: {
+ ...currentState[contractsKey],
+ registered: completed,
+ },
+ })
+ }
+
+ // Apply all updates in a single batch
+ updates.forEach((value, store) => {
+ store.set(value)
+ })
+
+ monitorStoreUpdate("CompleteUpdate", {
+ stepId,
+ subStepId,
+ duration: Date.now() - startTime,
+ })
+}
+
+// Helper to update progress for a specific step (internal use only)
+function updateProgressForStep(stepId: StepId, updates: Record) {
+ const store = getStoreForStep(stepId)
+ const current = store.get()
+ store.set({
+ ...current,
+ ...updates,
+ })
+}
+
+// Utility function to handle contract progress updates
+const updateContractProgress = (type: keyof DeployedContracts, chain: "source" | "destination", value: string) => {
+ const isValidAddress = Boolean(value) && utils.isAddress(value)
+
+ if (type === "token") {
+ updateProgressForStep(chain === "source" ? "sourceChain" : "destinationChain", {
+ [chain === "source" ? "token-deployed" : "dest-token-deployed"]: isValidAddress,
+ })
+ } else if (type === "tokenPool") {
+ updateProgressForStep(chain === "source" ? "sourceChain" : "destinationChain", {
+ [chain === "source" ? "pool-deployed" : "dest-pool-deployed"]: isValidAddress,
+ })
+ }
+}
+
+export const setSourceContract = (type: keyof DeployedContracts, value: string) => {
+ const currentState = laneStore.get()
+ monitorStoreUpdate("setSourceContract", { type, value })
+
+ laneStore.set({
+ ...currentState,
sourceContracts: {
- ...current.sourceContracts,
- [type]: address,
+ ...currentState.sourceContracts,
+ [type]: value,
},
})
+
+ updateContractProgress(type, "source", value)
+}
+
+export const setDestinationContract = (type: keyof DeployedContracts, value: string) => {
+ const currentState = laneStore.get()
+ monitorStoreUpdate("setDestinationContract", { type, value })
+
+ laneStore.set({
+ ...currentState,
+ destinationContracts: {
+ ...currentState.destinationContracts,
+ [type]: value,
+ },
+ })
+
+ updateContractProgress(type, "destination", value)
+}
+
+// Helper to subscribe to specific step's progress
+export const subscribeToStepProgress = (stepId: StepId, callback: (progress: Record) => void) => {
+ const store = getStoreForStep(stepId)
+ return store.subscribe(callback)
}
-export const setDestinationContract = (type: keyof DeployedContracts, address: string) => {
+export const setRateLimiterState = (type: "inbound" | "outbound", state: TokenBucketState | null) => {
const current = laneStore.get()
laneStore.set({
...current,
- destinationContracts: {
- ...current.destinationContracts,
- [type]: address,
+ [type === "inbound" ? "inboundRateLimiter" : "outboundRateLimiter"]: state,
+ })
+}
+
+export const setRemotePools = (chain: "source" | "destination", pools: string[]) => {
+ const current = laneStore.get()
+ const contracts = chain === "source" ? "sourceContracts" : "destinationContracts"
+ laneStore.set({
+ ...current,
+ [contracts]: {
+ ...current[contracts],
+ tokenPool: pools[0],
+ tokenPools: pools,
},
})
}
+
+export const validateRateLimits = (limits: RateLimits): boolean => {
+ if (!limits) return false
+
+ const validateConfig = (config: RateLimiterConfig) => {
+ if (config.enabled) {
+ try {
+ // Parse as BigInt and validate
+ const capacity = BigInt(config.capacity || "0")
+ const rate = BigInt(config.rate || "0")
+
+ // Ensure it's a valid uint128
+ const MAX_UINT128 = BigInt(2) ** BigInt(128) - BigInt(1)
+ return capacity >= 0n && capacity <= MAX_UINT128 && rate >= 0n && rate <= MAX_UINT128
+ } catch (e) {
+ console.error("Rate limit validation error:", e)
+ return false
+ }
+ }
+ return true
+ }
+
+ return validateConfig(limits.inbound) && validateConfig(limits.outbound)
+}
+
+// Helper to update rate limits with validation
+export const updateRateLimits = (
+ chain: "source" | "destination",
+ type: "inbound" | "outbound",
+ updates: Partial
+) => {
+ const current = laneStore.get()
+ const rateLimitsKey = `${chain}RateLimits` as const
+ const currentLimits = current[rateLimitsKey] ?? {
+ inbound: { enabled: false, capacity: "", rate: "" },
+ outbound: { enabled: false, capacity: "", rate: "" },
+ }
+
+ const newLimits = {
+ ...currentLimits,
+ [type]: {
+ ...currentLimits[type],
+ ...updates,
+ },
+ }
+
+ if (validateRateLimits(newLimits)) {
+ laneStore.set({
+ ...current,
+ [rateLimitsKey]: newLimits,
+ })
+ return true
+ }
+ return false
+}
+
+// Helper function to get complete state
+const getCompleteState = (): LaneState => ({
+ ...laneStore.get(),
+ progress: progressStore.get(),
+})
+
+// Update checkSpecificProgress to optionally accept state
+const checkSpecificProgress = (conditionsToCheck: typeof conditions, providedState?: LaneState) => {
+ const state = providedState || getCompleteState()
+
+ // Only check dependent conditions if prerequisites are complete
+ if (!arePrerequisitesComplete(state) && !conditionsToCheck.every((c) => c.stepId === "setup")) {
+ return
+ }
+
+ for (const condition of conditionsToCheck) {
+ const startTime = performance.now()
+ const isComplete = condition.check(state)
+
+ monitorStoreUpdate("ConditionCheck", {
+ stepId: condition.stepId,
+ subStepId: condition.subStepId,
+ isComplete,
+ duration: Math.round(performance.now() - startTime),
+ })
+
+ // Update progress if needed
+ if (isComplete !== state.progress[condition.stepId]?.[condition.subStepId]) {
+ updateProgressForStep(condition.stepId, { [condition.subStepId]: isComplete })
+ }
+ }
+}
+
+// Update the progress check function to be more focused
+export const checkProgress = (stepId: StepId, subStepId: string) => {
+ const state = getCompleteState()
+ monitorStoreUpdate("StartUpdate", { stepId, subStepId, currentState: state })
+
+ // For setup steps, check all setup conditions and their dependencies
+ if (stepId === "setup") {
+ const setupConditions = conditions.filter((condition) => condition.stepId === "setup")
+ checkSpecificProgress(setupConditions, state)
+
+ // If all prerequisites are complete, check dependent conditions
+ if (arePrerequisitesComplete(state)) {
+ const dependentConditions = conditions.filter((condition) => condition.dependencies.includes("setup"))
+ checkSpecificProgress(dependentConditions, state)
+ }
+ } else {
+ // For other steps, check only directly related conditions
+ const relevantConditions = conditions.filter(
+ (condition) =>
+ (condition.stepId === stepId && condition.subStepId === subStepId) || condition.dependencies.includes(stepId)
+ )
+ if (relevantConditions.length > 0) {
+ checkSpecificProgress(relevantConditions, state)
+ }
+ }
+}
+
+export const navigateToStep = (stepId: StepId) => {
+ const store = getStoreForStep(stepId)
+ const currentState = store.get()
+
+ // Update lane store to reflect the current step
+ const currentLaneState = laneStore.get()
+ laneStore.set({
+ ...currentLaneState,
+ currentStep: stepId,
+ })
+
+ // Emit navigation event for scroll handling
+ window.dispatchEvent(
+ new CustomEvent("tutorial-navigate", {
+ detail: { stepId },
+ })
+ )
+}
diff --git a/src/utils/dialog.ts b/src/utils/dialog.ts
new file mode 100644
index 00000000000..d85cc1494a2
--- /dev/null
+++ b/src/utils/dialog.ts
@@ -0,0 +1,13 @@
+interface ConfirmationDialogProps {
+ title: string
+ message: string
+ estimatedGas?: string
+ actionType: "critical" | "warning" | "info"
+}
+
+export const showConfirmationDialog = async (props: ConfirmationDialogProps): Promise => {
+ if (typeof window === "undefined") return false
+ return window.confirm(
+ `${props.title}\n\n${props.message}${props.estimatedGas ? `\n\nEstimated Gas: ${props.estimatedGas}` : ""}`
+ )
+}
diff --git a/src/utils/performance.ts b/src/utils/performance.ts
new file mode 100644
index 00000000000..fdccab078ca
--- /dev/null
+++ b/src/utils/performance.ts
@@ -0,0 +1,27 @@
+type AnyFunction = (...args: unknown[]) => unknown
+
+/**
+ * Creates a debounced function that delays invoking `func` until after `wait` milliseconds
+ * have elapsed since the last time the debounced function was invoked.
+ *
+ * @param func - The function to debounce
+ * @param wait - The number of milliseconds to delay
+ * @returns A debounced version of the function
+ */
+export function debounce(
+ func: T,
+ wait: number
+): (...args: Parameters) => ReturnType | undefined {
+ let timeoutId: NodeJS.Timeout | undefined
+
+ return function debounced(this: unknown, ...args: Parameters): ReturnType | undefined {
+ const later = () => {
+ timeoutId = undefined
+ return func.apply(this, args)
+ }
+
+ clearTimeout(timeoutId)
+ timeoutId = setTimeout(later, wait)
+ return undefined
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index e50d1a1b863..ef11db7bc8d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,16 +19,18 @@
"baseUrl": ".",
"paths": {
"~/*": ["src/*"],
+ "@components": ["src/components/index.ts"],
+ "@components/*": ["src/components/*"],
"@config": ["src/config/"],
"@config/*": ["src/config/*"],
"@features/*": ["src/features/*"],
- "@variables": ["src/config/markdown-variables.ts"],
- "@components/*": ["src/components/*"],
- "@components": ["src/components/index.ts"],
- "@abi": ["src/features/abi/index.ts"],
"@graphql": ["src/graphql/"],
"@graphql/*": ["src/graphql/*"],
- "@stores/*": ["src/stores/*"]
+ "@stores/*": ["src/stores/*"],
+ "@utils": ["src/utils/"],
+ "@utils/*": ["src/utils/*"],
+ "@variables": ["src/config/markdown-variables.ts"],
+ "@abi": ["src/features/abi/index.ts"]
},
"strictNullChecks": true,
"verbatimModuleSyntax": false