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
42 changes: 42 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,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 +544,47 @@ 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",
networkId: "base-sepolia",
Copy link
Contributor

Choose a reason for hiding this comment

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

please update to chainId as previously discussed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

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. When using authorization keys, you must provide the `authorizationPrivateKey` and `authorizationKeyId` parameters to the `configureWithWallet` method.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please hyperlink to Privy documentation on Authorization Keys given this may be a new concept to AgentKit developers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shuffled it around to put the link to the docs before this 👍


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).

#### 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;
networkId: string;
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

please update to chainId as previously discussed

Also - should this return value be a named interface to make it more clear?

Let's also add an example of instantiating a PrivyWalletProvider from the exported object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍



## 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
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";
165 changes: 165 additions & 0 deletions typescript/agentkit/src/wallet-providers/privyWalletProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
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 * as chains from "viem/chains";

/**
* Get a chain from the viem chains object
*
* @param id - The chain ID
* @returns The chain
*/
function getChain(id: number) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

const chainList = Object.values(chains);
return chainList.find(chain => chain.id === id);
}

/**
* 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 network ID to connect to */
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/** Optional network ID to connect to */
/** Optional chain ID to connect to */

chainId?: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
chainId?: number;
chainId?: string;

Update type to conform with Network interface typing.

/** Optional authorization key for the wallet API */
authorizationPrivateKey?: string;
/** Optional authorization key ID for creating new wallets */
authorizationKeyId?: string;
}

/**
* 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",
* networkId: 84532
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* networkId: 84532
* 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) {
throw new Error("authorizationPrivateKey is required when creating a new wallet");
}
if (!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",
);
Copy link
Contributor

Choose a reason for hiding this comment

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

It's my understanding that an authorization key is optional when creating a Privy server wallet, so can we relax the requirement here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was also my understanding, though when I tested the header was required - @colfax23 can you confirm if that's only in the case where an account has set up Authorization keys (which mine had)? If so we can update to helpfully handle the resulting error, rather than blocking here

Choose a reason for hiding this comment

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

Authorization keys are not required for wallet creation unless you've created one in the dashboard and have given in the permissions Can create and modify wallets. If you've done that, we require it for all wallet creation requests.

Two pieces of feedback we're taking from this & working on:

  • This UX of requiring it in all cases if a key is created is not intuitive, we're working on some updates here
  • The error message when you try to create a wallet without specifying the key, when it is required, is not very clear (it is Missing privy-authorization-signature header.)

For the next steps, I think we can relax this constraint, and @azf20 if you can handle the error gracefully if a key is required but not present that'd be great!

Also, @azf20 when you mention in the README about creating authorization keys in the Privy dashboard, can you add a note to uncheck Can create and modify wallets to help prevent people from getting into this state by mistake like you did? That way they can create a key that is intended for wallet transaction signing purposes only.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it, thanks for the clarification @colfax23!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks both! I added a more helpful error message for that specific case + updated the README

}

const wallet = await privy.walletApi.create({
chainType: "ethereum",
authorizationKeyIds: [config.authorizationKeyId],
});
walletId = wallet.id;
address = wallet.address as `0x${string}`;
} 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 network = {
protocolFamily: "evm" as const,
chainId: config.chainId || 84532,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Include networkId in network object. You can use the record here to map between chain ID and network 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.

Hmm actually thinking do we need this network object? we only use it to fetch the Viem chain, to be passed into the Viem WalletClient - could just pass chainId in directly? the underlying viem provider actually provides the network info https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/wallet-providers/viemWalletProvider.ts#L170


const chain = getChain(network.chainId);
if (!chain) {
throw new Error(`Chain with ID ${network.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(): {
walletId: string;
authorizationPrivateKey: string | undefined;
networkId?: string;
} {
return {
walletId: this.#walletId,
authorizationPrivateKey: this.#authorizationPrivateKey,
networkId: this.getNetwork().networkId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
networkId?: string;
} {
return {
walletId: this.#walletId,
authorizationPrivateKey: this.#authorizationPrivateKey,
networkId: this.getNetwork().networkId,
chainId: string;
} {
return {
walletId: this.#walletId,
authorizationPrivateKey: this.#authorizationPrivateKey,
chainId: this.getNetwork().chainId,

Update to use chain ID as previously discussed.

This should return a named interface object for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

};
}
}
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