diff --git a/.prettierignore b/.prettierignore
index d5237f8bc13..74de3d64622 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,6 +1,9 @@
dist
.cache
.test
+.vercel
+.astro
+temp
node_modules
.github
.changeset
diff --git a/public/samples/CCIP/cct/TokenDependencies.sol b/public/samples/CCIP/cct/TokenDependencies.sol
index 2905e1c937f..556528d605c 100644
--- a/public/samples/CCIP/cct/TokenDependencies.sol
+++ b/public/samples/CCIP/cct/TokenDependencies.sol
@@ -3,3 +3,7 @@ pragma solidity 0.8.24;
// solhint-disable no-unused-import
import {BurnMintERC677} from "@chainlink/contracts-ccip/src/v0.8/shared/token/ERC677/BurnMintERC677.sol";
+import {BurnMintTokenPool} from "@chainlink/contracts-ccip/src/v0.8/ccip/pools/BurnMintTokenPool.sol";
+import {LockReleaseTokenPool} from "@chainlink/contracts-ccip/src/v0.8/ccip/pools/LockReleaseTokenPool.sol";
+import {RegistryModuleOwnerCustom} from "@chainlink/contracts-ccip/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol";
+import {TokenAdminRegistry} from "@chainlink/contracts-ccip/src/v0.8/ccip/tokenAdminRegistry/TokenAdminRegistry.sol";
diff --git a/src/components/CCIP/TutorialBlockchainSelector/AdminSetupStep.module.css b/src/components/CCIP/TutorialBlockchainSelector/AdminSetupStep.module.css
new file mode 100644
index 00000000000..d49578f6c12
--- /dev/null
+++ b/src/components/CCIP/TutorialBlockchainSelector/AdminSetupStep.module.css
@@ -0,0 +1,130 @@
+.steps {
+ padding-left: var(--space-4x);
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4x);
+}
+
+.instructions {
+ padding-left: var(--space-6x);
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3x);
+}
+
+.contractInfo {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3x);
+ padding: var(--space-3x);
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ margin-bottom: var(--space-3x);
+}
+
+.contractInfo strong {
+ color: var(--color-text-primary);
+ font-weight: 600;
+}
+
+.actionDetails {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2x);
+}
+
+.actionTitle {
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.actionTitle code {
+ font-family: var(--font-mono);
+ color: var(--color-text-primary);
+ background: var(--color-background);
+ padding: var(--space-1x) var(--space-2x);
+ border-radius: 4px;
+ font-size: var(--font-size-sm);
+}
+
+.parameter {
+ display: grid;
+ grid-template-columns: 120px 1fr;
+ gap: var(--space-3x);
+ align-items: center;
+ padding: var(--space-2x) var(--space-3x);
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+}
+
+.paramName {
+ font-family: var(--font-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .parameter {
+ grid-template-columns: 1fr;
+ gap: var(--space-2x);
+ }
+}
+
+.functionDescription {
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+ margin: var(--space-2x) 0;
+}
+
+.parameters {
+ margin-top: var(--space-2x);
+}
+
+.functionCall {
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: var(--space-4x);
+ margin: var(--space-2x) 0;
+}
+
+.functionHeader {
+ margin-bottom: var(--space-3x);
+}
+
+.functionName {
+ font-family: var(--font-mono);
+ font-size: var(--font-size-lg);
+ color: var(--color-text-primary);
+ background: var(--color-background);
+ padding: var(--space-1x) var(--space-2x);
+ border-radius: 4px;
+}
+
+.functionPurpose {
+ color: var(--color-text);
+ margin-top: var(--space-2x);
+ font-size: var(--font-size-base);
+}
+
+.functionRequirement {
+ color: var(--color-warning);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-3x);
+}
+
+.parametersSection {
+ border-top: 1px solid var(--color-border);
+ padding-top: var(--space-3x);
+}
+
+.parametersTitle {
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin-bottom: var(--space-2x);
+}
diff --git a/src/components/CCIP/TutorialBlockchainSelector/AdminSetupStep.tsx b/src/components/CCIP/TutorialBlockchainSelector/AdminSetupStep.tsx
new file mode 100644
index 00000000000..4c95ffb20ef
--- /dev/null
+++ b/src/components/CCIP/TutorialBlockchainSelector/AdminSetupStep.tsx
@@ -0,0 +1,146 @@
+import { useStore } from "@nanostores/react"
+import { laneStore } from "@stores/lanes"
+import { NetworkCheck } from "../TutorialSetup/NetworkCheck"
+import { TutorialCard } from "../TutorialSetup/TutorialCard"
+import { TutorialStep } from "../TutorialSetup/TutorialStep"
+import { NetworkAddress } from "./NetworkAddress"
+import { StepCheckbox } from "../TutorialProgress/StepCheckbox"
+import { SolidityParam } from "../TutorialSetup/SolidityParam"
+import { Callout } from "../TutorialSetup/Callout"
+import styles from "./AdminSetupStep.module.css"
+
+interface AdminSetupStepProps {
+ chain: "source" | "destination"
+}
+
+export const AdminSetupStep = ({ chain }: AdminSetupStepProps) => {
+ const state = useStore(laneStore)
+ const network = chain === "source" ? state.sourceNetwork : state.destinationNetwork
+ const networkInfo = network ? { name: network.name, logo: network.logo } : { name: "loading..." }
+ const stepId = chain === "source" ? "sourceChain" : "destinationChain"
+ const tokenAddress = chain === "source" ? state.sourceContracts.token : state.destinationContracts.token
+
+ const getSubStepId = (subStepId: string) => `${stepId}-${subStepId}`
+
+ const content = (
+ <>
+
registerAdminViaOwner() in this tutorial because our deployed BurnMintERC677 token
+ implements the owner() function. For other token implementations, you might use different
+ registration methods. See the{" "}
+
+ self-service registration documentation
+ {" "}
+ for all available options.
+ registerAdminViaOwner:
+ registerAdminViaOwner
+ acceptAdminRole:
+ acceptAdminRole
+ + Rate limits control how many tokens can be transferred over a given blockchain lane within a specific time + frame. When working with rate limits, consider the following: +
+1000000000000000000.
+ + Learn more in the CCIP rate limits documentation. +
+{readOnly.chainSelector}
+ {readOnly.poolAddress}
+ {readOnly.tokenAddress}
+ applyChainUpdates:
+ applyChainUpdates
+ chainsToAdd parameter in Remix:
+ + Official guide for verifying contracts using the Remix IDE verification plugin +
+ + View Guide ↗ + ++ Step-by-step tutorial for contract verification on blockchain explorers +
+ + View Tutorial ↗ + +Configure your pool by setting these required parameters in Remix:
+The tutorial will use your provided token address for subsequent steps.
+Configure your token by setting these required parameters in Remix:
+
+ 1000 * 1018
+ {" "}
+ = 1000000000000000000000 (that's 1 followed by 21 zeros).
+ grantMintAndBurnRoles:
+ grantMintAndBurnRoles
+ [Select {chain} blockchain first] : null
+
+ const networks = getAllNetworks({ filter: state.environment })
+ const network = networks.find((n) => n.chain === chainId)
+
+ if (!network) return required ? Network not found : null
+
+ let address: string | undefined
+ switch (type) {
+ case "registryModule":
+ address = network.registryModule
+ break
+ case "tokenAdminRegistry":
+ address = network.tokenAdminRegistry
+ break
+ case "router":
+ address = network.router?.address
+ break
+ case "armProxy":
+ address = network.armProxy?.address
+ break
+ case "chainSelector":
+ address = network.chainSelector
+ break
+ }
+
+ if (!address) return required ? Contract address not available : null
+
+ return (
+ 0
+ Current block timestamp
+ {config?.enabled ? "true" : "false"}
+ {config?.capacity || "0"}
+ {config?.rate || "0"}
+ getRemoteToken:
+ getRemoteToken
+ getRemotePools:
+ getRemotePools
+ getCurrentInboundRateLimiterState:
+ getCurrentInboundRateLimiterState
+ tokens field starts at 0 and the lastUpdated field will show the
+ current block timestamp.
+ getCurrentOutboundRateLimiterState:
+ getCurrentOutboundRateLimiterState
+ tokens field starts at 0 and the lastUpdated field will show the
+ current block timestamp.
+ setPool:
+ setPool
+ {description}
} +{name}
+ {type}
+ {description}
+ {example && (typeof example === "string" ?{displayText} : displayText}
+
+
+ )
+}
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
+---
+
+