Skip to content

Commit

Permalink
wip continued
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkmin committed Oct 23, 2024
1 parent c3424e7 commit 6921265
Show file tree
Hide file tree
Showing 9 changed files with 799 additions and 98 deletions.
9 changes: 9 additions & 0 deletions examples/with-zerodev-aa/.env.local.example
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/
131 changes: 131 additions & 0 deletions examples/with-zerodev-aa/README.md
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
```
28 changes: 28 additions & 0 deletions examples/with-zerodev-aa/package.json
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"
}
}
50 changes: 50 additions & 0 deletions examples/with-zerodev-aa/src/createNewWallet.ts
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);
}
}
142 changes: 142 additions & 0 deletions examples/with-zerodev-aa/src/ethers.ts
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);
});
24 changes: 24 additions & 0 deletions examples/with-zerodev-aa/src/util.ts
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));
}
Loading

0 comments on commit 6921265

Please sign in to comment.