Skip to main content
Integrate Para’s embedded wallets with Porto to combine Para’s authentication and key management with Porto’s smart account features. This guide covers upgrading a Para EOA to a Porto Smart Account while preserving the same address.

What You Get

FeaturePara EOAPorto Smart Account
AddressSingle addressSame address (preserved)
Signing Keys1 (MPC-secured)Multiple authorized keys
Session KeysNot supportedSupported
Batched TransactionsNot supportedSupported
Gas SponsorshipNot supportedSupported
Programmable PermissionsNot supportedSupported

Integration Pattern

Para handles:
  • Authentication (social login, email, SMS)
  • Key custody (MPC security)
  • Cross-app identity
  • Raw message signing
Porto handles:
  • Smart account operations (EIP-7702)
  • Transaction batching
  • Gas sponsorship
  • Session key management

Prerequisites

Installation

npm install @getpara/react-sdk porto viem @tanstack/react-query
Porto currently supports Base Sepolia testnet. Ensure your Para provider is configured for this chain.

Setup Para Provider

Configure the Para provider with Base Sepolia chain support:
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ParaProvider } from "@getpara/react-sdk";
import { baseSepolia } from "wagmi/chains";

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <ParaProvider
        paraClientConfig={{
          apiKey: process.env.NEXT_PUBLIC_PARA_API_KEY!,
          env: "BETA",
        }}
        externalWalletConfig={{
          evmConnector: {
            config: {
              chains: [baseSepolia],
            },
          },
        }}
        config={{ appName: "My App" }}>
        {children}
      </ParaProvider>
    </QueryClientProvider>
  );
}

Core Integration Hook

The usePortoAccount hook handles the integration between Para and Porto.
Raw Signing Required: Para’s viemAccount.signMessage() applies an EIP-191 prefix to messages, but Porto expects raw ECDSA signatures. You must use Para’s client directly with signMessage({ walletId, messageBase64 }) to sign raw bytes.
Porto Relay Endpoint: You must use Porto’s relay RPC (https://rpc.porto.sh), not a standard Base Sepolia RPC. The relay handles Porto-specific methods like wallet_prepareCalls.
"use client";

import { useState, useCallback, useMemo, useEffect } from "react";
import { useViemAccount } from "@getpara/react-sdk/evm";
import { useClient, useWallet, useAccount } from "@getpara/react-sdk";
import { Chains } from "porto";
import { Account, Key, RelayActions } from "porto/viem";
import {
  createClient,
  http,
  parseSignature,
  serializeSignature,
  type Hex,
} from "viem";

function hexToBase64(hex: string): string {
  const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
  const bytes = new Uint8Array(
    cleanHex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))
  );
  return btoa(String.fromCharCode(...bytes));
}

function normalizeSignature(signature: Hex): Hex {
  const parsed = parseSignature(signature);
  return serializeSignature({
    r: parsed.r,
    s: parsed.s,
    yParity: parsed.yParity,
  });
}

export function usePortoAccount() {
  const { viemAccount, isLoading: isViemLoading } = useViemAccount();
  const { isConnected } = useAccount();
  const para = useClient();
  const { data: wallet } = useWallet();

  const [portoAccount, setPortoAccount] = useState<any>(null);
  const [isUpgrading, setIsUpgrading] = useState(false);
  const [isCheckingStatus, setIsCheckingStatus] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const portoClient = useMemo(
    () =>
      createClient({
        chain: Chains.baseSepolia,
        transport: http("https://rpc.porto.sh"),
      }),
    []
  );

  const signRawHash = useCallback(
    async (hash: Hex): Promise<Hex> => {
      if (!para || !wallet?.id) {
        throw new Error("Para client or wallet not available");
      }

      const hashBase64 = hexToBase64(hash);
      const res = await para.signMessage({
        walletId: wallet.id,
        messageBase64: hashBase64,
      });

      const signature = (res as { signature: string }).signature;
      return normalizeSignature(`0x${signature}` as Hex);
    },
    [para, wallet]
  );

  useEffect(() => {
    async function checkPortoStatus() {
      if (!viemAccount?.address || !isConnected) return;

      setIsCheckingStatus(true);
      try {
        const tempAccount = Account.from({ address: viemAccount.address });
        const keys = await RelayActions.getKeys(portoClient, {
          account: tempAccount,
        });

        if (keys.length > 0) {
          const account = Account.from({
            address: viemAccount.address,
            keys: [...keys],
            async sign({ hash }) {
              return signRawHash(hash as Hex);
            },
          });
          setPortoAccount(account);
        }
      } catch {
        // Account not upgraded yet
      } finally {
        setIsCheckingStatus(false);
      }
    }

    checkPortoStatus();
  }, [viemAccount?.address, isConnected, portoClient, signRawHash]);

  const upgradeToPorto = useCallback(async () => {
    if (!viemAccount || !isConnected || !para || !wallet?.id) {
      setError("Para client or wallet not available");
      return;
    }

    setIsUpgrading(true);
    setError(null);

    try {
      const customAccount = Account.from({
        address: viemAccount.address,
        async sign({ hash }) {
          return signRawHash(hash as Hex);
        },
      });

      const adminKey = Key.createSecp256k1({ role: "admin" });

      const prepared = await RelayActions.prepareUpgradeAccount(portoClient, {
        address: customAccount.address,
        authorizeKeys: [adminKey],
      });

      const signatures = {
        auth: await signRawHash(prepared.digests.auth as Hex),
        exec: await signRawHash(prepared.digests.exec as Hex),
      };

      const upgradedAccount = await RelayActions.upgradeAccount(portoClient, {
        ...prepared,
        signatures,
      });

      setPortoAccount(upgradedAccount);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Upgrade failed");
    } finally {
      setIsUpgrading(false);
    }
  }, [viemAccount, isConnected, para, wallet, portoClient, signRawHash]);

  return {
    viemAccount,
    portoAccount,
    portoClient,
    isViemLoading,
    isUpgrading,
    isCheckingStatus,
    error,
    upgradeToPorto,
    isConnected,
  };
}

Upgrade Flow Explained

The upgrade process follows these steps:
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Para Auth      │ ──▶ │  Account.from()  │ ──▶ │  prepareUpgrade │
│  (Get Address)  │     │  (Wrap Signer)   │     │  (Get Digests)  │
└─────────────────┘     └──────────────────┘     └─────────────────┘


┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Porto Smart    │ ◀── │  upgradeAccount  │ ◀── │  Sign Digests   │
│  Account Ready  │     │  (Submit to Relay)│    │  (Raw Signing)  │
└─────────────────┘     └──────────────────┘     └─────────────────┘

Step 1: Wrap Para Signer

Create a Porto account that delegates signing to Para:
const customAccount = Account.from({
  address: viemAccount.address,
  async sign({ hash }) {
    return signRawHash(hash as Hex);
  },
});

Step 2: Create Admin Key

Generate a new admin key for the smart account:
const adminKey = Key.createSecp256k1({ role: "admin" });

Step 3: Prepare Upgrade

Get the digests that need to be signed:
const prepared = await RelayActions.prepareUpgradeAccount(portoClient, {
  address: customAccount.address,
  authorizeKeys: [adminKey],
});

Step 4: Sign Digests

Sign both the auth and exec digests using raw signing:
const signatures = {
  auth: await signRawHash(prepared.digests.auth as Hex),
  exec: await signRawHash(prepared.digests.exec as Hex),
};

Step 5: Complete Upgrade

Submit the signed upgrade to the relay:
const upgradedAccount = await RelayActions.upgradeAccount(portoClient, {
  ...prepared,
  signatures,
});

Usage Example

"use client";

import { usePortoAccount } from "@/hooks/usePortoAccount";

export function PortoDemo() {
  const {
    viemAccount,
    portoAccount,
    isViemLoading,
    isUpgrading,
    isCheckingStatus,
    error,
    upgradeToPorto,
    isConnected,
  } = usePortoAccount();

  if (!isConnected) {
    return <p>Connect your wallet to get started</p>;
  }

  if (isViemLoading || isCheckingStatus) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <div>
        <h2>Para EOA</h2>
        <p>Address: {viemAccount?.address}</p>
        <p>Type: Externally Owned Account</p>
      </div>

      {!portoAccount ? (
        <div>
          <button onClick={upgradeToPorto} disabled={isUpgrading}>
            {isUpgrading ? "Upgrading..." : "Upgrade to Porto Smart Account"}
          </button>
          {error && <p style={{ color: "red" }}>{error}</p>}
        </div>
      ) : (
        <div>
          <h2>Porto Smart Account</h2>
          <p>Address: {portoAccount.address}</p>
          <p>Type: Delegated Smart Account (EIP-7702)</p>
          <p>Authorized Keys: {portoAccount.keys?.length || 0}</p>
        </div>
      )}
    </div>
  );
}

Send Batched Transactions

After upgrading, use Porto’s transaction batching:
"use client";

import { usePortoAccount } from "@/hooks/usePortoAccount";
import { parseEther } from "viem";
import { RelayActions } from "porto/viem";

export function BatchTransaction() {
  const { portoAccount, portoClient } = usePortoAccount();

  async function sendBatch() {
    if (!portoAccount || !portoClient) return;

    const { id } = await RelayActions.sendCalls(portoClient, {
      account: portoAccount,
      calls: [
        {
          to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
          value: parseEther("0.01"),
        },
        {
          to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
          value: parseEther("0.02"),
        },
      ],
    });

    console.log("Batch sent:", id);
  }

  if (!portoAccount) return null;

  return <button onClick={sendBatch}>Send Batch Transaction</button>;
}

Working with Keys

After upgrade, the Porto account has authorized keys. When displaying keys, deduplicate by publicKey since getKeys returns the same key for each supported chain:
const uniqueKeys = portoAccount.keys?.filter(
  (key, index, self) =>
    index === self.findIndex((k) => k.publicKey === key.publicKey)
);
Each key has these properties:
PropertyDescription
typeKey type: secp256k1, p256, or webauthn-p256
roleadmin or session
publicKeyHex-encoded public key
expiryUnix timestamp (0 = never expires)
hashUnique key identifier
permissionsOptional spend limits and call permissions

Common Issues

”Invalid auth item” Error

This occurs when the signature recovery returns the wrong address. Ensure you’re using raw signing:
// Wrong - applies EIP-191 prefix
const sig = await viemAccount.signMessage({ message: { raw: hash } });

// Correct - raw signing via Para client
const sig = await para.signMessage({
  walletId: wallet.id,
  messageBase64: hexToBase64(hash),
});

403 Error on RPC Calls

Standard RPC endpoints don’t support Porto’s methods. Use Porto’s relay:
// Wrong
transport: http("https://sepolia.base.org")

// Correct
transport: http("https://rpc.porto.sh")

TypeScript Error with getKeys

getKeys expects an account parameter (Porto Account object), not address:
// Wrong
const keys = await RelayActions.getKeys(client, { address: "0x..." });

// Correct
const tempAccount = Account.from({ address: "0x..." });
const keys = await RelayActions.getKeys(client, { account: tempAccount });

How It Works

  1. User authenticates with Para (email, social, SMS)
  2. Para creates/manages EOA wallet (MPC-secured)
  3. Porto account created using Account.from() with Para’s raw signing function
  4. Upgrade prepared via RelayActions.prepareUpgradeAccount() returning digests
  5. Digests signed using Para’s raw signing (no EIP-191 prefix)
  6. EOA upgraded via RelayActions.upgradeAccount() with signatures
  7. Transactions executed through Porto using RelayActions.sendCalls()

Next Steps