> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpara.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Canton Network External Party Onboarding with Para

> Learn how to use Para's React SDK to onboard a Para-managed Solana wallet as a Canton Network external party using the Ed25519 key ownership flow

Canton Network uses Ed25519 keypairs for **external party** identities — wallets that participate in the ledger without running a Canton node. Because Para-managed Solana wallets are native Ed25519 keys, they work directly with Canton's external party API.

This walkthrough shows how to connect through ParaModal, prove key ownership to Canton, and receive a `partyId` on the ledger — entirely from a React app.

## How it works

<Steps>
  <Step title="Connect with Para">
    User authenticates through ParaModal and gets an embedded Solana wallet.
  </Step>

  <Step title="Register the public key with Canton">
    Server sends the Solana public key to Canton's `generateExternalParty`, which returns a `multiHash` challenge.
  </Step>

  <Step title="Sign the challenge with Para">
    Client signs the `multiHash` with `useSignMessage`.
  </Step>

  <Step title="Allocate the party">
    Server submits the signature to Canton's `allocateExternalParty`, which returns a `partyId`.
  </Step>
</Steps>

## Prerequisites

* A Para API key from the [Para Developer Portal](https://developer.getpara.com)
* Access to a Canton ledger and validator — either a hosted deployment or a local [Splice LocalNet](https://docs.dev.sync.global/app_dev/testing/localnet.html) stack via docker-compose
* Node.js 18+ with Next.js (for the server-side Canton SDK calls)
* Basic familiarity with React and TypeScript

## Installation

```bash theme={null}
npm install @getpara/react-sdk @canton-network/wallet-sdk @tanstack/react-query bs58 pino server-only
```

The Canton SDK (`@canton-network/wallet-sdk`) must run on the server. Using `server-only` ensures it never bundles into the browser.

## Project structure

The Canton SDK holds credentials for your validator. Keep it in Next.js API routes so those credentials never reach the client.

```
src/
├── app/page.tsx                         # React UI — ParaModal + sign button
├── app/api/canton/generate/route.ts     # Server: generateExternalParty
├── app/api/canton/allocate/route.ts     # Server: allocateExternalParty
├── components/ParaProvider.tsx          # Para SDK + Solana config
├── hooks/useCantonOnboarding.ts         # generate → sign → allocate
└── lib/canton.ts                        # Server-only Canton SDK setup
```

## Setup

### 1. Configure the Para Provider

Set up `ParaProvider` with embedded-wallet signups enabled. Para automatically provisions a Solana (Ed25519) wallet for each user, which is what Canton needs.

```typescript theme={null}
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ParaWeb, { Environment, ParaProvider as ParaSDKProvider } from "@getpara/react-sdk";

const para = new ParaWeb(Environment.BETA, process.env.NEXT_PUBLIC_PARA_API_KEY!);
const queryClient = new QueryClient();

export function ParaProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <ParaSDKProvider
        paraClientConfig={para}
        config={{ appName: "My Canton App" }}
        paraModalConfig={{
          authLayout: ["AUTH:FULL"],
          oAuthMethods: ["APPLE", "DISCORD", "FACEBOOK", "GOOGLE", "TWITTER"],
        }}>
        {children}
      </ParaSDKProvider>
    </QueryClientProvider>
  );
}
```

<Note>
  No `externalWalletConfig` is set — Canton external parties are backed by Para's own MPC-managed Solana key, not a user-supplied wallet like Phantom or MetaMask.
</Note>

### 2. Initialize the Canton SDK (server-only)

Create `src/lib/canton.ts`. This mirrors Canton's own `localNetAuthDefault` pattern and is memoized so the SDK connects once per server process.

```typescript theme={null}
import "server-only";

import { WalletSDKImpl, localNetAuthDefault, LedgerController } from "@canton-network/wallet-sdk";
import { pino } from "pino";

const logger = pino({ name: "canton", level: "info" });

let sdkPromise: Promise<WalletSDKImpl> | null = null;

export function getSdk(): Promise<WalletSDKImpl> {
  if (sdkPromise) return sdkPromise;

  const ledgerApiUrl = process.env.LEDGER_API_URL!;
  const validatorApiUrl = process.env.VALIDATOR_API_URL!;
  const validatorAudience = process.env.VALIDATOR_AUDIENCE!;
  const unsafeSecret = process.env.AUTH_UNSAFE_SECRET!;
  const userId = process.env.AUTH_USER_ID ?? "ledger-api-user";

  const authFactory = () => {
    const auth = localNetAuthDefault(logger as any);
    auth.userId = userId;
    (auth as any).audience = validatorAudience;
    (auth as any).unsafeSecret = unsafeSecret;
    return auth;
  };

  sdkPromise = (async () => {
    const sdk = new WalletSDKImpl().configure({
      logger,
      authFactory,
      ledgerFactory: (uid, auth, isAdmin) =>
        new LedgerController(uid, new URL(ledgerApiUrl), undefined, isAdmin, auth),
    });
    await sdk.connect();
    await sdk.connectAdmin();
    await sdk.connectTopology(new URL(validatorApiUrl));
    return sdk;
  })().catch((err) => { sdkPromise = null; throw err; });

  return sdkPromise;
}
```

<Note>
  For production Canton deployments, replace `localNetAuthDefault` with your validator's authentication method (typically OAuth/JWT). Update `VALIDATOR_AUDIENCE` to match your validator's expected audience.
</Note>

### 3. Create the API routes

**`/api/canton/generate`** — decodes the Solana address and calls Canton to prepare the external party challenge.

```typescript theme={null}
// src/app/api/canton/generate/route.ts
import { NextResponse } from "next/server";
import bs58 from "bs58";
import { getSdk } from "@/lib/canton";

export const runtime = "nodejs";

export async function POST(request: Request) {
  const { solanaAddress, partyHint } = await request.json();

  // Solana address (base58) → raw 32-byte Ed25519 public key → base64
  const rawPubkey = bs58.decode(solanaAddress);
  const publicKeyBase64 = Buffer.from(rawPubkey).toString("base64");

  const sdk = await getSdk();
  const generatedParty = await sdk.userLedger?.generateExternalParty(
    publicKeyBase64,
    partyHint
  );

  return NextResponse.json({
    multiHash: generatedParty!.multiHash,
    generatedParty,
  });
}
```

**`/api/canton/allocate`** — submits the Para-produced Ed25519 signature to finalize the party.

```typescript theme={null}
// src/app/api/canton/allocate/route.ts
import { NextResponse } from "next/server";
import { getSdk } from "@/lib/canton";

export const runtime = "nodejs";

export async function POST(request: Request) {
  const { signatureBase64, generatedParty } = await request.json();

  const sdk = await getSdk();
  const allocatedParty = await sdk.userLedger?.allocateExternalParty(
    signatureBase64,
    generatedParty
  );

  return NextResponse.json({ partyId: allocatedParty!.partyId });
}
```

### 4. Build the onboarding hook

`useCantonOnboarding` does three things on the client:

1. Makes sure the user's Para **Solana** wallet is the active one (embedded accounts default to EVM).
2. Fetches the Canton challenge, signs it with `useSignMessage`, and submits the signature.

`useSignMessage` goes straight to Para's MPC `signMessage` endpoint, so there's no Solana RPC client to configure.

```typescript theme={null}
"use client";

import { useCallback, useEffect, useState } from "react";
import { useAccount, useSignMessage, useWallet, useWalletState } from "@getpara/react-sdk";

export function useCantonOnboarding() {
  const account = useAccount();
  const { data: wallet } = useWallet();
  const { setSelectedWallet } = useWalletState();
  const { signMessageAsync } = useSignMessage();

  // Para accounts can hold multiple embedded wallets (EVM, Solana, …).
  // Force-select the Solana one so `wallet` / `wallet.address` is Ed25519.
  useEffect(() => {
    if (account?.isConnected && wallet?.type !== "SOLANA") {
      const solanaWallet = account.embedded.wallets?.find((w) => w.type === "SOLANA");
      if (solanaWallet) {
        setSelectedWallet({ id: solanaWallet.id, type: "SOLANA" });
      }
    }
  }, [account, wallet, setSelectedWallet]);

  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [multiHash, setMultiHash] = useState<string>();
  const [partyId, setPartyId] = useState<string>();

  const isSolanaWallet = wallet?.type === "SOLANA";
  const address = isSolanaWallet ? wallet?.address : undefined;
  const walletId = isSolanaWallet ? wallet?.id : undefined;

  const onboard = useCallback(async () => {
    if (!address || !walletId) return;
    setIsPending(true);
    setError(null);

    try {
      // Step 1: Register public key with Canton, receive multiHash challenge
      const genRes = await fetch("/api/canton/generate", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ solanaAddress: address, partyHint: "my-app-party" }),
      });
      const { multiHash: mh, generatedParty } = await genRes.json();
      setMultiHash(mh);

      // Step 2: Sign the multiHash with Para's MPC-managed Ed25519 key
      const signRes = await signMessageAsync({ walletId, messageBase64: mh });
      if (!("signature" in signRes) || !signRes.signature) {
        throw new Error("Para signing was denied or returned no signature");
      }
      const signatureBase64 = signRes.signature;

      // Step 3: Submit signature to Canton to allocate the party
      const allocRes = await fetch("/api/canton/allocate", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ signatureBase64, generatedParty }),
      });
      const { partyId: pid } = await allocRes.json();
      setPartyId(pid);
    } catch (err) {
      setError(err instanceof Error ? err : new Error("Onboarding failed"));
    } finally {
      setIsPending(false);
    }
  }, [address, walletId, signMessageAsync]);

  return { onboard, address, partyId, multiHash, isPending, error };
}
```

### 5. Wire up the UI

```typescript theme={null}
"use client";

import { useModal, useAccount } from "@getpara/react-sdk";
import { useCantonOnboarding } from "@/hooks/useCantonOnboarding";

export default function Home() {
  const { openModal } = useModal();
  const { isConnected } = useAccount();
  const { onboard, address, partyId, multiHash, isPending, error } = useCantonOnboarding();

  if (!isConnected) {
    return <button onClick={openModal}>Connect with Para</button>;
  }

  return (
    <div>
      <p>Solana address: {address ?? "Selecting Solana wallet…"}</p>
      <button onClick={onboard} disabled={!address || isPending || Boolean(partyId)}>
        {isPending ? "Onboarding…" : partyId ? "Onboarded" : "Onboard as Canton external party"}
      </button>
      {error && <p style={{ color: "red" }}>{error.message}</p>}
      {multiHash && <p>Signed multiHash: {multiHash}</p>}
      {partyId && <p>Canton partyId: {partyId}</p>}
    </div>
  );
}
```

## Environment variables

```bash theme={null}
# Client
NEXT_PUBLIC_PARA_API_KEY=your_para_api_key
NEXT_PUBLIC_PARA_ENVIRONMENT=BETA

# Server-only (never prefix with NEXT_PUBLIC_)
LEDGER_API_URL=http://localhost:2975
VALIDATOR_API_URL=http://localhost:2903/api/validator
VALIDATOR_AUDIENCE=https://canton.network.global
AUTH_USER_ID=ledger-api-user
AUTH_UNSAFE_SECRET=unsafe
```

These defaults target the app-user node of a [Splice LocalNet](https://docs.dev.sync.global/app_dev/testing/localnet.html) docker-compose stack. For a hosted Canton deployment, update the URLs, swap `localNetAuthDefault` for the appropriate auth factory, and set `VALIDATOR_AUDIENCE` to whatever your validator expects.

## Complete example

A fully working Next.js app with this flow, including styled UI and error handling, is available in the Para Examples Hub:

<Card title="Canton External Party Example" icon="code" href="https://github.com/getpara/examples-hub/tree/3.0.0/web/with-react-nextjs/signer-canton-network">
  Next.js + ParaModal + Canton Network — generate, sign, and allocate an external party on the Canton ledger
</Card>

## Related walkthroughs

<CardGroup cols={3}>
  <Card title="Solana Transfers" icon="circle-dot" href="/v3/walkthroughs/solana-transfers">
    Intermediate · 25 min · Send SOL with Web3.js, Signers v2, and Anchor
  </Card>

  <Card title="Ethereum Transfers" icon="ethereum" href="/v3/walkthroughs/ethereum-transfers">
    Intermediate · 20 min · Send ETH with Ethers and Viem
  </Card>

  <Card title="EIP-712 Typed Data Signing" icon="file-signature" href="/v3/walkthroughs/eip712-typed-data-signing">
    Intermediate · 20 min · Structured data signing
  </Card>
</CardGroup>
