From 0a829b3733b177c23bacc8eb102b8991dadde526 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Sun, 6 Oct 2024 23:34:48 -0400 Subject: [PATCH 1/6] wip --- examples/with-biconomy-aa/.env.local.example | 6 + examples/with-biconomy-aa/README.md | 151 ++++++++++ examples/with-biconomy-aa/package.json | 25 ++ .../with-biconomy-aa/src/createNewWallet.ts | 72 +++++ examples/with-biconomy-aa/src/index.ts | 258 ++++++++++++++++++ examples/with-biconomy-aa/src/util.ts | 24 ++ examples/with-biconomy-aa/tsconfig.json | 8 + pnpm-lock.yaml | 95 ++++++- 8 files changed, 625 insertions(+), 14 deletions(-) create mode 100644 examples/with-biconomy-aa/.env.local.example create mode 100644 examples/with-biconomy-aa/README.md create mode 100644 examples/with-biconomy-aa/package.json create mode 100644 examples/with-biconomy-aa/src/createNewWallet.ts create mode 100644 examples/with-biconomy-aa/src/index.ts create mode 100644 examples/with-biconomy-aa/src/util.ts create mode 100644 examples/with-biconomy-aa/tsconfig.json diff --git a/examples/with-biconomy-aa/.env.local.example b/examples/with-biconomy-aa/.env.local.example new file mode 100644 index 000000000..f995c81c3 --- /dev/null +++ b/examples/with-biconomy-aa/.env.local.example @@ -0,0 +1,6 @@ +API_PUBLIC_KEY="" +API_PRIVATE_KEY="" +BASE_URL="https://api.turnkey.com" +ORGANIZATION_ID="" +SIGN_WITH="" # if blank, we will create a wallet for you +INFURA_KEY="" diff --git a/examples/with-biconomy-aa/README.md b/examples/with-biconomy-aa/README.md new file mode 100644 index 000000000..6be62b0dd --- /dev/null +++ b/examples/with-biconomy-aa/README.md @@ -0,0 +1,151 @@ +# Example: `with-ethers` + +This example shows how to construct and broadcast a transaction using [`Ethers`](https://docs.ethers.org/v6/api/providers/#Signer) with Turnkey. + +If you want to see a demo with passkeys, head to the example [`with-ethers-and-passkeys`](../with-ethers-and-passkeys/) to see a NextJS app using passkeys. + +## Getting started + +### 1/ Cloning the example + +Make sure you have `Node.js` installed locally; we recommend using Node v18+. + +```bash +$ git clone https://github.com/tkhq/sdk +$ cd sdk/ +$ corepack enable # Install `pnpm` +$ pnpm install -r # Install dependencies +$ pnpm run build-all # Compile source code +$ cd examples/with-ethers/ +``` + +### 2/ Setting up Turnkey + +The first step is to set up your Turnkey organization and account. By following the [Quickstart](https://docs.turnkey.com/getting-started/quickstart) guide, you should have: + +- A public/private API key pair for Turnkey +- An organization ID +- A Turnkey wallet account (address), private key address, or a private key ID + +Once you've gathered these values, add them to a new `.env.local` file. Notice that your private key should be securely managed and **_never_** be committed to git. + +```bash +$ cp .env.local.example .env.local +``` + +Now open `.env.local` and add the missing environment variables: + +- `API_PUBLIC_KEY` +- `API_PRIVATE_KEY` +- `BASE_URL` +- `ORGANIZATION_ID` +- `SIGN_WITH` -- a Turnkey wallet account address, private key address, or private key ID. If you leave this blank, we'll create a wallet for you. +- `INFURA_KEY` -- if this is not set, it will default to using the Community Infura key + +### 3/ Running the scripts + +Note: there are multiple scripts included. See `package.json` for all of them. The following is the default: + +```bash +$ pnpm start +``` + +This script will do the following: + +1. sign a raw payload +2. send ETH (via type 2 EIP-1559 transaction) +3. deposit ETH into the WETH contract (aka wrapping) + +Note that these transactions will all be broadcasted sequentially. + +The script constructs a transaction via Turnkey and broadcasts via Infura. If the script exits because your account isn't funded, you can request funds on https://sepoliafaucet.com/ or https://faucet.paradigm.xyz/. + +Visit the Etherscan link to view your transaction; you have successfully sent your first transaction with Turnkey! + +See the following for a sample output: + +``` +Network: + sepolia (chain ID 11155111) + +Address: + 0x064c0CfDD7C485Eba21988Ded4dbCD9358556842 + +Balance: + 0.07750465249126655 Ether + +Transaction count: + 14 + +Turnkey-powered signature: + 0x97da598ac1ad566e77be7c7d9cc77339730e48c557c5d6f32f93d9fdeeed13472b1faf20f1e457a897a409f31b9e680ad6b02086ac4fb9aa693ce10374976b201c + +Recovered address: + 0x064c0CfDD7C485Eba21988Ded4dbCD9358556842 + +Turnkey-signed transaction: + 0x02f8668080808080942ad9ea1e677949a536a270cec812d6e868c881088609184e72a00080c001a09881f59e48500ef8960ae1cb94e0c862e7d613f961c250b6f07b546a1b058b1da06ba1871d7aed5eb8ea8cb211a0e3e22a1c6b54b34b4376d0ef5b1daef4100c8f + +Sent 0.00001 Ether to 0x2Ad9eA1E677949a536A270CEC812D6e868C88108: + https://sepolia.etherscan.io/tx/0xe034bdc597766719aef04b1d08998e606e85da1dd73e52fad8586a7d79d659e0 + +WETH Balance: + 0.00007 WETH + +Wrapped 0.00001 ETH: + https://sepolia.etherscan.io/tx/0x7f98c1b2c7ff7f8ab876b27fdcd794653d8b7f728dbeec3b1d403789c38bcb71 +``` + +Note: if you have a consensus-related policy resembling the following + +``` +{ + "effect": "EFFECT_ALLOW", + "consensus": "approvers.count() >= 2" +} +``` + +then the script will await consensus to be met. Specifically, the script will attempt to poll for activity completion per the `activityPoller` config passed to the `TurnkeyServerSDK`. If consensus still isn't met during this period, then the resulting `Consensus Needed` error will be caught, and the script will prompt the user to indicate when consensus has been met. At that point, the script will continue. + +```bash +$ pnpm start-legacy-sepolia +``` + +This script will do the following: + +1. send ETH (via type 0, EIP-155-compliant legacy transaction) +2. deposit ETH into the WETH contract (aka wrapping) + +Note that these transactions will all be broadcasted sequentially. + +The script constructs a transaction via Turnkey and broadcasts via Infura. If the script exits because your account isn't funded, you can request funds on https://sepoliafaucet.com/ or via Coinbase Wallet. + +Visit the Etherscan link to view your transaction; you have successfully sent your first transaction with Turnkey! + +See the following for a sample output: + +``` +Network: + sepolia (chain ID 11155111) + +Address: + 0xc4f1EF91ea582E3020E9ac155c3b5B27ce1185Dd + +Balance: + 0.049896964862611 Ether + +Transaction count: + 4 + +Turnkey-signed transaction: + 0xf86c048308b821825208942ad9ea1e677949a536a270cec812d6e868c881088609184e72a000808401546d72a0883137063bfa04e1c6be6f79789f53e4226455ae1cbc4d610d164334a6e12c83a06dae6bd75b6cb28a7ed2548f207f860dd56a49c4bd63a642d7728d592225e408 + +Sent 0.00001 Ether to 0x2Ad9eA1E677949a536A270CEC812D6e868C88108: + https://sepolia.etherscan.io/tx/0xf4c3e6bd5c6a635088dc7fc7c0d7a715beb340a7fbff67daf0adc666709e23f1 + +WETH Balance: + 0.0 WETH + +Wrapped 0.00001 ETH: + https://sepolia.etherscan.io/tx/0x428a6f3c24f6f0c2de34f41776566c875bd56bfe4d5d8db4a7ef57c2c4e69dec +``` diff --git a/examples/with-biconomy-aa/package.json b/examples/with-biconomy-aa/package.json new file mode 100644 index 000000000..1284e2bfd --- /dev/null +++ b/examples/with-biconomy-aa/package.json @@ -0,0 +1,25 @@ +{ + "name": "@turnkey/example-with-ethers", + "version": "0.1.3", + "private": true, + "scripts": { + "start": "tsx src/index.ts", + "start-advanced": "tsx src/advanced.ts", + "start-sepolia-legacy": "tsx src/sepoliaLegacyTx.ts", + "clean": "rimraf ./dist ./.cache", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@biconomy/account": "^4.5.6", + "@turnkey/api-key-stamper": "workspace:*", + "@turnkey/ethers": "workspace:*", + "@turnkey/http": "workspace:*", + "@turnkey/sdk-server": "workspace:*", + "dotenv": "^16.0.3", + "ethers": "^6.10.0", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/prompts": "^2.4.2" + } +} diff --git a/examples/with-biconomy-aa/src/createNewWallet.ts b/examples/with-biconomy-aa/src/createNewWallet.ts new file mode 100644 index 000000000..12ffd7327 --- /dev/null +++ b/examples/with-biconomy-aa/src/createNewWallet.ts @@ -0,0 +1,72 @@ +import { + TurnkeyClient, + createActivityPoller, + TurnkeyActivityError, +} from "@turnkey/http"; +import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import * as crypto from "crypto"; +import { refineNonNull } from "./util"; + +export async function createNewWallet() { + console.log("creating a new wallet on Turnkey...\n"); + + const walletName = `ETH Wallet ${crypto.randomBytes(2).toString("hex")}`; + + try { + const turnkeyClient = new TurnkeyClient( + { baseUrl: process.env.BASE_URL! }, + new ApiKeyStamper({ + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + }) + ); + + const activityPoller = createActivityPoller({ + client: turnkeyClient, + requestFn: turnkeyClient.createWallet, + }); + + const completedActivity = await activityPoller({ + type: "ACTIVITY_TYPE_CREATE_WALLET", + timestampMs: String(Date.now()), + organizationId: process.env.ORGANIZATION_ID!, + parameters: { + walletName, + accounts: [ + { + curve: "CURVE_SECP256K1", + pathFormat: "PATH_FORMAT_BIP32", + path: "m/44'/60'/0'/0/0", + addressFormat: "ADDRESS_FORMAT_ETHEREUM", + }, + ], + }, + }); + + const wallet = refineNonNull(completedActivity.result.createWalletResult); + const walletId = refineNonNull(wallet.walletId); + const address = refineNonNull(wallet.addresses[0]); + + // Success! + console.log( + [ + `New Ethereum wallet created!`, + `- Name: ${walletName}`, + `- Wallet ID: ${walletId}`, + `- Address: ${address}`, + ``, + "Now you can take the address, put it in `.env.local` (`SIGN_WITH=
`), then re-run the script.", + ].join("\n") + ); + } catch (error) { + // If needed, you can read from `TurnkeyActivityError` to find out why the activity didn't succeed + if (error instanceof TurnkeyActivityError) { + throw error; + } + + throw new TurnkeyActivityError({ + message: "Failed to create a new Ethereum wallet", + cause: error as Error, + }); + } +} diff --git a/examples/with-biconomy-aa/src/index.ts b/examples/with-biconomy-aa/src/index.ts new file mode 100644 index 000000000..86802c646 --- /dev/null +++ b/examples/with-biconomy-aa/src/index.ts @@ -0,0 +1,258 @@ +import * as path from "path"; +import * as dotenv from "dotenv"; +import prompts, { PromptType } from "prompts"; +import { ethers } from "ethers"; +import { + createSmartAccountClient, + LightSigner, + BiconomySmartAccountV2, +} from "@biconomy/account"; + +// Load environment variables from `.env.local` +dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); + +import { + getSignatureFromActivity, + getSignedTransactionFromActivity, + TurnkeyActivityConsensusNeededError, + TERMINAL_ACTIVITY_STATUSES, + TActivity, +} from "@turnkey/http"; +import { TurnkeySigner, serializeSignature } from "@turnkey/ethers"; +import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; +import { createNewWallet } from "./createNewWallet"; +import { print, assertEqual } from "./util"; + +async function main() { + if (!process.env.SIGN_WITH) { + // If you don't specify a `SIGN_WITH`, we'll create a new wallet for you via calling the Turnkey API. + await createNewWallet(); + return; + } + + const turnkeyClient = new TurnkeyServerSDK({ + apiBaseUrl: process.env.BASE_URL!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + apiPublicKey: process.env.API_PUBLIC_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, + // The following config is useful in contexts where an activity requires consensus. + // By default, if the activity is not initially successful, it will poll a maximum + // of 3 times with an interval of 1000 milliseconds. Otherwise, use the values below. + // + // ----- + // + // activityPoller: { + // intervalMs: 5_000, + // numRetries: 10, + // }, + }); + + // Initialize a Turnkey Signer + const turnkeySigner = new TurnkeySigner({ + client: turnkeyClient.apiClient(), + organizationId: process.env.ORGANIZATION_ID!, + signWith: process.env.SIGN_WITH!, + }); + + const smartAccount = await connect(turnkeySigner); + const smartAccountAddress = await smartAccount.getAccountAddress(); + + print("Smart account address:", smartAccountAddress); + + // Bring your own provider (such as Alchemy or Infura: https://docs.ethers.org/v6/api/providers/) + const network = "sepolia"; + const provider = new ethers.JsonRpcProvider( + `https://${network}.infura.io/v3/${process.env.INFURA_KEY}` + ); + const connectedSigner = turnkeySigner.connect(provider); + + const chainId = (await connectedSigner.provider?.getNetwork())?.chainId ?? 0; + const address = await connectedSigner.getAddress(); + const transactionCount = await connectedSigner.provider?.getTransactionCount( + address + ); + let balance = (await connectedSigner.provider?.getBalance(address)) ?? 0; + + print("Network:", `${network} (chain ID ${chainId})`); + print("Address:", address); + print("Balance:", `${ethers.formatEther(balance)} Ether`); + print("Transaction count:", `${transactionCount}`); + + // 1. Sign a raw payload (`eth_sign` style) + const { message } = await prompts([ + { + type: "text" as PromptType, + name: "message", + message: "Message to sign", + initial: "Hello Turnkey", + }, + ]); + + let signature; + try { + signature = await connectedSigner.signMessage(message); + } catch (error: any) { + signature = await handleActivityError(error).then( + async (activity?: TActivity) => { + if (!activity) { + throw error; + } + + return serializeSignature(getSignatureFromActivity(activity)); + } + ); + } + + const recoveredAddress = ethers.verifyMessage(message, signature); + + print("Turnkey-powered signature:", `${signature}`); + print("Recovered address:", `${recoveredAddress}`); + assertEqual(recoveredAddress, address); + + // 2. Create a simple send transaction + const { amount, destination } = await prompts([ + { + type: "number" as PromptType, + name: "amount", + message: "Amount to send (wei). Default to 0.0000001 ETH", + initial: 100000000000, + }, + { + type: "text" as PromptType, + name: "destination", + message: "Destination address (default to TKHQ warchest)", + initial: "0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7", + }, + ]); + const transactionRequest = { + to: destination, + value: amount, + type: 2, + }; + + let signedTx; + try { + signedTx = await connectedSigner.signTransaction(transactionRequest); + } catch (error: any) { + signedTx = await handleActivityError(error).then( + async (activity?: TActivity) => { + if (!activity) { + throw error; + } + + return getSignedTransactionFromActivity(activity); + } + ); + } + + print("Turnkey-signed transaction:", `${signedTx}`); + + while (balance === 0) { + console.log( + [ + `\nšŸ’ø Your onchain balance is at 0! To continue this demo you'll need testnet funds! You can use:`, + `- Any online faucet (e.g. https://www.alchemy.com/faucets/)`, + `\nTo check your balance: https://${network}.etherscan.io/address/${address}`, + `\n--------`, + ].join("\n") + ); + + const { continue: _ } = await prompts([ + { + type: "text" as PromptType, + name: "continue", + message: "Ready to continue? y/n", + initial: "y", + }, + ]); + + balance = (await connectedSigner.provider?.getBalance(address))!; + } + + // 3. Make a simple send tx (which calls `signTransaction` under the hood) + let sentTx; + try { + sentTx = await connectedSigner.sendTransaction(transactionRequest); + } catch (error: any) { + sentTx = await handleActivityError(error).then( + async (activity?: TActivity) => { + if (!activity) { + throw error; + } + + return await connectedSigner.provider?.broadcastTransaction( + getSignedTransactionFromActivity(activity) + ); + } + ); + } + + print( + `Sent ${ethers.formatEther(sentTx!.value)} Ether to ${sentTx!.to}:`, + `https://${network}.etherscan.io/tx/${sentTx!.hash}` + ); + + async function handleActivityError(error: any) { + if (error instanceof TurnkeyActivityConsensusNeededError) { + const activityId = error["activityId"]!; + let activityStatus = error["activityStatus"]!; + let activity: TActivity | undefined; + + while (!TERMINAL_ACTIVITY_STATUSES.includes(activityStatus)) { + console.log("\nWaiting for consensus...\n"); + + const { retry } = await prompts([ + { + type: "text" as PromptType, + name: "retry", + message: "Consensus reached? y/n", + initial: "y", + }, + ]); + + if (retry === "n") { + continue; + } + + // Refresh activity status + activity = ( + await turnkeyClient.apiClient().getActivity({ + activityId, + organizationId: process.env.ORGANIZATION_ID!, + }) + ).activity; + activityStatus = activity.status; + } + + console.log("\nConsensus reached! Moving on...\n"); + + return activity; + } + + // Rethrow error + throw error; + } +} + +// Biconomy +const connect = async ( + turnkeySigner: TurnkeySigner +): Promise => { + try { + const smartAccount = await createSmartAccountClient({ + signer: turnkeySigner as LightSigner, + bundlerUrl: "", // <-- Read about this at https://docs.biconomy.io/dashboard#bundler-url + biconomyPaymasterApiKey: "", // <-- Read about at https://docs.biconomy.io/dashboard/paymaster + rpcUrl: "", // <-- read about this at https://docs.biconomy.io/account/methods#createsmartaccountclient + }); + + return smartAccount; + } catch (error: any) { + throw new Error(error); + } +}; + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/examples/with-biconomy-aa/src/util.ts b/examples/with-biconomy-aa/src/util.ts new file mode 100644 index 000000000..1ad4478e1 --- /dev/null +++ b/examples/with-biconomy-aa/src/util.ts @@ -0,0 +1,24 @@ +export function print(header: string, body: string): void { + console.log(`${header}\n\t${body}\n`); +} + +export function assertEqual(left: T, right: T) { + if (left !== right) { + throw new Error(`${JSON.stringify(left)} !== ${JSON.stringify(right)}`); + } +} + +export function refineNonNull( + input: T | null | undefined, + errorMessage?: string +): T { + if (input == null) { + throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`); + } + + return input; +} + +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/examples/with-biconomy-aa/tsconfig.json b/examples/with-biconomy-aa/tsconfig.json new file mode 100644 index 000000000..6d4b83714 --- /dev/null +++ b/examples/with-biconomy-aa/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./.cache/.tsbuildinfo" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.json"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f5346944..d5d83f3d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -570,6 +570,37 @@ importers: specifier: ^5.0.4 version: 5.1.5 + examples/with-biconomy-aa: + dependencies: + '@biconomy/account': + specifier: ^4.5.6 + version: 4.5.6(typescript@5.1.5)(viem@2.18.8) + '@turnkey/api-key-stamper': + specifier: workspace:* + version: link:../../packages/api-key-stamper + '@turnkey/ethers': + specifier: workspace:* + version: link:../../packages/ethers + '@turnkey/http': + specifier: workspace:* + version: link:../../packages/http + '@turnkey/sdk-server': + specifier: workspace:* + version: link:../../packages/sdk-server + dotenv: + specifier: ^16.0.3 + version: 16.0.3 + ethers: + specifier: ^6.10.0 + version: 6.10.0 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + devDependencies: + '@types/prompts': + specifier: ^2.4.2 + version: 2.4.2 + examples/with-bitcoin: dependencies: '@turnkey/http': @@ -4638,6 +4669,22 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@biconomy/account@4.5.6(typescript@5.1.5)(viem@2.18.8): + resolution: {integrity: sha512-Mq0X9HF4fsPTkf87eXklJJE/Hl3GQwQHN/2/D1fCA4Q4o4AM9HV7Tzpb+hBsh8cUJ+s2j0Q9wORXA1c0onAImQ==} + peerDependencies: + typescript: ^5 + viem: ^2 + dependencies: + '@silencelaboratories/walletprovider-sdk': 0.1.0(typescript@5.1.5) + merkletreejs: 0.4.0 + typescript: 5.1.5 + viem: 2.18.8(typescript@5.1.5) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false + /@chainsafe/as-sha256@0.3.1: resolution: {integrity: sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==} dev: true @@ -9951,6 +9998,18 @@ packages: resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} dev: false + /@silencelaboratories/walletprovider-sdk@0.1.0(typescript@5.1.5): + resolution: {integrity: sha512-53fV1noQJDUN9JNydDohyzsFl4+QYoWNkkkAfRzmIgtv+6DR+Dksb0fKmme2WdtA8MPEw/HsRwN3Lr6YC3iF7A==} + dependencies: + '@noble/curves': 1.4.2 + viem: 2.18.8(typescript@5.1.5) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + dev: false + /@sinclair/typebox@0.25.22: resolution: {integrity: sha512-6U6r2L7rnM7EG8G1tWzIjdB3QlsHF4slgcqXNN/SF0xJOAr0nDmT2GedlkyO3mrv8mDTJ24UuOMWR3diBrCvQQ==} dev: true @@ -13869,6 +13928,10 @@ packages: engines: {node: '>=4.5'} dev: false + /buffer-reverse@1.0.1: + resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==} + dev: false + /buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} @@ -18435,6 +18498,17 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + /merkletreejs@0.4.0: + resolution: {integrity: sha512-a48Ta5kWiVNBgeEbZVMm6FB1hBlp6vEuou/XnZdlkmd2zq6NZR6Sh2j+kR1B0iOZIXrTMcigBYzZ39MLdYhm1g==} + engines: {node: '>= 7.6.0'} + dependencies: + bignumber.js: 9.1.2 + buffer-reverse: 1.0.1 + crypto-js: 4.2.0 + treeify: 1.1.0 + web3-utils: 4.2.1 + dev: false + /metro-babel-transformer@0.80.5: resolution: {integrity: sha512-sxH6hcWCorhTbk4kaShCWsadzu99WBL4Nvq4m/sDTbp32//iGuxtAnUK+ZV+6IEygr2u9Z0/4XoZ8Sbcl71MpA==} engines: {node: '>=18'} @@ -21825,6 +21899,11 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + /treeify@1.1.0: + resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} + engines: {node: '>=0.6'} + dev: false + /trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -22503,7 +22582,6 @@ packages: - bufferutil - utf-8-validate - zod - dev: true /viem@2.7.19(typescript@5.1.5): resolution: {integrity: sha512-UOMeqy+8p2709ra2j9HEOL1NfjsXZzlJ8gwR6YO/zXH8KIZvyzW07t4iQARF5+ShVZ/7+/1ec8oPjVi1M//33A==} @@ -22582,7 +22660,7 @@ packages: web3-providers-ws: 4.0.7 web3-types: 1.5.0 web3-utils: 4.2.1 - web3-validator: 2.0.4 + web3-validator: 2.0.5 optionalDependencies: web3-providers-ipc: 4.0.7 transitivePeerDependencies: @@ -22794,18 +22872,7 @@ packages: eventemitter3: 5.0.1 web3-errors: 1.1.4 web3-types: 1.5.0 - web3-validator: 2.0.4 - dev: false - - /web3-validator@2.0.4: - resolution: {integrity: sha512-qRxVePwdW+SByOmTpDZFWHIUAa7PswvxNszrOua6BoGqAhERo5oJZBN+EbWtK/+O+ApNxt5FR3nCPmiZldiOQA==} - engines: {node: '>=14', npm: '>=6.12.0'} - dependencies: - ethereum-cryptography: 2.1.3 - util: 0.12.5 - web3-errors: 1.1.4 - web3-types: 1.5.0 - zod: 3.23.8 + web3-validator: 2.0.5 dev: false /web3-validator@2.0.5: From e0e84f4dc7ac5a369864f18e55773a0b2e3605b6 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Fri, 18 Oct 2024 00:56:38 -0400 Subject: [PATCH 2/6] add ethers support --- examples/with-biconomy-aa/.env.local.example | 2 + examples/with-biconomy-aa/package.json | 14 +- .../with-biconomy-aa/src/createNewWallet.ts | 52 +--- examples/with-biconomy-aa/src/ethers.ts | 161 +++++++++++ examples/with-biconomy-aa/src/index.ts | 258 ------------------ pnpm-lock.yaml | 55 +++- 6 files changed, 230 insertions(+), 312 deletions(-) create mode 100644 examples/with-biconomy-aa/src/ethers.ts delete mode 100644 examples/with-biconomy-aa/src/index.ts diff --git a/examples/with-biconomy-aa/.env.local.example b/examples/with-biconomy-aa/.env.local.example index f995c81c3..b09888df9 100644 --- a/examples/with-biconomy-aa/.env.local.example +++ b/examples/with-biconomy-aa/.env.local.example @@ -4,3 +4,5 @@ BASE_URL="https://api.turnkey.com" ORGANIZATION_ID="" SIGN_WITH="" # if blank, we will create a wallet for you INFURA_KEY="" +BICONOMY_BUNDLER_URL="" # see https://docs.biconomy.io/dashboard#bundler-url +BICONOMY_PAYMASTER_API_KEY="" # see https://docs.biconomy.io/dashboard/paymaster diff --git a/examples/with-biconomy-aa/package.json b/examples/with-biconomy-aa/package.json index 1284e2bfd..b61295e8a 100644 --- a/examples/with-biconomy-aa/package.json +++ b/examples/with-biconomy-aa/package.json @@ -1,23 +1,21 @@ { - "name": "@turnkey/example-with-ethers", - "version": "0.1.3", + "name": "@turnkey/example-with-biconomy-aa", + "version": "0.1.0", "private": true, "scripts": { - "start": "tsx src/index.ts", - "start-advanced": "tsx src/advanced.ts", - "start-sepolia-legacy": "tsx src/sepoliaLegacyTx.ts", + "start-ethers": "tsx src/ethers.ts", "clean": "rimraf ./dist ./.cache", "typecheck": "tsc --noEmit" }, "dependencies": { "@biconomy/account": "^4.5.6", - "@turnkey/api-key-stamper": "workspace:*", "@turnkey/ethers": "workspace:*", - "@turnkey/http": "workspace:*", + "@turnkey/viem": "workspace:*", "@turnkey/sdk-server": "workspace:*", "dotenv": "^16.0.3", "ethers": "^6.10.0", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "viem": "^1.16.6" }, "devDependencies": { "@types/prompts": "^2.4.2" diff --git a/examples/with-biconomy-aa/src/createNewWallet.ts b/examples/with-biconomy-aa/src/createNewWallet.ts index 12ffd7327..a2c3b4c5b 100644 --- a/examples/with-biconomy-aa/src/createNewWallet.ts +++ b/examples/with-biconomy-aa/src/createNewWallet.ts @@ -1,9 +1,5 @@ -import { - TurnkeyClient, - createActivityPoller, - TurnkeyActivityError, -} from "@turnkey/http"; -import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { Turnkey as TurnkeySDKServer } from "@turnkey/sdk-server"; + import * as crypto from "crypto"; import { refineNonNull } from "./util"; @@ -13,24 +9,16 @@ export async function createNewWallet() { const walletName = `ETH Wallet ${crypto.randomBytes(2).toString("hex")}`; try { - const turnkeyClient = new TurnkeyClient( - { baseUrl: process.env.BASE_URL! }, - new ApiKeyStamper({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - }) - ); - - const activityPoller = createActivityPoller({ - client: turnkeyClient, - requestFn: turnkeyClient.createWallet, + const turnkeyClient = new TurnkeySDKServer({ + apiBaseUrl: "https://api.turnkey.com", + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, }); - const completedActivity = await activityPoller({ - type: "ACTIVITY_TYPE_CREATE_WALLET", - timestampMs: String(Date.now()), - organizationId: process.env.ORGANIZATION_ID!, - parameters: { + const { walletId, addresses } = await turnkeyClient + .apiClient() + .createWallet({ walletName, accounts: [ { @@ -40,33 +28,23 @@ export async function createNewWallet() { addressFormat: "ADDRESS_FORMAT_ETHEREUM", }, ], - }, - }); + }); - const wallet = refineNonNull(completedActivity.result.createWalletResult); - const walletId = refineNonNull(wallet.walletId); - const address = refineNonNull(wallet.addresses[0]); + const newWalletId = refineNonNull(walletId); + const address = refineNonNull(addresses[0]); // Success! console.log( [ `New Ethereum wallet created!`, `- Name: ${walletName}`, - `- Wallet ID: ${walletId}`, + `- Wallet ID: ${newWalletId}`, `- Address: ${address}`, ``, "Now you can take the address, put it in `.env.local` (`SIGN_WITH=
`), then re-run the script.", ].join("\n") ); } catch (error) { - // If needed, you can read from `TurnkeyActivityError` to find out why the activity didn't succeed - if (error instanceof TurnkeyActivityError) { - throw error; - } - - throw new TurnkeyActivityError({ - message: "Failed to create a new Ethereum wallet", - cause: error as Error, - }); + throw new Error("Failed to create a new Ethereum wallet: " + error); } } diff --git a/examples/with-biconomy-aa/src/ethers.ts b/examples/with-biconomy-aa/src/ethers.ts new file mode 100644 index 000000000..040ef3e91 --- /dev/null +++ b/examples/with-biconomy-aa/src/ethers.ts @@ -0,0 +1,161 @@ +import * as path from "path"; +import * as dotenv from "dotenv"; +import prompts, { PromptType } from "prompts"; +import { ethers } from "ethers"; +import { + createSmartAccountClient, + LightSigner, + BiconomySmartAccountV2, + PaymasterMode, +} from "@biconomy/account"; + +// Load environment variables from `.env.local` +dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); + +import { TurnkeySigner } from "@turnkey/ethers"; +import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; +import { createNewWallet } from "./createNewWallet"; +import { print } from "./util"; + +async function main() { + if (!process.env.SIGN_WITH) { + // If you don't specify a `SIGN_WITH`, we'll create a new wallet for you via calling the Turnkey API. + await createNewWallet(); + return; + } + + const turnkeyClient = new TurnkeyServerSDK({ + apiBaseUrl: process.env.BASE_URL!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + apiPublicKey: process.env.API_PUBLIC_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, + }); + + // Initialize a Turnkey Signer via Ethers v6 + const turnkeySigner = new TurnkeySigner({ + client: turnkeyClient.apiClient(), + organizationId: process.env.ORGANIZATION_ID!, + signWith: process.env.SIGN_WITH!, + }); + + // Bring your own provider (such as Alchemy or Infura: https://docs.ethers.org/v6/api/providers/) + const network = "sepolia"; + const provider = new ethers.JsonRpcProvider( + `https://${network}.infura.io/v3/${process.env.INFURA_KEY}` + ); + const connectedSigner = turnkeySigner.connect(provider); + + // Connect a TurnkeySigner to a Biconomy Smart Account Client, defaulting to Sepolia + // Ensure this method is hoisted + const connect = async ( + turnkeySigner: TurnkeySigner + ): Promise => { + try { + const smartAccount = await createSmartAccountClient({ + signer: turnkeySigner as LightSigner, + bundlerUrl: process.env.BICONOMY_BUNDLER_URL!, // <-- Read about this at https://docs.biconomy.io/dashboard#bundler-url + biconomyPaymasterApiKey: process.env.BICONOMY_PAYMASTER_API_KEY!, // <-- Read about at https://docs.biconomy.io/dashboard/paymaster + rpcUrl: `https://${network}.infura.io/v3/${process.env.INFURA_KEY!}`, // <-- read about this at https://docs.biconomy.io/account/methods#createsmartaccountclient + chainId: Number(chainId), + }); + + return smartAccount; + } catch (error: any) { + throw new Error(error); + } + }; + + const chainId = (await connectedSigner.provider?.getNetwork())?.chainId ?? 0; + const signerAddress = await connectedSigner.getAddress(); // signer + + const smartAccount = await connect(turnkeySigner); + const smartAccountAddress = await smartAccount.getAccountAddress(); + + const transactionCount = await connectedSigner.provider?.getTransactionCount( + smartAccountAddress + ); + const nonce = await smartAccount.getNonce(); + let balance = + (await connectedSigner.provider?.getBalance(smartAccountAddress)) ?? 0; + + print("Network:", `${network} (chain ID ${chainId})`); + print("Signer address:", signerAddress); + print("Smart wallet address:", smartAccountAddress); + print("Balance:", `${ethers.formatEther(balance)} Ether`); + print("Transaction count:", `${transactionCount}`); + print("Nonce:", `${nonce}`); + + while (balance === 0n) { + console.log( + [ + `\nšŸ’ø Your onchain balance is at 0! To continue this demo you'll need testnet funds! You can use:`, + `- Any online faucet (e.g. https://www.alchemy.com/faucets/)`, + `\nTo check your balance: https://${network}.etherscan.io/address/${smartAccountAddress}`, + `\n--------`, + ].join("\n") + ); + + const { continue: _ } = await prompts([ + { + type: "text" as PromptType, + name: "continue", + message: "Ready to continue? y/n", + initial: "y", + }, + ]); + + balance = (await connectedSigner.provider?.getBalance( + smartAccountAddress + ))!; + } + + const { amount, destination } = await prompts([ + { + type: "number" as PromptType, + name: "amount", + message: "Amount to send (wei). Default to 0.0000001 ETH", + initial: 100000000000, + }, + { + type: "text" as PromptType, + name: "destination", + message: "Destination address (default to TKHQ warchest)", + initial: "0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7", + }, + ]); + const transactionRequest = { + to: destination, + value: amount, + // nonce, + // nonce: transactionCount, + type: 2, + }; + + // Make a simple send tx (which calls `signTransaction` under the hood) + const userOpResponse = await smartAccount?.sendTransaction( + transactionRequest, + { + nonceOptions: { nonceKey: Number(0) }, + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + } + ); + + const { transactionHash } = await userOpResponse.waitForTxHash(); + + print( + `Sent ${ethers.formatEther(transactionRequest.value)} Ether to ${ + transactionRequest.to + }:`, + `https://${network}.etherscan.io/tx/${transactionHash}` + ); + + print( + `User Ops can be found here:`, + `https://jiffyscan.xyz/bundle/${transactionHash}?network=${network}&pageNo=0&pageSize=10` + ); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/examples/with-biconomy-aa/src/index.ts b/examples/with-biconomy-aa/src/index.ts deleted file mode 100644 index 86802c646..000000000 --- a/examples/with-biconomy-aa/src/index.ts +++ /dev/null @@ -1,258 +0,0 @@ -import * as path from "path"; -import * as dotenv from "dotenv"; -import prompts, { PromptType } from "prompts"; -import { ethers } from "ethers"; -import { - createSmartAccountClient, - LightSigner, - BiconomySmartAccountV2, -} from "@biconomy/account"; - -// Load environment variables from `.env.local` -dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); - -import { - getSignatureFromActivity, - getSignedTransactionFromActivity, - TurnkeyActivityConsensusNeededError, - TERMINAL_ACTIVITY_STATUSES, - TActivity, -} from "@turnkey/http"; -import { TurnkeySigner, serializeSignature } from "@turnkey/ethers"; -import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; -import { createNewWallet } from "./createNewWallet"; -import { print, assertEqual } from "./util"; - -async function main() { - if (!process.env.SIGN_WITH) { - // If you don't specify a `SIGN_WITH`, we'll create a new wallet for you via calling the Turnkey API. - await createNewWallet(); - return; - } - - const turnkeyClient = new TurnkeyServerSDK({ - apiBaseUrl: process.env.BASE_URL!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - apiPublicKey: process.env.API_PUBLIC_KEY!, - defaultOrganizationId: process.env.ORGANIZATION_ID!, - // The following config is useful in contexts where an activity requires consensus. - // By default, if the activity is not initially successful, it will poll a maximum - // of 3 times with an interval of 1000 milliseconds. Otherwise, use the values below. - // - // ----- - // - // activityPoller: { - // intervalMs: 5_000, - // numRetries: 10, - // }, - }); - - // Initialize a Turnkey Signer - const turnkeySigner = new TurnkeySigner({ - client: turnkeyClient.apiClient(), - organizationId: process.env.ORGANIZATION_ID!, - signWith: process.env.SIGN_WITH!, - }); - - const smartAccount = await connect(turnkeySigner); - const smartAccountAddress = await smartAccount.getAccountAddress(); - - print("Smart account address:", smartAccountAddress); - - // Bring your own provider (such as Alchemy or Infura: https://docs.ethers.org/v6/api/providers/) - const network = "sepolia"; - const provider = new ethers.JsonRpcProvider( - `https://${network}.infura.io/v3/${process.env.INFURA_KEY}` - ); - const connectedSigner = turnkeySigner.connect(provider); - - const chainId = (await connectedSigner.provider?.getNetwork())?.chainId ?? 0; - const address = await connectedSigner.getAddress(); - const transactionCount = await connectedSigner.provider?.getTransactionCount( - address - ); - let balance = (await connectedSigner.provider?.getBalance(address)) ?? 0; - - print("Network:", `${network} (chain ID ${chainId})`); - print("Address:", address); - print("Balance:", `${ethers.formatEther(balance)} Ether`); - print("Transaction count:", `${transactionCount}`); - - // 1. Sign a raw payload (`eth_sign` style) - const { message } = await prompts([ - { - type: "text" as PromptType, - name: "message", - message: "Message to sign", - initial: "Hello Turnkey", - }, - ]); - - let signature; - try { - signature = await connectedSigner.signMessage(message); - } catch (error: any) { - signature = await handleActivityError(error).then( - async (activity?: TActivity) => { - if (!activity) { - throw error; - } - - return serializeSignature(getSignatureFromActivity(activity)); - } - ); - } - - const recoveredAddress = ethers.verifyMessage(message, signature); - - print("Turnkey-powered signature:", `${signature}`); - print("Recovered address:", `${recoveredAddress}`); - assertEqual(recoveredAddress, address); - - // 2. Create a simple send transaction - const { amount, destination } = await prompts([ - { - type: "number" as PromptType, - name: "amount", - message: "Amount to send (wei). Default to 0.0000001 ETH", - initial: 100000000000, - }, - { - type: "text" as PromptType, - name: "destination", - message: "Destination address (default to TKHQ warchest)", - initial: "0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7", - }, - ]); - const transactionRequest = { - to: destination, - value: amount, - type: 2, - }; - - let signedTx; - try { - signedTx = await connectedSigner.signTransaction(transactionRequest); - } catch (error: any) { - signedTx = await handleActivityError(error).then( - async (activity?: TActivity) => { - if (!activity) { - throw error; - } - - return getSignedTransactionFromActivity(activity); - } - ); - } - - print("Turnkey-signed transaction:", `${signedTx}`); - - while (balance === 0) { - console.log( - [ - `\nšŸ’ø Your onchain balance is at 0! To continue this demo you'll need testnet funds! You can use:`, - `- Any online faucet (e.g. https://www.alchemy.com/faucets/)`, - `\nTo check your balance: https://${network}.etherscan.io/address/${address}`, - `\n--------`, - ].join("\n") - ); - - const { continue: _ } = await prompts([ - { - type: "text" as PromptType, - name: "continue", - message: "Ready to continue? y/n", - initial: "y", - }, - ]); - - balance = (await connectedSigner.provider?.getBalance(address))!; - } - - // 3. Make a simple send tx (which calls `signTransaction` under the hood) - let sentTx; - try { - sentTx = await connectedSigner.sendTransaction(transactionRequest); - } catch (error: any) { - sentTx = await handleActivityError(error).then( - async (activity?: TActivity) => { - if (!activity) { - throw error; - } - - return await connectedSigner.provider?.broadcastTransaction( - getSignedTransactionFromActivity(activity) - ); - } - ); - } - - print( - `Sent ${ethers.formatEther(sentTx!.value)} Ether to ${sentTx!.to}:`, - `https://${network}.etherscan.io/tx/${sentTx!.hash}` - ); - - async function handleActivityError(error: any) { - if (error instanceof TurnkeyActivityConsensusNeededError) { - const activityId = error["activityId"]!; - let activityStatus = error["activityStatus"]!; - let activity: TActivity | undefined; - - while (!TERMINAL_ACTIVITY_STATUSES.includes(activityStatus)) { - console.log("\nWaiting for consensus...\n"); - - const { retry } = await prompts([ - { - type: "text" as PromptType, - name: "retry", - message: "Consensus reached? y/n", - initial: "y", - }, - ]); - - if (retry === "n") { - continue; - } - - // Refresh activity status - activity = ( - await turnkeyClient.apiClient().getActivity({ - activityId, - organizationId: process.env.ORGANIZATION_ID!, - }) - ).activity; - activityStatus = activity.status; - } - - console.log("\nConsensus reached! Moving on...\n"); - - return activity; - } - - // Rethrow error - throw error; - } -} - -// Biconomy -const connect = async ( - turnkeySigner: TurnkeySigner -): Promise => { - try { - const smartAccount = await createSmartAccountClient({ - signer: turnkeySigner as LightSigner, - bundlerUrl: "", // <-- Read about this at https://docs.biconomy.io/dashboard#bundler-url - biconomyPaymasterApiKey: "", // <-- Read about at https://docs.biconomy.io/dashboard/paymaster - rpcUrl: "", // <-- read about this at https://docs.biconomy.io/account/methods#createsmartaccountclient - }); - - return smartAccount; - } catch (error: any) { - throw new Error(error); - } -}; - -main().catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5d83f3d4..b9cb743a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,19 +574,16 @@ importers: dependencies: '@biconomy/account': specifier: ^4.5.6 - version: 4.5.6(typescript@5.1.5)(viem@2.18.8) - '@turnkey/api-key-stamper': - specifier: workspace:* - version: link:../../packages/api-key-stamper + version: 4.5.6(typescript@5.1.5)(viem@1.16.6) '@turnkey/ethers': specifier: workspace:* version: link:../../packages/ethers - '@turnkey/http': - specifier: workspace:* - version: link:../../packages/http '@turnkey/sdk-server': specifier: workspace:* version: link:../../packages/sdk-server + '@turnkey/viem': + specifier: workspace:* + version: link:../../packages/viem dotenv: specifier: ^16.0.3 version: 16.0.3 @@ -596,6 +593,9 @@ importers: prompts: specifier: ^2.4.2 version: 2.4.2 + viem: + specifier: ^1.16.6 + version: 1.16.6(typescript@5.1.5) devDependencies: '@types/prompts': specifier: ^2.4.2 @@ -4669,7 +4669,7 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@biconomy/account@4.5.6(typescript@5.1.5)(viem@2.18.8): + /@biconomy/account@4.5.6(typescript@5.1.5)(viem@1.16.6): resolution: {integrity: sha512-Mq0X9HF4fsPTkf87eXklJJE/Hl3GQwQHN/2/D1fCA4Q4o4AM9HV7Tzpb+hBsh8cUJ+s2j0Q9wORXA1c0onAImQ==} peerDependencies: typescript: ^5 @@ -4678,7 +4678,7 @@ packages: '@silencelaboratories/walletprovider-sdk': 0.1.0(typescript@5.1.5) merkletreejs: 0.4.0 typescript: 5.1.5 - viem: 2.18.8(typescript@5.1.5) + viem: 1.16.6(typescript@5.1.5) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -12852,6 +12852,20 @@ packages: typescript: 5.1.3 dev: false + /abitype@0.9.8(typescript@5.1.5): + resolution: {integrity: sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.19.1 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + dependencies: + typescript: 5.1.5 + dev: false + /abitype@1.0.0(typescript@5.1.5): resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} peerDependencies: @@ -22513,6 +22527,29 @@ packages: - zod dev: false + /viem@1.16.6(typescript@5.1.5): + resolution: {integrity: sha512-jcWcFQ+xzIfDwexwPJRvCuCRJKEkK9iHTStG7mpU5MmuSBpACs4nATBDyXNFtUiyYTFzLlVEwWkt68K0nCSImg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.9.4 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 0.9.8(typescript@5.1.5) + isows: 1.0.3(ws@8.17.1) + typescript: 5.1.5 + ws: 8.17.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false + /viem@2.1.1(typescript@5.1.5): resolution: {integrity: sha512-gJiwYceD7Dsjioglr+85GQS3u5Gp9XGG8oJqGsauBaEPFlkmbRx7cxD2Q5RZXFToVvEbarOWtITZtGHBsGv4MQ==} peerDependencies: From ebdc599c9cf41bf9a8217adf1c79296f438e9e75 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Fri, 18 Oct 2024 12:46:33 -0400 Subject: [PATCH 3/6] add viem support --- examples/with-biconomy-aa/package.json | 5 +- examples/with-biconomy-aa/src/viem.ts | 176 +++++++++++++++++++++++++ pnpm-lock.yaml | 146 +++++++++++++------- 3 files changed, 278 insertions(+), 49 deletions(-) create mode 100644 examples/with-biconomy-aa/src/viem.ts diff --git a/examples/with-biconomy-aa/package.json b/examples/with-biconomy-aa/package.json index b61295e8a..9d7b476bd 100644 --- a/examples/with-biconomy-aa/package.json +++ b/examples/with-biconomy-aa/package.json @@ -4,18 +4,19 @@ "private": true, "scripts": { "start-ethers": "tsx src/ethers.ts", + "start-viem": "tsx src/viem.ts", "clean": "rimraf ./dist ./.cache", "typecheck": "tsc --noEmit" }, "dependencies": { "@biconomy/account": "^4.5.6", "@turnkey/ethers": "workspace:*", - "@turnkey/viem": "workspace:*", "@turnkey/sdk-server": "workspace:*", + "@turnkey/viem": "workspace:*", "dotenv": "^16.0.3", "ethers": "^6.10.0", "prompts": "^2.4.2", - "viem": "^1.16.6" + "viem": "^2.21.29" }, "devDependencies": { "@types/prompts": "^2.4.2" diff --git a/examples/with-biconomy-aa/src/viem.ts b/examples/with-biconomy-aa/src/viem.ts new file mode 100644 index 000000000..16f2e06cc --- /dev/null +++ b/examples/with-biconomy-aa/src/viem.ts @@ -0,0 +1,176 @@ +import * as path from "path"; +import * as dotenv from "dotenv"; +import prompts, { PromptType } from "prompts"; +import { + createWalletClient, + createPublicClient, + http, + type Account, + WalletClient, + formatEther, +} from "viem"; +import { sepolia } from "viem/chains"; +import { + createSmartAccountClient, + BiconomySmartAccountV2, + PaymasterMode, +} from "@biconomy/account"; + +// Load environment variables from `.env.local` +dotenv.config({ path: path.resolve(process.cwd(), ".env.local") }); + +import { createAccount } from "@turnkey/viem"; +import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server"; +import { createNewWallet } from "./createNewWallet"; +import { print } from "./util"; + +async function main() { + if (!process.env.SIGN_WITH) { + // If you don't specify a `SIGN_WITH`, we'll create a new wallet for you via calling the Turnkey API. + await createNewWallet(); + return; + } + + const turnkeyClient = new TurnkeyServerSDK({ + apiBaseUrl: process.env.BASE_URL!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + apiPublicKey: process.env.API_PUBLIC_KEY!, + defaultOrganizationId: process.env.ORGANIZATION_ID!, + }); + + // Initialize a Turnkey-powered Viem Account + const turnkeyAccount = await createAccount({ + client: turnkeyClient.apiClient(), + organizationId: process.env.ORGANIZATION_ID!, + signWith: process.env.SIGN_WITH!, + }); + + const network = "sepolia"; + + // Bring your own provider (such as Alchemy or Infura: https://docs.ethers.org/v6/api/providers/) + const client = createWalletClient({ + account: turnkeyAccount as Account, + chain: sepolia, + transport: http( + `https://${network}.infura.io/v3/${process.env.INFURA_KEY!}` + ), + }); + + const publicClient = createPublicClient({ + chain: sepolia, + transport: http( + `https://${network}.infura.io/v3/${process.env.INFURA_KEY!}` + ), + }); + + // Connect a TurnkeySigner to a Biconomy Smart Account Client, defaulting to Sepolia + // Ensure this method is hoisted + const connect = async ( + turnkeyClient: WalletClient + ): Promise => { + try { + const smartAccount = await createSmartAccountClient({ + signer: turnkeyClient, + bundlerUrl: process.env.BICONOMY_BUNDLER_URL!, // <-- Read about this at https://docs.biconomy.io/dashboard#bundler-url + biconomyPaymasterApiKey: process.env.BICONOMY_PAYMASTER_API_KEY!, // <-- Read about at https://docs.biconomy.io/dashboard/paymaster + rpcUrl: `https://${network}.infura.io/v3/${process.env.INFURA_KEY!}`, // <-- read about this at https://docs.biconomy.io/account/methods#createsmartaccountclient + chainId: Number(chainId), + }); + + return smartAccount; + } catch (error: any) { + throw new Error(error); + } + }; + + const chainId = client.chain.id; + const signerAddress = client.account.address; // signer + + const smartAccount = await connect(client); + const smartAccountAddress = await smartAccount.getAccountAddress(); + + const transactionCount = await publicClient.getTransactionCount({ + address: smartAccountAddress, + }); + const nonce = await smartAccount.getNonce(); + let balance = + (await publicClient.getBalance({ address: smartAccountAddress })) ?? 0; + + print("Network:", `${network} (chain ID ${chainId})`); + print("Signer address:", signerAddress); + print("Smart wallet address:", smartAccountAddress); + print("Balance:", `${formatEther(balance)} Ether`); + print("Transaction count:", `${transactionCount}`); + print("Nonce:", `${nonce}`); + + while (balance === 0n) { + console.log( + [ + `\nšŸ’ø Your onchain balance is at 0! To continue this demo you'll need testnet funds! You can use:`, + `- Any online faucet (e.g. https://www.alchemy.com/faucets/)`, + `\nTo check your balance: https://${network}.etherscan.io/address/${smartAccountAddress}`, + `\n--------`, + ].join("\n") + ); + + const { continue: _ } = await prompts([ + { + type: "text" as PromptType, + name: "continue", + message: "Ready to continue? y/n", + initial: "y", + }, + ]); + + balance = await publicClient.getBalance({ + address: smartAccountAddress, + })!; + } + + const { amount, destination } = await prompts([ + { + type: "number" as PromptType, + name: "amount", + message: "Amount to send (wei). Default to 0.0000001 ETH", + initial: 100000000000, + }, + { + type: "text" as PromptType, + name: "destination", + message: "Destination address (default to TKHQ warchest)", + initial: "0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7", + }, + ]); + const transactionRequest = { + to: destination, + value: amount, + type: 2, + }; + + // Make a simple send tx (which calls `signTransaction` under the hood) + const userOpResponse = await smartAccount?.sendTransaction( + transactionRequest, + { + paymasterServiceData: { mode: PaymasterMode.SPONSORED }, + } + ); + + const { transactionHash } = await userOpResponse.waitForTxHash(); + + print( + `Sent ${formatEther(transactionRequest.value)} Ether to ${ + transactionRequest.to + }:`, + `https://${network}.etherscan.io/tx/${transactionHash}` + ); + + print( + `User Ops can be found here:`, + `https://jiffyscan.xyz/bundle/${transactionHash}?network=${network}&pageNo=0&pageSize=10` + ); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9cb743a3..88dd79d97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,7 +574,7 @@ importers: dependencies: '@biconomy/account': specifier: ^4.5.6 - version: 4.5.6(typescript@5.1.5)(viem@1.16.6) + version: 4.5.6(typescript@5.1.5)(viem@2.21.29) '@turnkey/ethers': specifier: workspace:* version: link:../../packages/ethers @@ -594,8 +594,8 @@ importers: specifier: ^2.4.2 version: 2.4.2 viem: - specifier: ^1.16.6 - version: 1.16.6(typescript@5.1.5) + specifier: ^2.21.29 + version: 2.21.29(typescript@5.1.5) devDependencies: '@types/prompts': specifier: ^2.4.2 @@ -1736,6 +1736,10 @@ packages: /@adraffy/ens-normalize@1.10.0: resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + /@adraffy/ens-normalize@1.11.0: + resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + dev: false + /@adraffy/ens-normalize@1.9.4: resolution: {integrity: sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw==} dev: false @@ -4669,7 +4673,7 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@biconomy/account@4.5.6(typescript@5.1.5)(viem@1.16.6): + /@biconomy/account@4.5.6(typescript@5.1.5)(viem@2.21.29): resolution: {integrity: sha512-Mq0X9HF4fsPTkf87eXklJJE/Hl3GQwQHN/2/D1fCA4Q4o4AM9HV7Tzpb+hBsh8cUJ+s2j0Q9wORXA1c0onAImQ==} peerDependencies: typescript: ^5 @@ -4678,7 +4682,7 @@ packages: '@silencelaboratories/walletprovider-sdk': 0.1.0(typescript@5.1.5) merkletreejs: 0.4.0 typescript: 5.1.5 - viem: 1.16.6(typescript@5.1.5) + viem: 2.21.29(typescript@5.1.5) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6744,6 +6748,13 @@ packages: dependencies: '@noble/hashes': 1.4.0 + /@noble/curves@1.6.0: + resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==} + engines: {node: ^14.21.3 || >=16} + dependencies: + '@noble/hashes': 1.5.0 + dev: false + /@noble/ed25519@2.1.0: resolution: {integrity: sha512-KM4qTyXPinyCgMzeYJH/UudpdL+paJXtY3CHtHYZQtBkS8MZoPr4rOikZllIutJe0d06QDQKisyn02gxZ8TcQA==} dev: false @@ -6763,6 +6774,11 @@ packages: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} + /@noble/hashes@1.5.0: + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + engines: {node: ^14.21.3 || >=16} + dev: false + /@noble/secp256k1@1.7.1: resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} @@ -9866,27 +9882,31 @@ packages: /@scure/base@1.1.7: resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} + dev: false + + /@scure/base@1.1.9: + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} /@scure/bip32@1.1.5: resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} dependencies: '@noble/hashes': 1.2.0 '@noble/secp256k1': 1.7.1 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 /@scure/bip32@1.3.2: resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} dependencies: '@noble/curves': 1.2.0 '@noble/hashes': 1.3.3 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 /@scure/bip32@1.3.3: resolution: {integrity: sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==} dependencies: '@noble/curves': 1.3.0 '@noble/hashes': 1.3.3 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 dev: false /@scure/bip32@1.4.0: @@ -9894,32 +9914,47 @@ packages: dependencies: '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + /@scure/bip32@1.5.0: + resolution: {integrity: sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==} + dependencies: + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 '@scure/base': 1.1.7 + dev: false /@scure/bip39@1.1.1: resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==} dependencies: '@noble/hashes': 1.2.0 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 /@scure/bip39@1.2.1: resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} dependencies: '@noble/hashes': 1.3.3 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 /@scure/bip39@1.2.2: resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} dependencies: '@noble/hashes': 1.3.3 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 dev: false /@scure/bip39@1.3.0: resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} dependencies: '@noble/hashes': 1.4.0 - '@scure/base': 1.1.7 + '@scure/base': 1.1.9 + + /@scure/bip39@1.4.0: + resolution: {integrity: sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==} + dependencies: + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 + dev: false /@sentry/core@5.30.0: resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} @@ -10002,7 +10037,7 @@ packages: resolution: {integrity: sha512-53fV1noQJDUN9JNydDohyzsFl4+QYoWNkkkAfRzmIgtv+6DR+Dksb0fKmme2WdtA8MPEw/HsRwN3Lr6YC3iF7A==} dependencies: '@noble/curves': 1.4.2 - viem: 2.18.8(typescript@5.1.5) + viem: 2.21.29(typescript@5.1.5) transitivePeerDependencies: - bufferutil - typescript @@ -11019,7 +11054,7 @@ packages: resolution: {integrity: sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og==} dependencies: '@babel/runtime': 7.25.6 - '@noble/curves': 1.4.2 + '@noble/curves': 1.6.0 '@noble/hashes': 1.4.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.5.0 @@ -12852,11 +12887,11 @@ packages: typescript: 5.1.3 dev: false - /abitype@0.9.8(typescript@5.1.5): - resolution: {integrity: sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==} + /abitype@1.0.0(typescript@5.1.5): + resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} peerDependencies: typescript: '>=5.0.4' - zod: ^3 >=3.19.1 + zod: ^3 >=3.22.0 peerDependenciesMeta: typescript: optional: true @@ -12866,8 +12901,8 @@ packages: typescript: 5.1.5 dev: false - /abitype@1.0.0(typescript@5.1.5): - resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} + /abitype@1.0.5(typescript@5.1.5)(zod@3.23.8): + resolution: {integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==} peerDependencies: typescript: '>=5.0.4' zod: ^3 >=3.22.0 @@ -12878,10 +12913,10 @@ packages: optional: true dependencies: typescript: 5.1.5 - dev: false + zod: 3.23.8 - /abitype@1.0.5(typescript@5.1.5)(zod@3.23.8): - resolution: {integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==} + /abitype@1.0.6(typescript@5.1.5): + resolution: {integrity: sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==} peerDependencies: typescript: '>=5.0.4' zod: ^3 >=3.22.0 @@ -12892,7 +12927,7 @@ packages: optional: true dependencies: typescript: 5.1.5 - zod: 3.23.8 + dev: false /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -17326,6 +17361,14 @@ packages: dependencies: ws: 8.17.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + /isows@1.0.6(ws@8.17.1): + resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} + peerDependencies: + ws: '*' + dependencies: + ws: 8.17.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + dev: false + /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -22527,29 +22570,6 @@ packages: - zod dev: false - /viem@1.16.6(typescript@5.1.5): - resolution: {integrity: sha512-jcWcFQ+xzIfDwexwPJRvCuCRJKEkK9iHTStG7mpU5MmuSBpACs4nATBDyXNFtUiyYTFzLlVEwWkt68K0nCSImg==} - peerDependencies: - typescript: '>=5.0.4' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@adraffy/ens-normalize': 1.9.4 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@scure/bip32': 1.3.2 - '@scure/bip39': 1.2.1 - abitype: 0.9.8(typescript@5.1.5) - isows: 1.0.3(ws@8.17.1) - typescript: 5.1.5 - ws: 8.17.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - zod - dev: false - /viem@2.1.1(typescript@5.1.5): resolution: {integrity: sha512-gJiwYceD7Dsjioglr+85GQS3u5Gp9XGG8oJqGsauBaEPFlkmbRx7cxD2Q5RZXFToVvEbarOWtITZtGHBsGv4MQ==} peerDependencies: @@ -22619,6 +22639,31 @@ packages: - bufferutil - utf-8-validate - zod + dev: true + + /viem@2.21.29(typescript@5.1.5): + resolution: {integrity: sha512-n9LoCJjmI1XsE33nl+M4p3Wy5hczv7YC682RpX4Qk9cw8s9HJU+hUi5eDcNDPBcAwIHGCPKsf8yFBEYnE2XYVg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@scure/bip32': 1.5.0 + '@scure/bip39': 1.4.0 + abitype: 1.0.6(typescript@5.1.5) + isows: 1.0.6(ws@8.17.1) + typescript: 5.1.5 + webauthn-p256: 0.0.10 + ws: 8.17.1(bufferutil@4.0.7)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + dev: false /viem@2.7.19(typescript@5.1.5): resolution: {integrity: sha512-UOMeqy+8p2709ra2j9HEOL1NfjsXZzlJ8gwR6YO/zXH8KIZvyzW07t4iQARF5+ShVZ/7+/1ec8oPjVi1M//33A==} @@ -22763,7 +22808,7 @@ packages: resolution: {integrity: sha512-qYj34te2UctoObt8rlEIY/t2MuTMiMiiHhO2JAHRGqSLCQ7b8DM3RpvkiiSB0N0ZyEn+CetZqJCTYb8DNKBS/g==} engines: {node: '>=14', npm: '>=6.12.0'} dependencies: - '@adraffy/ens-normalize': 1.10.0 + '@adraffy/ens-normalize': 1.11.0 web3-core: 4.3.2 web3-errors: 1.1.4 web3-eth: 4.5.0(typescript@5.1.5) @@ -22951,6 +22996,13 @@ packages: - zod dev: false + /webauthn-p256@0.0.10: + resolution: {integrity: sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==} + dependencies: + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + dev: false + /webauthn-p256@0.0.5: resolution: {integrity: sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg==} dependencies: From 7a39e48a286d57dee07a4e9bb89e9265885f6e97 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Fri, 18 Oct 2024 12:46:43 -0400 Subject: [PATCH 4/6] update readme --- examples/with-biconomy-aa/README.md | 134 ++++++++++++---------------- 1 file changed, 57 insertions(+), 77 deletions(-) diff --git a/examples/with-biconomy-aa/README.md b/examples/with-biconomy-aa/README.md index 6be62b0dd..e13b2659c 100644 --- a/examples/with-biconomy-aa/README.md +++ b/examples/with-biconomy-aa/README.md @@ -1,8 +1,8 @@ -# Example: `with-ethers` +# Example: `with-biconomy-aa` -This example shows how to construct and broadcast a transaction using [`Ethers`](https://docs.ethers.org/v6/api/providers/#Signer) with Turnkey. +This example shows how to construct and broadcast a transaction using Turnkey with [`Ethers`](https://docs.ethers.org/v6/api/providers/#Signer), [`Viem`](https://viem.sh/docs/clients/wallet.html), and [`Biconomy`](https://docs.biconomy.io/account). -If you want to see a demo with passkeys, head to the example [`with-ethers-and-passkeys`](../with-ethers-and-passkeys/) to see a NextJS app using passkeys. +If you want to see a demo with passkeys, it's coming šŸ”œā„¢ļø! ## Getting started @@ -16,10 +16,10 @@ $ cd sdk/ $ corepack enable # Install `pnpm` $ pnpm install -r # Install dependencies $ pnpm run build-all # Compile source code -$ cd examples/with-ethers/ +$ cd examples/with-biconomy-aa/ ``` -### 2/ Setting up Turnkey +### 2a/ Setting up Turnkey The first step is to set up your Turnkey organization and account. By following the [Quickstart](https://docs.turnkey.com/getting-started/quickstart) guide, you should have: @@ -27,6 +27,13 @@ The first step is to set up your Turnkey organization and account. By following - An organization ID - A Turnkey wallet account (address), private key address, or a private key ID +### 2b/ Setting up Biconomy + +The next step is to navigate to Biconomy to create a paymaster. Visit the [Biconomy Dashboard](https://dashboard.biconomy.io/) to create a your paymaster and find the following: + +- Bundler URL +- Paymaster API Key + Once you've gathered these values, add them to a new `.env.local` file. Notice that your private key should be securely managed and **_never_** be committed to git. ```bash @@ -41,111 +48,84 @@ Now open `.env.local` and add the missing environment variables: - `ORGANIZATION_ID` - `SIGN_WITH` -- a Turnkey wallet account address, private key address, or private key ID. If you leave this blank, we'll create a wallet for you. - `INFURA_KEY` -- if this is not set, it will default to using the Community Infura key +- `BICONOMY_BUNDLER_URL` +- `BICONOMY_PAYMASTER_API_KEY` ### 3/ Running the scripts -Note: there are multiple scripts included. See `package.json` for all of them. The following is the default: +Note: there are two included ā€” one for Viem and another for Ethers. See `package.json` for more details. + +These scripts construct transactions via Turnkey and broadcast them via Infura. If the scripts exit because your account isn't funded, you can request funds on https://sepoliafaucet.com/ or https://faucet.paradigm.xyz/. + +#### Viem ```bash -$ pnpm start +$ pnpm start-viem ``` This script will do the following: -1. sign a raw payload -2. send ETH (via type 2 EIP-1559 transaction) -3. deposit ETH into the WETH contract (aka wrapping) - -Note that these transactions will all be broadcasted sequentially. - -The script constructs a transaction via Turnkey and broadcasts via Infura. If the script exits because your account isn't funded, you can request funds on https://sepoliafaucet.com/ or https://faucet.paradigm.xyz/. - -Visit the Etherscan link to view your transaction; you have successfully sent your first transaction with Turnkey! +1. instantiate a Turnkey Viem wallet client +2. instantiate a Viem public client (to be used to fetch onchain data) +3. connect the wallet client to the Biconomy paymaster +4. send ETH (via type 2 EIP-1559 transaction) See the following for a sample output: ``` Network: - sepolia (chain ID 11155111) + sepolia (chain ID 11155111) -Address: - 0x064c0CfDD7C485Eba21988Ded4dbCD9358556842 +Signer address: + 0xDC608F098255C89B36da905D9132A9Ee3DD266D9 + +Smart wallet address: + 0x7fDD1569812a168fe4B6637943BD36ec2c836A6A Balance: - 0.07750465249126655 Ether + 0.0499994 Ether Transaction count: - 14 - -Turnkey-powered signature: - 0x97da598ac1ad566e77be7c7d9cc77339730e48c557c5d6f32f93d9fdeeed13472b1faf20f1e457a897a409f31b9e680ad6b02086ac4fb9aa693ce10374976b201c - -Recovered address: - 0x064c0CfDD7C485Eba21988Ded4dbCD9358556842 - -Turnkey-signed transaction: - 0x02f8668080808080942ad9ea1e677949a536a270cec812d6e868c881088609184e72a00080c001a09881f59e48500ef8960ae1cb94e0c862e7d613f961c250b6f07b546a1b058b1da06ba1871d7aed5eb8ea8cb211a0e3e22a1c6b54b34b4376d0ef5b1daef4100c8f + 1 -Sent 0.00001 Ether to 0x2Ad9eA1E677949a536A270CEC812D6e868C88108: - https://sepolia.etherscan.io/tx/0xe034bdc597766719aef04b1d08998e606e85da1dd73e52fad8586a7d79d659e0 +Nonce: + 9 -WETH Balance: - 0.00007 WETH +āœ” Amount to send (wei). Default to 0.0000001 ETH ā€¦ 100000000000 +āœ” Destination address (default to TKHQ warchest) ā€¦ 0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7 +Sent 0.0000001 Ether to 0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7: + https://sepolia.etherscan.io/tx/0x2f2d996d6b262ebf0263b432ca3e6d621ba42d60b92344f31cf3ed94d09f49c4 -Wrapped 0.00001 ETH: - https://sepolia.etherscan.io/tx/0x7f98c1b2c7ff7f8ab876b27fdcd794653d8b7f728dbeec3b1d403789c38bcb71 +User Ops can be found here: + https://jiffyscan.xyz/bundle/0x2f2d996d6b262ebf0263b432ca3e6d621ba42d60b92344f31cf3ed94d09f49c4?network=sepolia&pageNo=0&pageSize=10 ``` -Note: if you have a consensus-related policy resembling the following - -``` -{ - "effect": "EFFECT_ALLOW", - "consensus": "approvers.count() >= 2" -} -``` - -then the script will await consensus to be met. Specifically, the script will attempt to poll for activity completion per the `activityPoller` config passed to the `TurnkeyServerSDK`. If consensus still isn't met during this period, then the resulting `Consensus Needed` error will be caught, and the script will prompt the user to indicate when consensus has been met. At that point, the script will continue. - -```bash -$ pnpm start-legacy-sepolia -``` - -This script will do the following: - -1. send ETH (via type 0, EIP-155-compliant legacy transaction) -2. deposit ETH into the WETH contract (aka wrapping) - -Note that these transactions will all be broadcasted sequentially. - -The script constructs a transaction via Turnkey and broadcasts via Infura. If the script exits because your account isn't funded, you can request funds on https://sepoliafaucet.com/ or via Coinbase Wallet. - -Visit the Etherscan link to view your transaction; you have successfully sent your first transaction with Turnkey! - -See the following for a sample output: +#### Ethers ``` Network: - sepolia (chain ID 11155111) + sepolia (chain ID 11155111) + +Signer address: + 0xDC608F098255C89B36da905D9132A9Ee3DD266D9 -Address: - 0xc4f1EF91ea582E3020E9ac155c3b5B27ce1185Dd +Smart wallet address: + 0x7fDD1569812a168fe4B6637943BD36ec2c836A6A Balance: - 0.049896964862611 Ether + 0.0499993 Ether Transaction count: - 4 - -Turnkey-signed transaction: - 0xf86c048308b821825208942ad9ea1e677949a536a270cec812d6e868c881088609184e72a000808401546d72a0883137063bfa04e1c6be6f79789f53e4226455ae1cbc4d610d164334a6e12c83a06dae6bd75b6cb28a7ed2548f207f860dd56a49c4bd63a642d7728d592225e408 + 1 -Sent 0.00001 Ether to 0x2Ad9eA1E677949a536A270CEC812D6e868C88108: - https://sepolia.etherscan.io/tx/0xf4c3e6bd5c6a635088dc7fc7c0d7a715beb340a7fbff67daf0adc666709e23f1 +Nonce: + 10 -WETH Balance: - 0.0 WETH +āœ” Amount to send (wei). Default to 0.0000001 ETH ā€¦ 100000000000 +āœ” Destination address (default to TKHQ warchest) ā€¦ 0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7 +Sent 0.0000001 Ether to 0x08d2b0a37F869FF76BACB5Bab3278E26ab7067B7: + https://sepolia.etherscan.io/tx/0x0f0d5346ba726f7ccf80142ae295f28bf3873b0aeb7b29488b1e3dfb949d5ba6 -Wrapped 0.00001 ETH: - https://sepolia.etherscan.io/tx/0x428a6f3c24f6f0c2de34f41776566c875bd56bfe4d5d8db4a7ef57c2c4e69dec +User Ops can be found here: + https://jiffyscan.xyz/bundle/0x0f0d5346ba726f7ccf80142ae295f28bf3873b0aeb7b29488b1e3dfb949d5ba6?network=sepolia&pageNo=0&pageSize=10 ``` From 527ae48c55c59a3c97244bd836023c1df91d896b Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Fri, 18 Oct 2024 14:23:25 -0400 Subject: [PATCH 5/6] nit --- examples/with-biconomy-aa/src/ethers.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/with-biconomy-aa/src/ethers.ts b/examples/with-biconomy-aa/src/ethers.ts index 040ef3e91..33e3f5742 100644 --- a/examples/with-biconomy-aa/src/ethers.ts +++ b/examples/with-biconomy-aa/src/ethers.ts @@ -126,8 +126,6 @@ async function main() { const transactionRequest = { to: destination, value: amount, - // nonce, - // nonce: transactionCount, type: 2, }; From 3306c93672dd2c0398d4cc1d6e9dab71cbd3a9d2 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Wed, 23 Oct 2024 12:56:04 -0400 Subject: [PATCH 6/6] feedback: clarify provider --- examples/with-biconomy-aa/src/ethers.ts | 2 +- examples/with-biconomy-aa/src/viem.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/with-biconomy-aa/src/ethers.ts b/examples/with-biconomy-aa/src/ethers.ts index 33e3f5742..d0564361e 100644 --- a/examples/with-biconomy-aa/src/ethers.ts +++ b/examples/with-biconomy-aa/src/ethers.ts @@ -38,7 +38,7 @@ async function main() { signWith: process.env.SIGN_WITH!, }); - // Bring your own provider (such as Alchemy or Infura: https://docs.ethers.org/v6/api/providers/) + // Bring your own provider const network = "sepolia"; const provider = new ethers.JsonRpcProvider( `https://${network}.infura.io/v3/${process.env.INFURA_KEY}` diff --git a/examples/with-biconomy-aa/src/viem.ts b/examples/with-biconomy-aa/src/viem.ts index 16f2e06cc..08953971d 100644 --- a/examples/with-biconomy-aa/src/viem.ts +++ b/examples/with-biconomy-aa/src/viem.ts @@ -47,7 +47,7 @@ async function main() { const network = "sepolia"; - // Bring your own provider (such as Alchemy or Infura: https://docs.ethers.org/v6/api/providers/) + // Bring your own provider const client = createWalletClient({ account: turnkeyAccount as Account, chain: sepolia,