feat: add privy server wallet provider#242
Conversation
✅ Heimdall Review Status
|
|
|
||
| const network = { | ||
| protocolFamily: "evm" as const, | ||
| networkId: config.networkId || "84532", |
There was a problem hiding this comment.
network ID for base sepolia is base-sepolia
|
thanks @John-peterson-coinbase, added fixes here f49f51d, including package-lock.json (though on reflection wasn't sure about the policy there). Let me know if there is anything else! |
|
Thanks @azf20 - please rebase and sign the commits following the commit signing instructions |
bbb3359 to
fca9cf3
Compare
|
thanks so much, I rebased and verified, hope that looks right! |
fca9cf3 to
452f2b0
Compare
|
Hey @azf20 I tested this locally and it's working great! As a couple final asks, could you:
Thank you! |
452f2b0 to
6d6eb39
Compare
|
OK great! I rebased and added an example - I added a comment explaining that Privy server wallets are embedded & controllable via the dashboard & API. Let me know if that makes sense or if you wanted that elsewhere |
Nice, thanks! Two things:
|
|
Also, what are your thoughts on this ask:
Is this simply saving off the wallet ID, then using that to query Privy? |
4b584eb to
73a8080
Compare
Thanks! |
typescript/agentkit/src/wallet-providers/privyWalletProvider.ts
Outdated
Show resolved
Hide resolved
61d7a46 to
0b67c61
Compare
|
Thanks so much! Rebased + updated README and Changelog |
| /** The ID of the wallet to use */ | ||
| walletId?: string; | ||
| /** Optional network ID to connect to */ | ||
| networkId?: string; |
There was a problem hiding this comment.
@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.
|
Review Error for John-peterson-coinbase @ 2025-02-13 23:31:28 UTC |
colfax23
left a comment
There was a problem hiding this comment.
Wow, this is great! Thanks for doing this. I'm Colfax from the Privy team, it's nice to meet you, this was enjoyable to review.
The core change that needs to be made is around how the authorization private key + ID is handled during wallet creation. Feel free to let me know if you have any questions!
| networkId: config.networkId || "base-sepolia", | ||
| }; | ||
|
|
||
| const chain = NETWORK_ID_TO_VIEM_CHAIN[network.networkId!]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
done, thanks for the hint!
| const walletId = | ||
| config.walletId ?? | ||
| ( | ||
| await privy.walletApi.create({ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
nice yes did that too
| ( | ||
| await privy.walletApi.create({ | ||
| chaintype: "ethereum", | ||
| authorizationKeyIds: config.authorizationKey ? [config.authorizationKey] : undefined, |
There was a problem hiding this comment.
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:
- You can implement authorization key registration with Privy before wallet creation. We don't have support for it in
server-authbut we do via our REST API (docs - seeREST APItab). The developer would generate thep256private + 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. Theserver-authconfig above correctly takes in the private key. - You can take in an
authorizationKeyIdin the config, however that would require the developer to register the key with Privy themselves beforehand, which isn't an ideal experience.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| /** Optional network ID to connect to */ | ||
| networkId?: string; | ||
| /** Optional authorization key for the wallet API */ | ||
| authorizationKey?: string; |
There was a problem hiding this comment.
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
| - 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] |
There was a problem hiding this comment.
I think it is worth specifying that this is the authorization private key
|
|
||
| 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. |
There was a problem hiding this comment.
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"
|
Review Error for colfax23 @ 2025-02-14 04:56:41 UTC |
|
Thanks so much for taking a look @colfax23! I fixed and tested new wallet creation, see the approach taken here: #242 (comment). I encountered some issues which might be on the Privy side, so I went ahead with an approach that worked to create new server wallets in Privy: Also updated to take any chainId (assuming it's supported by viem), as well as some docs / naming clarifications. Appreciate the help! |
|
An update: @colfax23 shared a URL which will work for wallet creation, and I go that working locally, with the slight downside that it creates a new signing key (with the same public key) on every run. Would be good to clarify expected behaviour on |
colfax23
left a comment
There was a problem hiding this comment.
Looks great! Left a few minor comments, but once addressed this looks good to go from my perspective.
| * appId: "your-app-id", | ||
| * appSecret: "your-app-secret", | ||
| * walletId: "wallet-id", | ||
| * networkId: "base-sepolia" |
There was a problem hiding this comment.
nit: update to chainId: 84532
| * ``` | ||
| */ | ||
| public static async configureWithWallet(config: PrivyWalletConfig): Promise<PrivyWalletProvider> { | ||
| const privy = new PrivyClient(config.appId, config.appSecret, { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I think it's ok to initialise with just an authorizationPrivateKey if you're using an existing wallet, not creating a key?
There was a problem hiding this comment.
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.
| - 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] |
| ( | ||
| await privy.walletApi.create({ | ||
| chaintype: "ethereum", | ||
| authorizationKeyIds: config.authorizationKey ? [config.authorizationKey] : undefined, |
There was a problem hiding this comment.
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.
|
Review Error for colfax23 @ 2025-02-14 13:26:16 UTC |
|
Thanks, sounds good - updated READMEs accordingly |
John-peterson-coinbase
left a comment
There was a problem hiding this comment.
LGTM pending a few instances of networkID -> chainID remaining, a few documentation updates, and a small change to the example.
We can land this in today's EOW release if these are resolved.
typescript/agentkit/README.md
Outdated
| const config = { | ||
| appId: "PRIVY_APP_ID", | ||
| appSecret: "PRIVY_APP_SECRET", | ||
| networkId: "base-sepolia", |
There was a problem hiding this comment.
please update to chainId as previously discussed
| * @param id - The chain ID | ||
| * @returns The chain | ||
| */ | ||
| function getChain(id: number) { |
There was a problem hiding this comment.
This function should be exported from https://github.com/coinbase/agentkit/blob/main/typescript/agentkit/src/network/network.ts
| #### 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; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
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.
| 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 */ |
There was a problem hiding this comment.
| /** Optional network ID to connect to */ | |
| /** Optional chain ID to connect to */ |
| /** The ID of the wallet to use, if not provided a new wallet will be created */ | ||
| walletId?: string; | ||
| /** Optional network ID to connect to */ | ||
| chainId?: number; |
There was a problem hiding this comment.
| chainId?: number; | |
| chainId?: string; |
Update type to conform with Network interface typing.
| networkId?: string; | ||
| } { | ||
| return { | ||
| walletId: this.#walletId, | ||
| authorizationPrivateKey: this.#authorizationPrivateKey, | ||
| networkId: this.getNetwork().networkId, |
There was a problem hiding this comment.
| 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.
| const config = { | ||
| appId: process.env.PRIVY_APP_ID as string, | ||
| appSecret: process.env.PRIVY_APP_SECRET as string, | ||
| networkId: process.env.NETWORK_ID || "base-sepolia", |
There was a problem hiding this comment.
nit: use chain ID
| // Warn about optional NETWORK_ID | ||
| if (!process.env.NETWORK_ID) { | ||
| console.warn("Warning: NETWORK_ID not set, defaulting to base-sepolia testnet"); | ||
| } |
There was a problem hiding this comment.
nit: use chain ID
| const exportedWallet = walletProvider.exportWallet(); | ||
| fs.writeFileSync(WALLET_DATA_FILE, JSON.stringify(exportedWallet)); |
There was a problem hiding this comment.
If WALLET_DATA_FILE already exists, the example should read the exported wallet data from the file and instantiate the existing wallet.
typescript/agentkit/README.md
Outdated
|
|
||
| #### 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. |
There was a problem hiding this comment.
Please hyperlink to Privy documentation on Authorization Keys given this may be a new concept to AgentKit developers.
There was a problem hiding this comment.
Shuffled it around to put the link to the docs before this 👍
|
thanks so much for the thorough feedback, good catches! pushed changes |
| 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", | ||
| ); |
There was a problem hiding this comment.
It's my understanding that an authorization key is optional when creating a Privy server wallet, so can we relax the requirement here?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Got it, thanks for the clarification @colfax23!
There was a problem hiding this comment.
thanks both! I added a more helpful error message for that specific case + updated the README
typescript/agentkit/src/wallet-providers/privyWalletProvider.ts
Outdated
Show resolved
Hide resolved
0xRAG
left a comment
There was a problem hiding this comment.
Amazing work, thanks for doing this @azf20!
@colfax23 thank you as well for your review!
LGTM, @John-peterson-coinbase can you take a final pass?
🚀

What changed? Why?
EvmWalletProvider), extending the viem provider using @privy-io/server-auth (which has acreateViemAccountmethod)Qualified Impact