Privy Pre-sign Simulation
This guide uses to @shield3/react-sdk
to simulate transactions before signing. This allows your app to easily screen transactions before signing with the embedded wallet or sending to the wallet client
Introduction
This integration is best suited for apps using Privy with both embedded and external wallets (like Metamask) enabled. If your app only uses Privy's embedded wallet, we recommend Privy RPC Substitution, as it is simpler integration and does not require as many changes to your existing code.
Test out the integration here!
Integration Plan
For the final code, please visit our working example app repository. This guide will show you how to start a new project to use verifications provided by Shield3 by guiding you through how we integrated with Privy's auth-demo app. Pre-existing projects should be able to find relevant information here as well.
Before starting, make sure to have your Privy App ID (you can get it here) and your Shield3 API Key (found here). You should also navigate to the workflows section and enable the policy for blocking unknown contract calls.
Setting up
To set up, we will need to do the following:
- Clone the auth-demo repository
- Configure env variables
- Install packages
# 1. Cloning the repository
git clone https://github.com/privy-io/auth-demo.git
# 2. Copy the .env.example.local file into .env.local
cd auth-demo
cp .env.example.local .env.local
# Now set NEXT_PUBLIC_SHIELD3_API_KEY and NEXT_PUBLIC_PRIVY_APP_ID
3. Install Packages
npm i
Integrating and testing
- Install the Shield3 React SDK
# 1. Install the Shield3 React SDK
## Make sure you are at the root of the auth-demo repository
npm install @shield3/react-sdk
- Wrap your app with
Shield3Provider()
Here is the before and after.
// Before
...
return (
...
<PlausibleProvider domain="demo.privy.io">
<PrivyConfigContext.Provider value={{config, setConfig: setConfigWithAppearanceStorage}}>
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ''}
// @ts-expect-error internal api
apiUrl={process.env.NEXT_PUBLIC_PRIVY_AUTH_URL}
onSuccess={setDatadogUser}
config={config}
>
<Component {...pageProps} />
<div className='h-6'/>
</PrivyProvider>
</PrivyConfigContext.Provider>
</PlausibleProvider>
</>
);
}
export default MyApp;
// After
...
return (
...
<Shield3Provider apiKey={process.env.NEXT_PUBLIC_SHIELD3_API_KEY as string} chainId={11155111}>
<PlausibleProvider domain="demo.privy.io">
<PrivyConfigContext.Provider value={{config, setConfig: setConfigWithAppearanceStorage}}>
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ''}
// @ts-expect-error internal api
apiUrl={process.env.NEXT_PUBLIC_PRIVY_AUTH_URL}
onSuccess={setDatadogUser}
config={config}
>
<Component {...pageProps} />
<div className='h-6'/>
</PrivyProvider>
</PrivyConfigContext.Provider>
</PlausibleProvider>
</Shield3Provider>
</>
);
}
export default MyApp;
- Moving over to Sepolia
import { sepolia } from 'viem/chains';
...
// On line 35 and 36:
defaultChain:sepolia,
supportedChains:[sepolia]
If you have already run and signed in, you should sign out, and restart the app to update to the new configuration.
- Verifying transactions
"use client"
import {useWallets, usePrivy} from '@privy-io/react-auth';
import {useShield3Context} from "@shield3/react-sdk"
import type { RoutingDecision } from '@shield3/react-sdk/dist/shield3/simulate'
import React, { useState, useEffect } from 'react'
import type { WalletWithMetadata } from '@privy-io/react-auth';
import { useRouter } from "next/router";
import { type WalletClient, createWalletClient, custom } from 'viem'
import { sepolia } from 'viem/chains'
interface Transaction {
to: string;
data?: string;
nonce?: number;
value: number;
chainId: number;
gasLimit: number;
[key: string]: unknown; // Allows any number of other unknown keys
}
const exampleFlaggedTx = {
to: '0x6aabdd49a7f97f5242fd0fd6938987e039827666',
data: '0xa9059cbb0000000000000000000000006aabdd49a7f97f5242fd0fd6938987e03982766600000000000000000000000000000000000000000000000001e32b4789740000',
value: 0,
chainId: 1,
gasLimit: 100000,
}
const Signer = () => {
const {user, sendTransaction, ready, authenticated }=usePrivy()
const [receiptUrl,setReceiptUrl]=useState<string|null>(null)
let receipt=null
let client:null|WalletClient=null
const router = useRouter();
useEffect(() => {
if (!ready || !authenticated) router.push("/");
}, [ready, authenticated, router.push]);
const {wallets: connectedWallets} = useWallets();
const linkedAccounts = user?.linkedAccounts || [];
const wallets = linkedAccounts.filter((a) => a.type === 'wallet') as WalletWithMetadata[];
const linkedAndConnectedWallets = wallets
.filter((w) => connectedWallets.some((cw) => cw.address === w.address))
.sort((a, b) => b.firstVerifiedAt.toLocaleString().localeCompare(a.firstVerifiedAt.toLocaleString()));
const account=linkedAndConnectedWallets[0]?.address as `0x${string}`
const { shield3Client } = useShield3Context()
const [result, setResult] = useState<RoutingDecision| string | null>(null)
const [response, setResponse] = useState("")
const uiConfig = {
header: 'Send ETH (sepolia)',
description: 'Send a protected transaction',
buttonText: 'Submit'
};
const sign = async (isBlocked:boolean) => {
setResult("Getting Results...")
let transaction:Transaction
if (isBlocked) transaction = exampleFlaggedTx
else
transaction = {
to: account,
value: 0,
chainId: 11155111,
gasLimit: 100000,
}
console.log({ transaction, account })
const results = await shield3Client.getPolicyResults(transaction, account )
const decision =results?.decision ?? null
setResult(decision)
setResponse(JSON.stringify(results,null,2))
if (decision==='Allow'){
try {
if (user?.wallet?.walletClientType==='privy') {
receipt=(await sendTransaction(transaction, uiConfig)).transactionHash as `0x${string}`}
else {
client=client ?
client :
createWalletClient({
account:account as `0x${string}`,
chain: sepolia,
transport: custom(window.ethereum)
})
const prepped_tx=await client.prepareTransactionRequest(transaction)
console.log(prepped_tx)
receipt=await client.sendTransaction(prepped_tx)
}
setReceiptUrl(`https://sepolia.etherscan.io/tx/${receipt}`)
}
catch (error){console.log(error)}
}
}
return (
<div className='flex flex-col gap-2'>
<div className='flex flex-col items-center gap-2'>
<button className="button px-4 w-full h-10" type="button" onClick={() => sign(true)}>Try flagged transaction</button>
<button className="button px-4 w-full h-10" type="button" onClick={() => sign(false)}>Try allowed transaction</button>
</div>
{result && (
<div className='p-2 transform-all duration-1000 flex flex-col w-full border border-privy-3 h-auto rounded-xl items-center text-center'>
<h2 className='text-privy-color-foreground-3 text-xs'>Policy Result</h2>
<h2 className='w-full text-privy-color-foreground-1 text-2xl'>{result}</h2>
{receiptUrl && (
<a className="text-xs" href={receiptUrl} target="_blank" rel="noopener noreferrer">View Transaction</a>
)}
</div>)}
{result && (<div className="p-2 transform-all duration-1000 bg-privy-color-background-2 h-32 hover:h-96 font-mono text-xs text-privy-color-foreground-2 no-scrollbar overflow-auto rounded-xl">
<pre style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
overflow: 'auto',
borderRadius: '8px'
}}>
<code>
{response}
</code>
</pre>
</div>)}
</div>
)
}
export default Signer
Please note the following:
- The presence of "use client"
- Viem is being used to handle transactions with external wallets, wagmi or ethers could also be used.
- Viem is conditionally configured when the wallet type is not embedded. If not, errors may occur
- Privy's sendTransaction increments the nonce (among others), and Viem's prepareTransactionRequest does the same for external wallets.
- Privy's sendTransaction returns an object on which the transaction hash can be found as a property. Viem's sendTransaction simply returns the transaction hash.
- Displaying the Signer component
import Signer from './Signer';
...
// On line 284:
<CanvasCard>
<CanvasCardHeader>
<div className="h-5 w-5 mr-1">
<svg width="20" height="20" viewBox="0 0 481 483" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>Shield3 Image</title>
<path d="M0.0425779 72.0683C-0.00757237 65.185 4.6323 59.1732 11.2609 57.4532L232.672 0V143.391H123.842C117.624 143.391 112.584 148.459 112.584 154.711V192.445C112.584 198.697 117.624 203.766 123.842 203.766H232.672V483C141.277 455.896 85.0503 404.458 50.8349 339.609H131.347C137.565 339.609 142.606 334.541 142.606 328.289V290.555C142.606 284.303 137.565 279.234 131.347 279.234H26.0369C6.18469 215.65 0.567133 144.065 0.0425779 72.0683Z" fill="#F4E065"/>
<path d="M480.312 72.0693C480.362 65.186 475.722 59.1732 469.094 57.4532L247.682 0V143.391H356.512C362.73 143.391 367.771 148.459 367.771 154.711V192.445C367.771 198.697 362.73 203.766 356.512 203.766H247.682V279.234H356.512C362.73 279.234 367.771 284.303 367.771 290.555V328.289C367.771 334.541 362.73 339.609 356.512 339.609H247.682V483C446.888 423.925 479.021 249.241 480.312 72.0693Z" fill="#D5AE56"/>
</svg>
</div>
Get Shield3 Policy Results
</CanvasCardHeader>
<div className="flex shrink-0 grow-0 flex-row items-center justify-start gap-x-1 text-privy-color-foreground-3">
{`Address: ${user.wallet ? formatWallet(user.wallet.address): 'wallet not connected'}`}
</div>
<div className="flex flex-col gap-2 pt-4">
<Signer/>
</div>
</CanvasCard>
Updated 7 months ago