-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c3424e7
commit 6921265
Showing
9 changed files
with
799 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
API_PUBLIC_KEY="<Turnkey API Public Key (that starts with 02 or 03)>" | ||
API_PRIVATE_KEY="<Turnkey API Private Key>" | ||
BASE_URL="https://api.turnkey.com" | ||
ORGANIZATION_ID="<Turnkey organization ID>" | ||
SIGN_WITH="<Turnkey Wallet Account Address, Private Key Address, or Private Key ID>" # if blank, we will create a wallet for you | ||
INFURA_KEY="<Infura API Key>" | ||
ZERODEV_PROJECT_ID="<Zerodev Project ID>" | ||
ZERODEV_BUNDLER_RPC="<Zerodev Bundler RPC URL>" # see https://dashboard.zerodev.app/ | ||
ZERODEV_PAYMASTER_RPC="<Zerodev Paymaster RPC URL>" # see https://dashboard.zerodev.app/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
# Example: `with-biconomy-aa` | ||
|
||
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, it's coming 🔜™️! | ||
|
||
## 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-biconomy-aa/ | ||
``` | ||
|
||
### 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: | ||
|
||
- A public/private API key pair for Turnkey | ||
- 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 | ||
$ 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 | ||
- `BICONOMY_BUNDLER_URL` | ||
- `BICONOMY_PAYMASTER_API_KEY` | ||
|
||
### 3/ Running the scripts | ||
|
||
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-viem | ||
``` | ||
|
||
This script will do the following: | ||
|
||
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) | ||
Signer address: | ||
0xDC608F098255C89B36da905D9132A9Ee3DD266D9 | ||
Smart wallet address: | ||
0x7fDD1569812a168fe4B6637943BD36ec2c836A6A | ||
Balance: | ||
0.0499994 Ether | ||
Transaction count: | ||
1 | ||
Nonce: | ||
9 | ||
✔ 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 | ||
User Ops can be found here: | ||
https://jiffyscan.xyz/bundle/0x2f2d996d6b262ebf0263b432ca3e6d621ba42d60b92344f31cf3ed94d09f49c4?network=sepolia&pageNo=0&pageSize=10 | ||
``` | ||
|
||
#### Ethers | ||
|
||
``` | ||
Network: | ||
sepolia (chain ID 11155111) | ||
Signer address: | ||
0xDC608F098255C89B36da905D9132A9Ee3DD266D9 | ||
Smart wallet address: | ||
0x7fDD1569812a168fe4B6637943BD36ec2c836A6A | ||
Balance: | ||
0.0499993 Ether | ||
Transaction count: | ||
1 | ||
Nonce: | ||
10 | ||
✔ 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 | ||
User Ops can be found here: | ||
https://jiffyscan.xyz/bundle/0x0f0d5346ba726f7ccf80142ae295f28bf3873b0aeb7b29488b1e3dfb949d5ba6?network=sepolia&pageNo=0&pageSize=10 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "@turnkey/example-with-biconomy-aa", | ||
"version": "0.1.0", | ||
"private": true, | ||
"scripts": { | ||
"start-ethers": "tsx src/ethers.ts", | ||
"start-viem": "tsx src/viem.ts", | ||
"clean": "rimraf ./dist ./.cache", | ||
"typecheck": "tsc --noEmit" | ||
}, | ||
"dependencies": { | ||
"@turnkey/ethers": "workspace:*", | ||
"@turnkey/sdk-server": "workspace:*", | ||
"@turnkey/viem": "workspace:*", | ||
"@zerodev/ecdsa-validator": "^5.3.3", | ||
"@zerodev/sdk": "^5.3.22", | ||
"dotenv": "^16.0.3", | ||
"ethers": "^6.10.0", | ||
"permissionless": "^0.2.10", | ||
"prompts": "^2.4.2", | ||
"viem": "^2.18.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^22.7.7", | ||
"@types/prompts": "^2.4.2", | ||
"tslib": "^2.8.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Turnkey as TurnkeySDKServer } from "@turnkey/sdk-server"; | ||
|
||
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 TurnkeySDKServer({ | ||
apiBaseUrl: "https://api.turnkey.com", | ||
apiPublicKey: process.env.API_PUBLIC_KEY!, | ||
apiPrivateKey: process.env.API_PRIVATE_KEY!, | ||
defaultOrganizationId: process.env.ORGANIZATION_ID!, | ||
}); | ||
|
||
const { walletId, addresses } = await turnkeyClient | ||
.apiClient() | ||
.createWallet({ | ||
walletName, | ||
accounts: [ | ||
{ | ||
curve: "CURVE_SECP256K1", | ||
pathFormat: "PATH_FORMAT_BIP32", | ||
path: "m/44'/60'/0'/0/0", | ||
addressFormat: "ADDRESS_FORMAT_ETHEREUM", | ||
}, | ||
], | ||
}); | ||
|
||
const newWalletId = refineNonNull(walletId); | ||
const address = refineNonNull(addresses[0]); | ||
|
||
// Success! | ||
console.log( | ||
[ | ||
`New Ethereum wallet created!`, | ||
`- Name: ${walletName}`, | ||
`- Wallet ID: ${newWalletId}`, | ||
`- Address: ${address}`, | ||
``, | ||
"Now you can take the address, put it in `.env.local` (`SIGN_WITH=<address>`), then re-run the script.", | ||
].join("\n") | ||
); | ||
} catch (error) { | ||
throw new Error("Failed to create a new Ethereum wallet: " + error); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import * as path from "path"; | ||
import * as dotenv from "dotenv"; | ||
import prompts, { PromptType } from "prompts"; | ||
import { ethers } from "ethers"; | ||
import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator"; | ||
import { KERNEL_V3_1 } from "@zerodev/sdk/constants"; | ||
// import { entryPoint07Address } from "viem/account-abstraction" | ||
import { toEcdsaKernelSmartAccount } from "permissionless/accounts" | ||
|
||
|
||
// 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 Zerodev | ||
const smartAccountSigner = walletClientToSmartAccountSigner(turnkeySigner); | ||
|
||
const chainId = (await connectedSigner.provider?.getNetwork())?.chainId ?? 0; | ||
const signerAddress = await connectedSigner.getAddress(); // signer | ||
|
||
const smartAccountAddress = await zerodevSigner.getAccountAddress(); | ||
|
||
const transactionCount = await connectedSigner.provider?.getTransactionCount( | ||
smartAccountAddress | ||
); | ||
const nonce = await zerodevSigner.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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export function print(header: string, body: string): void { | ||
console.log(`${header}\n\t${body}\n`); | ||
} | ||
|
||
export function assertEqual<T>(left: T, right: T) { | ||
if (left !== right) { | ||
throw new Error(`${JSON.stringify(left)} !== ${JSON.stringify(right)}`); | ||
} | ||
} | ||
|
||
export function refineNonNull<T>( | ||
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)); | ||
} |
Oops, something went wrong.