Web SDK
- Overview
- Getting Started
- Migrate from v4 to v5
- Guides
- Hooks
- Secondary Sales Marketplace
- Custom Configuration
- Custom Connectors
Game Engine SDKs
- Unity
- Unreal
Other SDKs
- Typescript
- Overview
- Backend Integration
- Frontend Integration
- Go
- Mobile
Privy & Sequence
Learn how to use Privy as a signer for your Sequence Smart Wallet.
In this guide, you’ll learn how to use the core sequence.js
libraries to seamlessly integrate Privy with Sequence, enabling your users to sign in and interact with your dApp through a Sequence Smart Wallet. This involves creating a Sequence wallet controlled by a user’s Privy-managed EOA, and then using that Sequence wallet to send gasless transactions on Base Sepolia.
We will be using Next.js 15, React 19, and Tailwind CSS 4.
Install Dependencies
You’ll need the Sequence, Privy, and wagmi/viem packages.
pnpm install @0xsequence/account @0xsequence/core @0xsequence/network @0xsequence/sessions @0xsequence/signhub @privy-io/react-auth @privy-io/wagmi-connector wagmi @privy-io/wagmi @tanstack/react-query viem ethers
Get your Privy App ID & Client ID
You’ll need to get a Privy App ID and Client ID. You can get these by creating a new app in the Privy Dashboard.
Create an .env.local
file in your project root and add your NEXT_PUBLIC_PRIVY_APP_ID
and NEXT_PUBLIC_PRIVY_CLIENT_ID
.
Setup the Providers
Set up WagmiProvider
and PrivyProvider
in your a providers.tsx
file.
We do this to allow our app to use both Privy for authentication and wagmi for wallet interactions.
'use client'
import { type PrivyClientConfig, PrivyProvider } from '@privy-io/react-auth'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createConfig, WagmiProvider } from '@privy-io/wagmi'
import { baseSepolia } from 'viem/chains'
import { http } from 'wagmi'
const queryClient = new QueryClient()
const wagmiConfig = createConfig({
chains: [baseSepolia],
transports: {
[baseSepolia.id]: http()
}
})
const privyConfig: PrivyClientConfig = {
embeddedWallets: {
requireUserPasswordOnCreate: true,
showWalletUIs: true
},
loginMethods: ['wallet', 'email', 'google'],
appearance: {
showWalletLoginFirst: true
},
defaultChain: baseSepolia
}
const APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID
const CLIENT_ID = process.env.NEXT_PUBLIC_PRIVY_CLIENT_ID
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={APP_ID as string}
clientId={CLIENT_ID as string}
config={privyConfig}
>
<QueryClientProvider client={queryClient}>
<WagmiProvider config={wagmiConfig}>
{children}
</WagmiProvider>
</QueryClientProvider>
</PrivyProvider>
)
}
Wrap the App's layout with the Providers
Wrap the App’s layout with the Providers.
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import Providers from './providers'
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin']
})
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin']
})
export const metadata: Metadata = {
title: 'Privy + Sequence',
description: 'A demo showcasing how Sequence can be used with Privy'
}
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Providers>{children}</Providers>
</body>
</html>
)
}
Add transaction types
Add the following types to ./constants/types.ts
.
export type FlatTransaction = {
to: string
value?: string
data?: string
gasLimit?: string
delegateCall?: boolean
revertOnError?: boolean
}
export type TransactionsEntry = {
subdigest?: string
wallet: string
space: string
nonce: string
chainId: string
transactions: FlatTransaction[]
}
Create a StaticSigner class
Create a StaticSigner
class in ./utils/StaticSigner.ts
.
import type { commons } from '@0xsequence/core'
import type { signers } from '@0xsequence/signhub'
import { type BytesLike, ethers } from 'ethers'
type TransactionBundle = commons.transaction.TransactionBundle
type SignedTransactionBundle = commons.transaction.SignedTransactionBundle
type IntendedTransactionBundle = commons.transaction.IntendedTransactionBundle
export class StaticSigner implements signers.SapientSigner {
private readonly signatureBytes: Uint8Array
private readonly savedSuffix: Uint8Array
constructor(
private readonly address: string,
private readonly signature: string
) {
const raw = ethers.getBytes(this.signature)
// Separate last byte as suffix
this.savedSuffix = raw.slice(-1)
this.signatureBytes = raw.slice(0, -1)
}
async buildDeployTransaction(): Promise<TransactionBundle | undefined> {
return undefined
}
async predecorateSignedTransactions(): Promise<SignedTransactionBundle[]> {
return []
}
async decorateTransactions(
og: IntendedTransactionBundle
): Promise<IntendedTransactionBundle> {
return og
}
async sign(): Promise<BytesLike> {
return this.signatureBytes
}
notifyStatusChange(): void {}
suffix(): BytesLike {
return this.savedSuffix
}
async getAddress() {
return this.address
}
}
Add the utility methods
We need a couple of utility methods.
Add this file in ./utils/index.ts
.
import { Account } from '@0xsequence/account'
import { trackers } from '@0xsequence/sessions'
import { commons } from '@0xsequence/core'
import { Orchestrator, signers } from '@0xsequence/signhub'
import { allNetworks } from '@0xsequence/network'
import type { FlatTransaction, TransactionsEntry } from '../constants/types'
import { ethers } from 'ethers'
import { StaticSigner } from './StaticSigner'
export const TRACKER = new trackers.remote.RemoteConfigTracker(
'https://sessions.sequence.app'
)
export const NETWORKS = allNetworks
/**
* Creates a new Sequence Account with the specified threshold and signers.
*
* @param threshold - The minimum weight required to authorize transactions.
* @param signers - An array of signer objects with address and weight.
* @returns A Promise that resolves to the created Account instance.
*/
export async function createSequenceAccount(
threshold: number,
signers: { address: string; weight: number }[]
): Promise<Account> {
const account = await Account.new({
config: {
threshold,
// By default a random checkpoint is generated every second
checkpoint: 0,
signers: signers
},
tracker: TRACKER,
contexts: commons.context.defaultContexts,
orchestrator: new Orchestrator([]),
networks: NETWORKS
})
return account
}
/**
* Converts an array of FlatTransaction objects to Sequence Transaction objects.
*
* @param txs - Array of FlatTransaction objects to convert.
* @returns An array of Sequence Transaction objects.
*/
export function toSequenceTransactions(
txs: FlatTransaction[]
): commons.transaction.Transaction[] {
return txs.map(toSequenceTransaction)
}
/**
* Converts a FlatTransaction object to a Sequence Transaction object.
*
* @param tx - The FlatTransaction object to convert.
* @returns The corresponding Sequence Transaction object.
*/
export function toSequenceTransaction(
tx: FlatTransaction
): commons.transaction.Transaction {
return {
to: tx.to,
value: tx.value ? BigInt(tx.value) : undefined,
data: tx.data,
gasLimit: tx.gasLimit ? BigInt(tx.gasLimit) : undefined,
delegateCall: tx.delegateCall || false,
revertOnError: tx.revertOnError || false
}
}
/**
* Creates an Account instance for a given address and optional signatures.
*
* @param args - Object containing the address and optional signatures array.
* @returns An Account instance configured with the provided signers.
*/
export function accountFor(args: {
address: string
signatures?: { signer: string; signature: string }[]
}) {
const signers: signers.SapientSigner[] = []
if (args.signatures) {
for (const { signer, signature } of args.signatures) {
const signatureArr = ethers.getBytes(signature)
if (
signatureArr.length === 66 &&
(signatureArr[64] === 0 || signatureArr[64] === 1)
) {
signatureArr[64] = signatureArr[64] + 27
}
signers.push(new StaticSigner(signer, ethers.hexlify(signatureArr)))
}
}
return new Account({
address: args.address,
tracker: TRACKER,
contexts: commons.context.defaultContexts,
orchestrator: new Orchestrator(signers),
networks: NETWORKS
})
}
/**
* Computes the digest for a given TransactionsEntry.
*
* @param tx - The TransactionsEntry containing transaction details.
* @returns The digest string for the transactions.
*/
export function digestOf(tx: TransactionsEntry): string {
return commons.transaction.digestOfTransactions(
commons.transaction.encodeNonce(tx.space, tx.nonce),
toSequenceTransactions(tx.transactions)
)
}
/**
* Computes the subdigest for a given TransactionsEntry.
*
* @param tx - The TransactionsEntry containing transaction details.
* @returns The subdigest string for the transactions.
*/
export function subdigestOf(tx: TransactionsEntry): string {
const digest = digestOf(tx)
return commons.signature.subdigestOf({
digest,
chainId: tx.chainId,
address: tx.wallet
})
}
/**
* Converts Sequence Transactionish objects to an array of FlatTransaction objects.
*
* @param wallet - The wallet address associated with the transactions.
* @param txs - The Sequence Transactionish object(s) to convert.
* @returns An array of FlatTransaction objects.
*/
export function fromSequenceTransactions(
wallet: string,
txs: commons.transaction.Transactionish
): FlatTransaction[] {
const sequenceTxs = commons.transaction.fromTransactionish(wallet, txs)
return sequenceTxs.map((stx) => ({
to: stx.to,
value: stx.value?.toString(),
data: stx.data?.toString(),
gasLimit: stx.gasLimit?.toString(),
delegateCall: stx.delegateCall,
revertOnError: stx.revertOnError
}))
}
/**
* Recovers the signer addresses from an array of signatures and a subdigest.
*
* @param signatures - Array of signature strings to recover signers from.
* @param subdigest - The subdigest string used for recovery.
* @returns An array of objects containing the signer address and signature.
*/
export function recoverSigner(
signatures: string[],
subdigest: string
): { signer: string; signature: string }[] {
const res: { signer: string; signature: string }[] = []
for (const signature of signatures) {
try {
const r = commons.signer.recoverSigner(subdigest, signature)
res.push({ signer: r, signature: signature })
} catch (e) {
console.error('Failed to recover signature', e)
}
}
return res
}
Create a Sequence Wallet and send a gasless transaction
"use client"
import { usePublicClient, useSignMessage } from "wagmi"
import { accountFor, createSequenceAccount, subdigestOf, toSequenceTransactions } from "./utils"
import { useState, useEffect } from "react"
import { commons } from "@0xsequence/core"
import { ethers } from "ethers"
import { zeroAddress } from "viem"
import { usePrivy } from "@privy-io/react-auth"
const CHAIN_ID = 84532
export default function Home() {
const { ready, authenticated, login, logout, user } = usePrivy()
const publicClient = usePublicClient({ chainId: CHAIN_ID })
const { signMessageAsync } = useSignMessage()
const [walletAddress, setWalletAddress] = useState<`0x${string}` | null>(null)
const [txHash, setTxHash] = useState<string | null>(null)
const [loadingSendTx, setLoadingSendTx] = useState(false)
const [isWalletDeployed, setIsWalletDeployed] = useState(false)
const [checkingWalletDeployed, setCheckingWalletDeployed] = useState(true)
useEffect(() => {
const createWallet = async () => {
if (user?.wallet && user.wallet.address) {
const seqeunceAccount = await createSequenceAccount(1, [
{ address: user.wallet.address, weight: 1 },
])
const accountWithSig = accountFor({
address: seqeunceAccount.address,
})
const status = await accountWithSig.status(CHAIN_ID)
const wallet = accountWithSig.walletForStatus(CHAIN_ID, status)
setCheckingWalletDeployed(true)
const hasCode = await publicClient?.getCode({ address: accountWithSig.address as `0x${string}` })
setCheckingWalletDeployed(false)
if (!hasCode) {
wallet.deploy()
// Wait for the wallet to be deploy, most of the times it takes less than 4 seconds
await new Promise((resolve) => setTimeout(resolve, 4000))
}
setWalletAddress(wallet.address as `0x${string}`)
setIsWalletDeployed(true)
setCheckingWalletDeployed(false)
} else {
setWalletAddress(null)
setTxHash(null)
}
}
createWallet()
}, [user])
const handleSend = async () => {
if (!user?.wallet?.address || !walletAddress) return
setLoadingSendTx(true)
const txs = [
{ to: zeroAddress, data: "0x", value: "0", revertOnError: true },
]
const txe = {
wallet: walletAddress,
space: Date.now().toString(),
nonce: "0",
chainId: CHAIN_ID.toString(),
transactions: txs,
}
const subdigest = subdigestOf(txe)
const digestBytes = ethers.getBytes(subdigest)
const signature = await signMessageAsync({ message: { raw: digestBytes } })
const suffixed = signature + "02"
const account = accountFor({
address: walletAddress,
signatures: [
{ signer: user.wallet.address as `0x${string}`, signature: suffixed },
],
})
const sequenceTxs = toSequenceTransactions(txs)
const status = await account.status(CHAIN_ID)
const wallet = account.walletForStatus(CHAIN_ID, status)
const signed = await wallet.signTransactions(
sequenceTxs,
commons.transaction.encodeNonce(txe.space, txe.nonce)
)
const relayer = account.relayer(CHAIN_ID)
const relayed = await relayer.relay(signed)
setTxHash(relayed?.hash || null)
setLoadingSendTx(false)
}
if (!ready)
return (
<div className="flex items-center justify-center min-h-screen">
<span className="text-xs text-gray-400">Loading Privy...</span>
</div>
)
return (
<main className="flex flex-col items-center justify-center min-h-screen gap-6">
<button
onClick={authenticated ? logout : login}
aria-label={authenticated ? "Log out" : "Log in with Privy"}
tabIndex={0}
className="px-4 py-2 border border-white rounded text-white font-semibold bg-black hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 transition disabled:opacity-50 text-sm"
onKeyDown={e => {
if ((e.key === "Enter" || e.key === " ") && ready) {
if (authenticated) {
logout()
} else {
login()
}
}
}}
>
{authenticated ? "Log out" : "Log in with Privy"}
</button>
{isWalletDeployed ? (
<div className="text-center">
<div className="text-xs text-gray-400 mb-2">Smart Wallet Address</div>
<div className="font-mono text-sm break-all">{walletAddress}</div>
</div>
) : (
<div className="text-center">
{checkingWalletDeployed ? (
<div className="text-xs text-gray-400 mb-2">Checking if wallet is deployed...</div>
) : (
<div className="text-xs text-gray-400 mb-2">Deploying Sequence Smart Wallet...</div>
)}
</div>
)}
{walletAddress && (
<button
onClick={handleSend}
aria-label="Send gasless transaction"
tabIndex={0}
disabled={loadingSendTx || !isWalletDeployed}
className="px-4 py-2 border border-black rounded text-black font-semibold bg-white hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2 transition text-sm"
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") handleSend()
}}
>
{loadingSendTx ? "Sending..." : "Send gasless transaction"}
</button>
)}
{txHash && (
<div className="text-center">
<div className="text-xs text-gray-400 mb-2">Transaction Hash</div>
<div className="font-mono text-sm break-all">{txHash}</div>
</div>
)}
</main>
)
}
Run the app
pnpm dev
Was this page helpful?