|
| 1 | +import * as React from "react" |
| 2 | +import * as ReactDOM from "react-dom/client" |
| 3 | +import { polygonAmoy } from "viem/chains" |
| 4 | +import Web3 from "web3" |
| 5 | + |
| 6 | +import { type Hex, createClient, createPublicClient, http } from "viem" |
| 7 | +import { |
| 8 | + type P256Credential, |
| 9 | + type SmartAccount, |
| 10 | + WebAuthnAccount, |
| 11 | + createBundlerClient, |
| 12 | + toWebAuthnAccount, |
| 13 | +} from "viem/account-abstraction" |
| 14 | +import { |
| 15 | + EIP1193Provider, |
| 16 | + WebAuthnMode, |
| 17 | + toCircleSmartAccount, |
| 18 | + toModularTransport, |
| 19 | + toPasskeyTransport, |
| 20 | + toWebAuthnCredential, |
| 21 | +} from "@circle-fin/modular-wallets-core" |
| 22 | + |
| 23 | +const clientKey = import.meta.env.VITE_CLIENT_KEY as string |
| 24 | +const clientUrl = import.meta.env.VITE_CLIENT_URL as string |
| 25 | +const publicRpcUrl = import.meta.env.VITE_PUBLIC_RPC_URL as string |
| 26 | + |
| 27 | +// Create Circle transports |
| 28 | +const passkeyTransport = toPasskeyTransport(clientUrl, clientKey) |
| 29 | +const modularTransport = toModularTransport(`${clientUrl}/polygonAmoy`, clientKey) |
| 30 | + |
| 31 | +// Create a public client |
| 32 | +const client = createClient({ |
| 33 | + chain: polygonAmoy, |
| 34 | + transport: modularTransport, |
| 35 | +}) |
| 36 | + |
| 37 | +function Example() { |
| 38 | + const [account, setAccount] = React.useState<SmartAccount>() |
| 39 | + const [credential, setCredential] = React.useState<P256Credential>(() => |
| 40 | + JSON.parse(localStorage.getItem("credential") || "null") |
| 41 | + ) |
| 42 | + |
| 43 | + const [web3, setWeb3] = React.useState<Web3>() |
| 44 | + const [hash, setHash] = React.useState<Hex>() |
| 45 | + const [address, setAddress] = React.useState<string>() |
| 46 | + const [personalSignature, setPersonalSignature] = React.useState<string>() |
| 47 | + const [typedDataSignature, setTypedDataSignature] = React.useState<string>() |
| 48 | + |
| 49 | + React.useEffect(() => { |
| 50 | + if (!credential) return |
| 51 | + |
| 52 | + init() |
| 53 | + |
| 54 | + async function init() { |
| 55 | + // Create a circle smart account |
| 56 | + const account = await toCircleSmartAccount({ |
| 57 | + client, |
| 58 | + owner: toWebAuthnAccount({ credential }) as WebAuthnAccount, |
| 59 | + }) |
| 60 | + |
| 61 | + setAccount(account) |
| 62 | + |
| 63 | + const publicClientInstance = createPublicClient({ |
| 64 | + chain: polygonAmoy, |
| 65 | + transport: http(publicRpcUrl), |
| 66 | + }) |
| 67 | + const bundlerClientInstance = createBundlerClient({ |
| 68 | + account, |
| 69 | + chain: polygonAmoy, |
| 70 | + transport: modularTransport, |
| 71 | + }) |
| 72 | + |
| 73 | + const provider = new EIP1193Provider(bundlerClientInstance, publicClientInstance) |
| 74 | + setWeb3(new Web3(provider)) |
| 75 | + } |
| 76 | + }, [credential]) |
| 77 | + |
| 78 | + const register = async () => { |
| 79 | + const username = (document.getElementById("username") as HTMLInputElement).value |
| 80 | + const credential = await toWebAuthnCredential({ |
| 81 | + transport: passkeyTransport, |
| 82 | + mode: WebAuthnMode.Register, |
| 83 | + username, |
| 84 | + }) |
| 85 | + localStorage.setItem("credential", JSON.stringify(credential)) |
| 86 | + setCredential(credential) |
| 87 | + } |
| 88 | + |
| 89 | + const login = async () => { |
| 90 | + const credential = await toWebAuthnCredential({ |
| 91 | + transport: passkeyTransport, |
| 92 | + mode: WebAuthnMode.Login, |
| 93 | + }) |
| 94 | + localStorage.setItem("credential", JSON.stringify(credential)) |
| 95 | + setCredential(credential) |
| 96 | + } |
| 97 | + |
| 98 | + const getProviderAddress = async () => { |
| 99 | + if (!web3) return |
| 100 | + |
| 101 | + const accounts = await web3.eth.getAccounts() |
| 102 | + |
| 103 | + setAddress(accounts[0]) |
| 104 | + } |
| 105 | + |
| 106 | + const signPersonalMessage = async () => { |
| 107 | + if (!web3) return |
| 108 | + |
| 109 | + const accounts = await web3.eth.getAccounts() |
| 110 | + const signature = await web3.eth.personal.sign("Hello World", accounts[0], "passphrase") |
| 111 | + |
| 112 | + setPersonalSignature(signature) |
| 113 | + } |
| 114 | + |
| 115 | + const sendTx = async (event: React.FormEvent<HTMLFormElement>) => { |
| 116 | + event.preventDefault() |
| 117 | + |
| 118 | + if (!web3) return |
| 119 | + |
| 120 | + const formData = new FormData(event.currentTarget) |
| 121 | + const to = formData.get("to") as `0x${string}` |
| 122 | + const value = formData.get("value") as string |
| 123 | + |
| 124 | + try { |
| 125 | + const suggestedGasPrice = ((await web3.eth.getGasPrice()) * 11n) / 10n // 10% higher than the current gas price to ensure the transaction goes through |
| 126 | + |
| 127 | + // Send tokens to the address that was input |
| 128 | + const tx = await web3.eth.sendTransaction({ |
| 129 | + to, |
| 130 | + value: web3.utils.toWei(value, "ether"), |
| 131 | + gas: 53638, // Estimated gas limit for a simple transaction |
| 132 | + gasPrice: suggestedGasPrice, |
| 133 | + }) |
| 134 | + |
| 135 | + setHash(tx.transactionHash as Hex) |
| 136 | + } catch (err) { |
| 137 | + console.log(err) |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + const signTypedData = async () => { |
| 142 | + if (!web3) return |
| 143 | + |
| 144 | + const accounts = await web3.eth.getAccounts() |
| 145 | + const from = accounts[0] |
| 146 | + |
| 147 | + const domain = { |
| 148 | + name: "MyDApp", |
| 149 | + version: "1.0", |
| 150 | + chainId: 80002, |
| 151 | + verifyingContract: "0x1111111111111111111111111111111111111111", |
| 152 | + } |
| 153 | + const message = { |
| 154 | + content: "Hello from typed data!", |
| 155 | + sender: from, |
| 156 | + timestamp: Math.floor(Date.now() / 1000), |
| 157 | + } |
| 158 | + const dataToSign = { |
| 159 | + domain, |
| 160 | + message, |
| 161 | + primaryType: "Message", |
| 162 | + types: { |
| 163 | + EIP712Domain: [ |
| 164 | + { name: "name", type: "string" }, |
| 165 | + { name: "version", type: "string" }, |
| 166 | + { name: "chainId", type: "uint256" }, |
| 167 | + { name: "verifyingContract", type: "address" }, |
| 168 | + ], |
| 169 | + Message: [ |
| 170 | + { name: "content", type: "string" }, |
| 171 | + { name: "sender", type: "address" }, |
| 172 | + { name: "timestamp", type: "uint256" }, |
| 173 | + ], |
| 174 | + }, |
| 175 | + } |
| 176 | + |
| 177 | + const signature = await web3.eth.signTypedData(from, dataToSign) |
| 178 | + |
| 179 | + setTypedDataSignature(signature) |
| 180 | + } |
| 181 | + |
| 182 | + if (!credential) |
| 183 | + return ( |
| 184 | + <> |
| 185 | + <input id="username" name="username" placeholder="Username" /> |
| 186 | + <br /> |
| 187 | + <button onClick={register}>Register</button> |
| 188 | + <button onClick={login}>Login</button> |
| 189 | + </> |
| 190 | + ) |
| 191 | + if (!account) return <p>Loading...</p> |
| 192 | + |
| 193 | + return ( |
| 194 | + <> |
| 195 | + <h2>Account</h2> |
| 196 | + <p>Address: {account?.address}</p> |
| 197 | + |
| 198 | + <h2>Send Transaction</h2> |
| 199 | + <form onSubmit={sendTx}> |
| 200 | + <input name="to" placeholder="Address" /> |
| 201 | + <input name="value" placeholder="Amount (ETH)" /> |
| 202 | + <button type="submit">Send</button> |
| 203 | + </form> |
| 204 | + <button onClick={getProviderAddress}>Get address</button> |
| 205 | + <button onClick={signPersonalMessage}>SignPersonalMessage</button> |
| 206 | + <button onClick={signTypedData}>SignTypedData</button> |
| 207 | + {address && <p>Address: {address}</p>} |
| 208 | + {personalSignature && ( |
| 209 | + <p |
| 210 | + style={{ |
| 211 | + width: "100%", |
| 212 | + wordWrap: "break-word", |
| 213 | + }} |
| 214 | + > |
| 215 | + Personal Signature: {personalSignature} |
| 216 | + </p> |
| 217 | + )} |
| 218 | + {typedDataSignature && ( |
| 219 | + <p |
| 220 | + style={{ |
| 221 | + width: "100%", |
| 222 | + wordWrap: "break-word", |
| 223 | + }} |
| 224 | + > |
| 225 | + Typed Data Signature: {typedDataSignature} |
| 226 | + </p> |
| 227 | + )} |
| 228 | + {hash && <p>Transaction Hash: {hash}</p>} |
| 229 | + </> |
| 230 | + ) |
| 231 | +} |
| 232 | + |
| 233 | +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(<Example />) |
0 commit comments