Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add privy server wallet provider #242

Merged
merged 13 commits into from
Feb 14, 2025
292 changes: 287 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"typescript/framework-extensions/langchain",
"typescript/examples/langchain-cdp-chatbot",
"typescript/examples/langchain-twitter-chatbot",
"typescript/examples/langchain-farcaster-chatbot"
"typescript/examples/langchain-farcaster-chatbot",
"typescript/examples/langchain-privy-chatbot"
],
"packageManager": "[email protected]",
"scripts": {
Expand Down Expand Up @@ -54,4 +55,4 @@
"typedoc": "^0.27.2",
"typescript": "^5.4.5"
}
}
}
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"strict": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"moduleResolution": "node",
"moduleResolution": "node16",
John-peterson-coinbase marked this conversation as resolved.
Show resolved Hide resolved
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Added `svmWalletProvider` abstract class for interacting with Solana.
- Added `solanaKeypairWalletProvider` to concretely implement `svmWalletProvider` with a local keypair.
- Added gas configuration parameters (`gasLimitMultiplier`, `feePerGasMultiplier`) to `CdpWalletProvider` and `ViemWalletProvider`.
- Added `privyWalletProvider` to use a Privy server wallet for agent actions.

## [0.1.2] - 2025-02-07

Expand Down
47 changes: 47 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ AgentKit is a framework for easily enabling AI agents to take actions onchain. I
- [Configuring gas parameters](#configuring-cdpwalletprovider-gas-parameters)
- [ViemWalletProvider](#viemwalletprovider)
- [Configuring gas parameters](#configuring-viemwalletprovider-gas-parameters)
- [PrivyWalletProvider](#privywalletprovider)
- [Authorization Keys](#authorization-keys)
- [Exporting Privy Wallet information](#exporting-privy-wallet-information)
- [Contributing](#contributing)

## Getting Started
Expand Down Expand Up @@ -398,6 +401,7 @@ Wallet providers give an agent access to a wallet. AgentKit currently supports t
EVM:
- [CdpWalletProvider](./src/wallet-providers/cdpWalletProvider.ts)
- [ViemWalletProvider](./src/wallet-providers/viemWalletProvider.ts)
- [PrivyWalletProvider](./src/wallet-providers/privyWalletProvider.ts)

### CdpWalletProvider

Expand Down Expand Up @@ -543,6 +547,49 @@ const walletProvider = new ViemWalletProvider(client, {
});
```

### PrivyWalletProvider
0xRAG marked this conversation as resolved.
Show resolved Hide resolved

The `PrivyWalletProvider` is a wallet provider that uses [Privy Server Wallets](https://docs.privy.io/guide/server-wallets/). This implementation extends the `ViemWalletProvider`.

```typescript
import { PrivyWalletProvider } from "@coinbase/agentkit";

// Configure Wallet Provider
const config = {
appId: "PRIVY_APP_ID",
appSecret: "PRIVY_APP_SECRET",
chainId: "84532", // base-sepolia
walletId: "PRIVY_WALLET_ID", // optional, otherwise a new wallet will be created
authorizationPrivateKey: PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY, // optional, required if your account is using authorization keys
authorizationKeyId: PRIVY_WALLET_AUTHORIZATION_KEY_ID, // optional, only required to create a new wallet if walletId is not provided
};

const walletProvider = await PrivyWalletProvider.configureWithWallet(config);
```

#### Authorization Keys

Privy offers the option to use authorization keys to secure your server wallets.

You can manage authorization keys from your [Privy dashboard](https://dashboard.privy.io/account) or programmatically [using the API](https://docs.privy.io/guide/server-wallets/authorization/signatures).

When using authorization keys, you must provide the `authorizationPrivateKey` and `authorizationKeyId` parameters to the `configureWithWallet` method if you are creating a new wallet. Please note that when creating a key, if you enable "Create and modify wallets", you will be required to use that key when creating new wallets via the PrivyWalletProvider.

#### Exporting Privy Wallet information

The `PrivyWalletProvider` can export wallet information by calling the `exportWallet` method.

```typescript
const walletData = await walletProvider.exportWallet();

// walletData will be in the following format:
{
walletId: string;
authorizationKey: string | undefined;
chainId: string | undefined;
}
```


## Contributing

Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@coinbase/coinbase-sdk": "^0.17.0",
"@solana/web3.js": "^1.98.0",
"@privy-io/server-auth": "^1.18.4",
"md5": "^2.3.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
Expand Down
12 changes: 12 additions & 0 deletions typescript/agentkit/src/network/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
polygonMumbai,
polygon,
} from "viem/chains";
import * as chains from "viem/chains";

/**
* Maps EVM chain IDs to Coinbase network IDs
Expand Down Expand Up @@ -56,3 +57,14 @@ export const NETWORK_ID_TO_VIEM_CHAIN: Record<string, Chain> = {
"optimism-mainnet": optimism,
"optimism-sepolia": optimismSepolia,
};

/**
* Get a chain from the viem chains object
*
* @param id - The chain ID
* @returns The chain
*/
export const getChain = (id: string): Chain => {
const chainList = Object.values(chains);
return chainList.find(chain => chain.id === parseInt(id)) as Chain;
};
1 change: 1 addition & 0 deletions typescript/agentkit/src/wallet-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./viemWalletProvider";
export * from "./cdpWalletProvider";
export * from "./svmWalletProvider";
export * from "./solanaKeypairWalletProvider";
export * from "./privyWalletProvider";
169 changes: 169 additions & 0 deletions typescript/agentkit/src/wallet-providers/privyWalletProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { PrivyClient } from "@privy-io/server-auth";
import { createViemAccount } from "@privy-io/server-auth/viem";
import { ViemWalletProvider } from "./viemWalletProvider";
import { createWalletClient, http, WalletClient } from "viem";
import { getChain } from "../network/network";
/**
* Configuration options for the Privy wallet provider.
*
* @interface
*/
interface PrivyWalletConfig {
John-peterson-coinbase marked this conversation as resolved.
Show resolved Hide resolved
/** The Privy application ID */
appId: string;
/** The Privy application secret */
appSecret: string;
/** The ID of the wallet to use, if not provided a new wallet will be created */
walletId?: string;
/** Optional chain ID to connect to */
chainId?: string;
/** Optional authorization key for the wallet API */
authorizationPrivateKey?: string;
/** Optional authorization key ID for creating new wallets */
authorizationKeyId?: string;
}

type PrivyWalletExport = {
walletId: string;
authorizationPrivateKey: string | undefined;
chainId: string | undefined;
};

/**
* A wallet provider that uses Privy's server wallet API.
* This provider extends the ViemWalletProvider to provide Privy-specific wallet functionality
* while maintaining compatibility with the base wallet provider interface.
*/
export class PrivyWalletProvider extends ViemWalletProvider {
#walletId: string;
#authorizationPrivateKey: string | undefined;

/**
* Private constructor to enforce use of factory method.
*
* @param walletClient - The Viem wallet client instance
* @param config - The configuration options for the Privy wallet
*/
private constructor(
walletClient: WalletClient,
config: PrivyWalletConfig & { walletId: string }, // Require walletId in constructor
) {
super(walletClient);
this.#walletId = config.walletId; // Now guaranteed to exist
this.#authorizationPrivateKey = config.authorizationPrivateKey;
}

/**
* Creates and configures a new PrivyWalletProvider instance.
*
* @param config - The configuration options for the Privy wallet
* @returns A configured PrivyWalletProvider instance
*
* @example
* ```typescript
* const provider = await PrivyWalletProvider.configureWithWallet({
* appId: "your-app-id",
* appSecret: "your-app-secret",
* walletId: "wallet-id",
* chainId: "84532"
* });
* ```
*/
public static async configureWithWallet(config: PrivyWalletConfig): Promise<PrivyWalletProvider> {
const privy = new PrivyClient(config.appId, config.appSecret, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an edge case here where the authorizationPrivateKey is provided but not authorizationKeyId which will get things into a funky state. Can you do a check to enforce that either both or neither are provided before initializing the PrivyClient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to initialise with just an authorizationPrivateKey if you're using an existing wallet, not creating a key?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right - if the user has an existing wallet that has a key attached to it, you need to specify authorizationPrivateKey and technically don't need to specify the ID. My bad! Fine to leave as is, good catch.

walletApi: config.authorizationPrivateKey
? {
authorizationPrivateKey: config.authorizationPrivateKey,
}
: undefined,
});

let walletId: string;
let address: `0x${string}`;

if (!config.walletId) {
if (config.authorizationPrivateKey && !config.authorizationKeyId) {
throw new Error(
"authorizationKeyId is required when creating a new wallet with an authorization key, this can be found in your Privy Dashboard",
);
}
0xRAG marked this conversation as resolved.
Show resolved Hide resolved

if (config.authorizationKeyId && !config.authorizationPrivateKey) {
throw new Error(
"authorizationPrivateKey is required when creating a new wallet with an authorizationKeyId. " +
"If you don't have it, you can create a new one in your Privy Dashboard, or delete the authorization key.",
);
}

try {
const wallet = await privy.walletApi.create({
chainType: "ethereum",
authorizationKeyIds: config.authorizationKeyId ? [config.authorizationKeyId] : undefined,
});
walletId = wallet.id;
address = wallet.address as `0x${string}`;
} catch (error) {
console.error(error);
if (
error instanceof Error &&
error.message.includes("Missing `privy-authorization-signature` header")
) {
// Providing a more informative error message, see context: https://github.com/coinbase/agentkit/pull/242#discussion_r1956428617
throw new Error(
"Privy error: you have an authorization key on your account which can create and modify wallets, please delete this key or pass it to the PrivyWalletProvider to create a new wallet",
);
}
throw new Error("Failed to create wallet");
}
} else {
walletId = config.walletId;
const wallet = await privy.walletApi.getWallet({ id: walletId });
if (!wallet) {
throw new Error(`Wallet with ID ${walletId} not found`);
}
address = wallet.address as `0x${string}`;
}

const account = await createViemAccount({
walletId,
address,
privy,
});

const chainId = config.chainId || "84532";

const chain = getChain(chainId);
if (!chain) {
throw new Error(`Chain with ID ${chainId} not found`);
}

const walletClient = createWalletClient({
account,
chain,
transport: http(),
});
return new PrivyWalletProvider(walletClient, { ...config, walletId });
}

/**
* Gets the name of the wallet provider.
*
* @returns The string identifier for this wallet provider
*/
getName(): string {
return "privy_wallet_provider";
}

/**
* Exports the wallet data.
*
* @returns The wallet data
*/
exportWallet(): PrivyWalletExport {
return {
walletId: this.#walletId,
authorizationPrivateKey: this.#authorizationPrivateKey,
chainId: this.getNetwork().chainId,
};
}
}
3 changes: 2 additions & 1 deletion typescript/agentkit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
"rootDir": "./src",
"module": "Node16"
},
"include": ["src/**/*.ts"],
"exclude": ["src/tests"]
Expand Down
14 changes: 14 additions & 0 deletions typescript/examples/langchain-privy-chatbot/.env-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
OPENAI_API_KEY=

# Privy Configuration - get these from your Privy dashboard
PRIVY_APP_ID=
PRIVY_APP_SECRET=

# Optional Wallet ID, otherwise a new wallet will be created
PRIVY_WALLET_ID=

# Optional Authorization Private Key, if you are using them for your server wallets
PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY=

# Optional Authorization Key ID from your Privy Dashboard, if you want to create a new wallet
PRIVY_WALLET_AUTHORIZATION_KEY_ID=
4 changes: 4 additions & 0 deletions typescript/examples/langchain-privy-chatbot/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"parser": "@typescript-eslint/parser",
"extends": ["../../../.eslintrc.base.json"]
}
52 changes: 52 additions & 0 deletions typescript/examples/langchain-privy-chatbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Privy AgentKit LangChain Extension Examples - Chatbot Typescript

This example demonstrates an agent setup as a self-aware terminal style chatbot with a [Privy server wallet](https://docs.privy.io/guide/server-wallets/).

Privy's server wallets enable you to securely provision and manage cross-chain wallets via a flexible API - learn more at https://docs.privy.io/guide/server-wallets/. The Agentkit integration assumes you have a Privy server wallet ID which you want to use for your agent - creation and management of Privy wallets can be done via the Privy dashboard or API.

## Ask the chatbot to engage in the Web3 ecosystem!

- "Transfer a portion of your ETH to a random address"
- "What is the price of BTC?"
- "What kind of wallet do you have?"

## Requirements

- Node.js 18+
- [Privy](https://dashboard.privy.io/apps) (see ENV Vars below for details)

### Checking Node Version

Before using the example, ensure that you have the correct version of Node.js installed. The example requires Node.js 18 or higher. You can check your Node version by running:

```bash
node --version
npm --version
```

## Installation

```bash
npm install
```

## Run the Chatbot

### Set ENV Vars

- Ensure the following ENV Vars from your Privy dashboard are set in `.env`:
- PRIVY_APP_ID=
- PRIVY_APP_SECRET=
- PRIVY_WALLET_ID=[optional, otherwise a new wallet will be created]
- PRIVY_WALLET_AUTHORIZATION_PRIVATE_KEY=[optional, only if you are using authorization keys for your server wallets]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you link the key creation docs and / or point folks to the Privy dashboard to create their key + register it with Privy to get their ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updating README

- PRIVY_WALLET_AUTHORIZATION_KEY_ID=[optional, only if walletId is not provided in order to create a new wallet, this can be found in your Privy Dashboard]

You can manage authorization keys from your [Privy dashboard](https://dashboard.privy.io/account) or programmatically [using the API](https://docs.privy.io/guide/server-wallets/authorization/signatures).

```bash
npm start
```

## License

Apache-2.0
Loading