diff --git a/docs.json b/docs.json index fb35d22e..795acda9 100644 --- a/docs.json +++ b/docs.json @@ -469,7 +469,8 @@ "sdk/headless-wallet/on-ramp", "sdk/headless-wallet/fee-options", "sdk/headless-wallet/verification", - "sdk/headless-wallet/transaction-receipts" + "sdk/headless-wallet/transaction-receipts", + "sdk/headless-wallet/use-with-privy" ] } ] @@ -838,7 +839,8 @@ "guides/typed-on-chain-signatures", "guides/building-relaying-server", "guides/analytics-guide", - "guides/build-embedding-wallet" + "guides/build-embedding-wallet", + "guides/use-with-privy" ] }, { @@ -1253,7 +1255,8 @@ "ja/sdk/headless-wallet/on-ramp", "ja/sdk/headless-wallet/fee-options", "ja/sdk/headless-wallet/verification", - "ja/sdk/headless-wallet/transaction-receipts" + "ja/sdk/headless-wallet/transaction-receipts", + "ja/sdk/headless-wallet/use-with-privy" ] } ] @@ -2054,7 +2057,8 @@ "es/sdk/headless-wallet/on-ramp", "es/sdk/headless-wallet/fee-options", "es/sdk/headless-wallet/verification", - "es/sdk/headless-wallet/transaction-receipts" + "es/sdk/headless-wallet/transaction-receipts", + "es/sdk/headless-wallet/use-with-privy" ] } ] @@ -2591,6 +2595,10 @@ { "source": "/solutions/builder/overview", "destination": "/solutions/getting-started" + }, + { + "source": "/solutions/wallets/embedded-wallet/examples/use-with-privy", + "destination": "/sdk/headless-wallet/use-with-privy" } ], "theme": "mint" diff --git a/es/sdk/headless-wallet/use-with-privy.mdx b/es/sdk/headless-wallet/use-with-privy.mdx new file mode 100644 index 00000000..c97faa50 --- /dev/null +++ b/es/sdk/headless-wallet/use-with-privy.mdx @@ -0,0 +1,558 @@ +--- +title: Privy y Sequence +description: Aprenda cómo usar Privy como firmante para su Sequence Smart Wallet. +--- + +En esta guía, aprenderá a utilizar las bibliotecas principales de `sequence.js` para integrar Privy con Sequence de manera sencilla, permitiendo que sus usuarios inicien sesión e interactúen con su dApp a través de una Sequence Smart Wallet. Esto implica crear una wallet de Sequence controlada por la EOA gestionada por Privy del usuario, y luego usar esa wallet de Sequence para enviar transacciones sin gas en Base Sepolia. + + + Usaremos Next.js 15, React 19 y Tailwind CSS 4. + + + + + Necesitará los paquetes de Sequence, Privy y wagmi/viem. + + ```bash + 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 + ``` + + + + Necesitará obtener un Privy App ID y un Client ID. Puede conseguirlos creando una nueva aplicación en el [Privy Dashboard](https://dashboard.privy.io/apps). + Cree un archivo `.env.local` en la raíz de su proyecto y agregue sus valores de `NEXT_PUBLIC_PRIVY_APP_ID` y `NEXT_PUBLIC_PRIVY_CLIENT_ID`. + + + + Configure `WagmiProvider` y `PrivyProvider` en un archivo `providers.tsx`. + Esto nos permite que la aplicación use tanto Privy para autenticación como wagmi para interacciones con wallets. + + ```tsx [providers.tsx] + '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 ( + + + + {children} + + + + ) + } + ``` + + + + Envuelva el layout de la App con los Providers. + + ```tsx [layout.tsx] + 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 ( + + + {children} + + + ) + } + ``` + + + + Agregue los siguientes tipos en `./constants/types.ts`. + + ```tsx [./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[] + } + ``` + + + + Cree una clase `StaticSigner` en `./utils/StaticSigner.ts`. + + ```tsx [./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 { + return undefined + } + + async predecorateSignedTransactions(): Promise { + return [] + } + + async decorateTransactions( + og: IntendedTransactionBundle + ): Promise { + return og + } + + async sign(): Promise { + return this.signatureBytes + } + + notifyStatusChange(): void {} + + suffix(): BytesLike { + return this.savedSuffix + } + + async getAddress() { + return this.address + } + } + ``` + + + + Necesitamos algunos métodos utilitarios. + + Agregue este archivo en `./utils/index.ts`. + + ```tsx [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 { + 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 + } + ``` + + + + ```tsx [page.tsx] + "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(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 ( +
+ Loading Privy... +
+ ) + + return ( +
+ + {isWalletDeployed ? ( +
+
Smart Wallet Address
+
{walletAddress}
+
+ ) : ( +
+ {checkingWalletDeployed ? ( +
Checking if wallet is deployed...
+ ) : ( +
Deploying Sequence Smart Wallet...
+ )} +
+ )} + {walletAddress && ( + + )} + {txHash && ( +
+
Transaction Hash
+
{txHash}
+
+ )} +
+ ) + } + ``` +
+ + + ```bash + pnpm dev + ``` + +
\ No newline at end of file diff --git a/guides/guide-cards.json b/guides/guide-cards.json index fec939d1..1d88957c 100644 --- a/guides/guide-cards.json +++ b/guides/guide-cards.json @@ -1,6 +1,6 @@ { - "lastUpdated": "2025-06-04T08:39:42.458Z", - "totalCards": 11, + "lastUpdated": "2025-06-26T17:51:44.532Z", + "totalCards": 12, "sections": [ { "title": "Game Developers", @@ -58,6 +58,12 @@ "href": "/guides/mint-collectibles-serverless", "description": "Leveraging Sequence's Transaction API and a serverless environment, you will build a scalable minting service for NFT mints or any other transactions that automatically handles blockchain complexities like reorgs, nonce management, and transaction parallelization." }, + { + "title": "Use Privy with Sequence", + "img": "/images/guides/overview/privy_logo.png", + "href": "/guides/use-with-privy", + "description": "Learn how to connect Privy with Sequence." + }, { "title": "Creating a Custom Marketplace with Sequence", "img": "/images/guides/overview/marketplace.png", diff --git a/guides/guide-overview.mdx b/guides/guide-overview.mdx index 4a524199..c5662c88 100644 --- a/guides/guide-overview.mdx +++ b/guides/guide-overview.mdx @@ -75,6 +75,14 @@ Follow our step-by-step guides and open source code templates to accelerate your Leveraging Sequence's Transaction API and a serverless environment, you will build a scalable minting service for NFT mints or any other transactions that automatically handles blockchain complexities like reorgs, nonce management, and transaction parallelization. + + Learn how to connect Privy with Sequence. + + +We will be using Next.js 15, React 19, and Tailwind CSS 4. + + + + + +You'll need the Sequence, Privy, and wagmi/viem packages. + +```bash +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 +``` + + + +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](https://dashboard.privy.io/apps). +Create an `.env.local` file in your project root and add your `NEXT_PUBLIC_PRIVY_APP_ID` and `NEXT_PUBLIC_PRIVY_CLIENT_ID`. + + + +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. + +```tsx [providers.tsx] +'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 ( + + + + {children} + + + + ) +} +``` + + + +Wrap the App's layout with the Providers. + +```tsx [layout.tsx] +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 ( + + + {children} + + + ) +} +``` + + + +Add the following types to `./constants/types.ts`. + +```tsx [./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 in `./utils/StaticSigner.ts`. + +```tsx [./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 { + return undefined + } + + async predecorateSignedTransactions(): Promise { + return [] + } + + async decorateTransactions( + og: IntendedTransactionBundle + ): Promise { + return og + } + + async sign(): Promise { + return this.signatureBytes + } + + notifyStatusChange(): void {} + + suffix(): BytesLike { + return this.savedSuffix + } + + async getAddress() { + return this.address + } +} +``` + + + +We need a couple of utility methods. + +Add this file in `./utils/index.ts`. + +```tsx [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 { + 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 +} +``` + + + + +```tsx [page.tsx] +"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(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 ( +
+ Loading Privy... +
+ ) + + return ( +
+ + {isWalletDeployed ? ( +
+
Smart Wallet Address
+
{walletAddress}
+
+ ) : ( +
+ {checkingWalletDeployed ? ( +
Checking if wallet is deployed...
+ ) : ( +
Deploying Sequence Smart Wallet...
+ )} +
+ )} + {walletAddress && ( + + )} + {txHash && ( +
+
Transaction Hash
+
{txHash}
+
+ )} +
+ ) +} +``` +
+ + + +```bash +pnpm dev +``` + +
\ No newline at end of file diff --git a/images/guides/overview/privy_logo.png b/images/guides/overview/privy_logo.png new file mode 100644 index 00000000..b53dcc64 Binary files /dev/null and b/images/guides/overview/privy_logo.png differ diff --git a/ja/sdk/headless-wallet/use-with-privy.mdx b/ja/sdk/headless-wallet/use-with-privy.mdx new file mode 100644 index 00000000..5247e77f --- /dev/null +++ b/ja/sdk/headless-wallet/use-with-privy.mdx @@ -0,0 +1,558 @@ +--- +title: Privy と Sequence +description: Privy を Sequence スマートウォレットの署名者として利用する方法をご紹介します。 +--- + +このガイドでは、主要な `sequence.js` ライブラリを使って Privy と Sequence をシームレスに統合し、ユーザーが Sequence スマートウォレットを通じて dApp にサインインし操作できるようにする方法を解説します。具体的には、ユーザーの Privy で管理された EOA によって制御される Sequence ウォレットを作成し、そのウォレットを使って Base Sepolia 上でガスレス取引を行います。 + + + Next.js 15、React 19、Tailwind CSS 4 を使用します。 + + + + + Sequence、Privy、wagmi/viem の各パッケージが必要です。 + + ```bash + 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 + ``` + + + + Privy App ID と Client ID を取得する必要があります。[Privy Dashboard](https://dashboard.privy.io/apps) で新しいアプリを作成すると取得できます。 + プロジェクトのルートに `.env.local` ファイルを作成し、`NEXT_PUBLIC_PRIVY_APP_ID` と `NEXT_PUBLIC_PRIVY_CLIENT_ID` を追加してください。 + + + + `providers.tsx` ファイルで `WagmiProvider` と `PrivyProvider` をセットアップします。 + これにより、アプリで Privy を認証に、wagmi をウォレット連携に利用できるようになります。 + + ```tsx [providers.tsx] + '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 ( + + + + {children} + + + + ) + } + ``` + + + + アプリのレイアウト全体をプロバイダーでラップします。 + + ```tsx [layout.tsx] + 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 ( + + + {children} + + + ) + } + ``` + + + + `./constants/types.ts` に以下の型を追加してください。 + + ```tsx [./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[] + } + ``` + + + + `./utils/StaticSigner.ts` に `StaticSigner` クラスを作成します。 + + ```tsx [./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 { + return undefined + } + + async predecorateSignedTransactions(): Promise { + return [] + } + + async decorateTransactions( + og: IntendedTransactionBundle + ): Promise { + return og + } + + async sign(): Promise { + return this.signatureBytes + } + + notifyStatusChange(): void {} + + suffix(): BytesLike { + return this.savedSuffix + } + + async getAddress() { + return this.address + } + } + ``` + + + + いくつかのユーティリティメソッドが必要です。 + + このファイルを `./utils/index.ts` に追加してください。 + + ```tsx [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 { + 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 + } + ``` + + + + ```tsx [page.tsx] + "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(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 ( +
+ Loading Privy... +
+ ) + + return ( +
+ + {isWalletDeployed ? ( +
+
Smart Wallet Address
+
{walletAddress}
+
+ ) : ( +
+ {checkingWalletDeployed ? ( +
Checking if wallet is deployed...
+ ) : ( +
Deploying Sequence Smart Wallet...
+ )} +
+ )} + {walletAddress && ( + + )} + {txHash && ( +
+
Transaction Hash
+
{txHash}
+
+ )} +
+ ) + } + ``` +
+ + + ```bash + pnpm dev + ``` + +
\ No newline at end of file diff --git a/sdk/headless-wallet/use-with-privy.mdx b/sdk/headless-wallet/use-with-privy.mdx new file mode 100644 index 00000000..91ef1552 --- /dev/null +++ b/sdk/headless-wallet/use-with-privy.mdx @@ -0,0 +1,561 @@ +--- +title: "Privy & Sequence" +description: "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. + + + + + +You'll need the Sequence, Privy, and wagmi/viem packages. + +```bash +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 +``` + + + +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](https://dashboard.privy.io/apps). +Create an `.env.local` file in your project root and add your `NEXT_PUBLIC_PRIVY_APP_ID` and `NEXT_PUBLIC_PRIVY_CLIENT_ID`. + + + +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. + +```tsx [providers.tsx] +'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 ( + + + + {children} + + + + ) +} +``` + + + +Wrap the App's layout with the Providers. + +```tsx [layout.tsx] +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 ( + + + {children} + + + ) +} +``` + + + +Add the following types to `./constants/types.ts`. + +```tsx [./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 in `./utils/StaticSigner.ts`. + +```tsx [./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 { + return undefined + } + + async predecorateSignedTransactions(): Promise { + return [] + } + + async decorateTransactions( + og: IntendedTransactionBundle + ): Promise { + return og + } + + async sign(): Promise { + return this.signatureBytes + } + + notifyStatusChange(): void {} + + suffix(): BytesLike { + return this.savedSuffix + } + + async getAddress() { + return this.address + } +} +``` + + + +We need a couple of utility methods. + +Add this file in `./utils/index.ts`. + +```tsx [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 { + 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 +} +``` + + + + +```tsx [page.tsx] +"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(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 ( +
+ Loading Privy... +
+ ) + + return ( +
+ + {isWalletDeployed ? ( +
+
Smart Wallet Address
+
{walletAddress}
+
+ ) : ( +
+ {checkingWalletDeployed ? ( +
Checking if wallet is deployed...
+ ) : ( +
Deploying Sequence Smart Wallet...
+ )} +
+ )} + {walletAddress && ( + + )} + {txHash && ( +
+
Transaction Hash
+
{txHash}
+
+ )} +
+ ) +} +``` +
+ + + +```bash +pnpm dev +``` + +
\ No newline at end of file