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
37 changes: 37 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,42 @@ 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
...(PRIVY_WALLET_AUTHORIZATION_KEY && {
authorizationKey: PRIVY_WALLET_AUTHORIZATION_KEY, // optional, if using authorization keys
}),
};

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

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

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.

@0xRAG @azf20 - This config should take chainId instead of networkId . Network ID is a CDP concept and will limit the Privy implementation to only support CDP supported networks. Privy does not have the same constraints as CdpWalletProvider and therefore should take chainId which will unblock usage of all EVM.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call! This is done now in ba189e3

/** Optional authorization key for the wallet API */
authorizationKey?: string;

Choose a reason for hiding this comment

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

nit: consider renaming this to authorizationPrivateKey so it is clear this is the private key, not the public key or ID, also to be parallel with the Privy config

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call, done

}

/**
* 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;
#authorizationKey: 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.#authorizationKey = config.authorizationKey;
}

/**
* 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: "base-sepolia"

Choose a reason for hiding this comment

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

nit: update to chainId: 84532

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch!

* });
* ```
*/
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.authorizationKey
? {
authorizationPrivateKey: config.authorizationKey,
}
: undefined,
});

// Get wallet details to get the address
const walletId =
config.walletId ??
(
await privy.walletApi.create({

Choose a reason for hiding this comment

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

nit: this call returns the whole wallet object (including the address), so if you shift things around a bit you can save the second network hop below to getWallet

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nice yes did that too

chaintype: "ethereum",
authorizationKeyIds: config.authorizationKey ? [config.authorizationKey] : undefined,

Choose a reason for hiding this comment

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

This actually takes as an input an authorizationKeyId which is an ID assigned by to an authorization key when it is registered with Privy server wallets. In order to use an authorization key with a wallet, you need to pre register it with Privy. We are actually working on supporting key registration during wallet creation, but it is not implemented yet.

I can see two paths forward here:

  1. You can implement authorization key registration with Privy before wallet creation. We don't have support for it in server-auth but we do via our REST API (docs - see REST API tab). The developer would generate the p256 private + public keypair (instructions in above docs as well) and input them both as a config, then you would send the public key to privy, and you'll get back an ID which you'd use during wallet creation. The server-auth config above correctly takes in the private key.
  2. You can take in an authorizationKeyId in the config, however that would require the developer to register the key with Privy themselves beforehand, which isn't an ideal experience.

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 for that context, sorry I had missed that you need to pass IDs for creation.

I tried 1., but hit an issue where the https://api.privy.io/v1/wallets endpoint was returning a 404 - see this branch on my project repository as a test case azf20/hello-world-computer@cd61b55

So I reverted to 2, but also found that an authorization key was required on new wallet creation, not sure if that is expected? Got the following when I didn't pass in an auth key:

 ⨯ Error: Missing `privy-authorization-signature` header.

So I updated the error handling and docs accordingly - not an ideal experience but I think ok for now

Choose a reason for hiding this comment

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

Good catch - I'll look into this on our end and I think option 2 + pointing folks to the Privy dashboard to create their key is a good approach for now.

})
).id;

const wallet = await privy.walletApi.getWallet({ id: walletId });

const account = await createViemAccount({
walletId,
address: wallet.address as `0x${string}`,
privy,
});

const network = {
protocolFamily: "evm" as const,
networkId: config.networkId || "base-sepolia",
};

const chain = NETWORK_ID_TO_VIEM_CHAIN[network.networkId!];

Choose a reason for hiding this comment

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

As @John-peterson-coinbase mentioned above, Privy can support any EVM chain, so you can update the config to take in a chainId (eg 84532 for base sepolia). In order to resolve the viem chain object, here is a post I've found helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, thanks for the hint!

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;
authorizationKey: string | undefined;
networkId?: string;
} {
return {
walletId: this.#walletId,
authorizationKey: this.#authorizationKey,
networkId: this.getNetwork().networkId,
};
}
}
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
11 changes: 11 additions & 0 deletions typescript/examples/langchain-privy-chatbot/.env-local
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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 greated
PRIVY_WALLET_ID=

# Optional Authorization Key, if you are using them for your server wallets
PRIVY_WALLET_AUTHORIZATION_KEY=
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"]
}
49 changes: 49 additions & 0 deletions typescript/examples/langchain-privy-chatbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 wallets are embedded wallets - 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.

Choose a reason for hiding this comment

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

consider replacing "Privy wallets are embedded wallets" with "Privy's server wallets enable you to securely provision and manage cross-chain wallets via a flexible API"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1


## 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_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.

I think it is worth specifying that this is the authorization private key

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1


```bash
npm start
```

## License

Apache-2.0
Loading