-
Notifications
You must be signed in to change notification settings - Fork 41
Add docs about how to create a signer with a key pair #692
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,67 +1,100 @@ | ||||||
| # Create a EOA or SCW signer | ||||||
| # Create a signer | ||||||
|
|
||||||
| XMTP SDKs support message signing with 2 different types of Ethereum accounts: **Externally Owned Accounts** (EOAs) and [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) compatible **Smart Contract Wallets** (SCWs). | ||||||
| To connect to XMTP, your app needs a **signer**, an object that identifies a user and can sign messages on their behalf. Under the hood, XMTP uses [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) signatures on the **secp256k1** curve. | ||||||
|
|
||||||
| Smart contract wallets have addresses that are unique to the chain on which they are deployed, while EOAs share the same address across multiple EVM-compatible chains. | ||||||
| There are three ways to create a signer: | ||||||
|
|
||||||
| All XMTP clients require a signer object (or instance) that provides a method for signing messages on behalf of the account. | ||||||
| - **Key pair**: Generate or provide a secp256k1 private key directly. No wallet or blockchain needed. | ||||||
| - **Externally Owned Account (EOA)**: Use an Ethereum wallet. | ||||||
| - **Smart Contract Wallet (SCW)**: Use an [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) compatible smart contract wallet. | ||||||
|
|
||||||
| ## Wallet comparison | ||||||
| All three produce the same signer interface, so the rest of your XMTP integration is identical regardless of which you choose. | ||||||
|
|
||||||
| | Feature | Wallet (EOA) | Smart Wallet (SCW) | | ||||||
| | -------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | ||||||
| | On-chain form | Native account | Smart contract | | ||||||
| | Authentication | [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) private key | Programmable (e.g. [Passkeys](https://fidoalliance.org/passkeys/)) | | ||||||
| | Recovery | Recovery / Seed phrase | Programmable ([multisig](https://shivanisb10.medium.com/multisig-contracts-in-ethereum-ffd8a1a9a025), trusted guardians) | | ||||||
| | Gas payment | User pays native gas | Relayer (e.g. [Base](https://www.base.org/)) can pay | | ||||||
| | Security model | Single key | Policy-based, multi-layer security | | ||||||
| ## Create a signer from a key pair | ||||||
|
|
||||||
| ## How do I know if I have a SCW? | ||||||
| Generate a secp256k1 key pair and sign messages directly. | ||||||
|
|
||||||
| Your wallet is most likely a smart contract wallet if it shows several of these traits: | ||||||
| Install [@noble/curves](https://github.com/paulmillr/noble-curves) and [@noble/hashes](https://github.com/paulmillr/noble-hashes): | ||||||
|
|
||||||
| - It lets you sign in with email, social logins, or passkeys rather than a seed phrase | ||||||
| - It never reveals a private key or recovery phrase | ||||||
| - Transaction fees are sponsored or relayed (you can transact without holding native gas tokens) | ||||||
| - A block explorer for the relevant chain (e.g., [Basescan](https://basescan.org/), [Abstract Explorer](https://abscan.org/)) displays a "Contract" tab for your wallet address | ||||||
| ```bash | ||||||
| npm install @noble/curves @noble/hashes | ||||||
| ``` | ||||||
|
|
||||||
| The most reliable way to verify is by calling [eth_getCode(address)](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getcode) on any Ethereum-compatible RPC endpoint. A return value of `undefined` means the address is an EOA, while any non-empty bytecode confirms it is a smart contract account. | ||||||
| :::code-group | ||||||
|
|
||||||
| ```ts | ||||||
| import { createPublicClient, http } from 'viem'; | ||||||
| import { mainnet } from 'viem/chains'; | ||||||
| ```tsx [Browser] | ||||||
| import { IdentifierKind } from "@xmtp/browser-sdk"; | ||||||
| import type { Signer } from "@xmtp/browser-sdk"; | ||||||
| import { secp256k1 } from "@noble/curves/secp256k1"; | ||||||
| import { keccak_256 } from "@noble/hashes/sha3"; | ||||||
|
|
||||||
| // RPC endpoint for the target chain | ||||||
| // Update this to match your chain | ||||||
| const chainRpcUrl = 'https://base-mainnet.g.alchemy.com/v2'; | ||||||
| // Generate a secp256k1 key pair | ||||||
| const privateKey = crypto.getRandomValues(new Uint8Array(32)); | ||||||
|
|
||||||
| // Alchemy API key | ||||||
| // https://www.alchemy.com/docs/create-an-api-key | ||||||
| const apiKey = 'secret'; | ||||||
| // Derive the public address | ||||||
| const publicKey = secp256k1.getPublicKey(privateKey, false); | ||||||
| const addressBytes = keccak_256(publicKey.slice(1)).slice(-20); | ||||||
| const address = | ||||||
| "0x" + | ||||||
| Array.from(addressBytes, (b) => b.toString(16).padStart(2, "0")).join(""); | ||||||
|
|
||||||
| // Create a client to interact with the blockchain | ||||||
| const client = createPublicClient({ | ||||||
| chain: mainnet, | ||||||
| transport: http(`${chainRpcUrl}/${apiKey}`), | ||||||
| }); | ||||||
| const signer: Signer = { | ||||||
| type: "EOA", // direct key signing | ||||||
| getIdentifier: () => ({ | ||||||
| identifier: address.toLowerCase(), | ||||||
| identifierKind: IdentifierKind.Ethereum, | ||||||
| }), | ||||||
| signMessage: async (message: string) => { | ||||||
| const prefix = `\x19Ethereum Signed Message:\n${message.length}`; | ||||||
| const hash = keccak_256(new TextEncoder().encode(prefix + message)); | ||||||
| const sig = secp256k1.sign(hash, privateKey); | ||||||
| return new Uint8Array([...sig.toCompactRawBytes(), sig.recovery + 27]); | ||||||
| }, | ||||||
| }; | ||||||
| ``` | ||||||
|
|
||||||
| // The address to check | ||||||
| const address = '0x...'; | ||||||
| ```tsx [Node] | ||||||
| import { IdentifierKind } from "@xmtp/node-sdk"; | ||||||
| import type { Signer } from "@xmtp/node-sdk"; | ||||||
| import { secp256k1 } from "@noble/curves/secp256k1"; | ||||||
| import { keccak_256 } from "@noble/hashes/sha3"; | ||||||
|
|
||||||
| // Retrieve the bytecode deployed at the address. | ||||||
| const code = await client.getCode({ address }); | ||||||
| // Generate a secp256k1 key pair | ||||||
| const privateKey = crypto.getRandomValues(new Uint8Array(32)); | ||||||
|
|
||||||
| // EOAs have no bytecode, while smart contracts do. | ||||||
| if (code === undefined) { | ||||||
| console.log('This address is an EOA (no contract code).'); | ||||||
| } else { | ||||||
| console.log('This address has contract code (smart contract account).'); | ||||||
| } | ||||||
| // Derive the public address | ||||||
| const publicKey = secp256k1.getPublicKey(privateKey, false); | ||||||
| const addressBytes = keccak_256(publicKey.slice(1)).slice(-20); | ||||||
| const address = | ||||||
| "0x" + | ||||||
| Array.from(addressBytes, (b) => b.toString(16).padStart(2, "0")).join(""); | ||||||
|
|
||||||
| const signer: Signer = { | ||||||
| type: "EOA", // direct key signing | ||||||
| getIdentifier: () => ({ | ||||||
| identifier: address.toLowerCase(), | ||||||
| identifierKind: IdentifierKind.Ethereum, | ||||||
| }), | ||||||
| signMessage: async (message: string) => { | ||||||
| const prefix = `\x19Ethereum Signed Message:\n${message.length}`; | ||||||
| const hash = keccak_256(new TextEncoder().encode(prefix + message)); | ||||||
| const sig = secp256k1.sign(hash, privateKey); | ||||||
| return new Uint8Array([...sig.toCompactRawBytes(), sig.recovery + 27]); | ||||||
| }, | ||||||
| }; | ||||||
| ``` | ||||||
|
|
||||||
| ## Create an Externally Owned Account signer | ||||||
| ::: | ||||||
|
|
||||||
| :::tip[Naming conventions] | ||||||
| `type: 'EOA'` and `IdentifierKind.Ethereum` are XMTP SDK constants for direct-key signers. Your key pair doesn't need to be associated with an Ethereum account or blockchain. Any secp256k1 private key works. | ||||||
| ::: | ||||||
|
|
||||||
| To keep the same XMTP identity across sessions, save the private key and reload it instead of generating a new one each time. | ||||||
|
|
||||||
| The EOA signer must have 3 properties: the account type, a function that returns the account identifier, and a function that signs messages. | ||||||
| ## Create a signer from an EOA | ||||||
|
|
||||||
| If your users already have Ethereum wallets, you can create a signer from their Externally Owned Account. The signer must have 3 properties: the account type, a function that returns the account identifier, and a function that signs messages. | ||||||
|
|
||||||
| :::code-group | ||||||
|
|
||||||
|
|
@@ -156,11 +189,64 @@ public struct EOAWallet: SigningKey { | |||||
|
|
||||||
| ::: | ||||||
|
|
||||||
| ## Create a Smart Contract Wallet signer | ||||||
| ## Create a signer from a SCW | ||||||
|
|
||||||
| If your users have ERC-1271 compatible smart contract wallets, the SCW signer has the same 3 required properties as the EOA signer, but also requires a function that returns the chain ID and an optional function that returns the block number to verify signatures against. If a function is not provided to retrieve the block number, the latest block number will be used. | ||||||
|
|
||||||
| ### EOA vs SCW | ||||||
|
|
||||||
| | Feature | Wallet (EOA) | Smart Wallet (SCW) | | ||||||
| | -------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | ||||||
| | On-chain form | Native account | Smart contract | | ||||||
| | Authentication | [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) private key | Programmable (e.g. [Passkeys](https://fidoalliance.org/passkeys/)) | | ||||||
| | Recovery | Recovery / Seed phrase | Programmable ([multisig](https://shivanisb10.medium.com/multisig-contracts-in-ethereum-ffd8a1a9a025), trusted guardians) | | ||||||
| | Gas payment | User pays native gas | Relayer (e.g. [Base](https://www.base.org/)) can pay | | ||||||
| | Security model | Single key | Policy-based, multi-layer security | | ||||||
|
|
||||||
| ### How do I know if I have a SCW? | ||||||
|
|
||||||
| The SCW signer has the same 3 required properties as the EOA signer, but also requires a function that returns the chain ID of the blockchain being used and an optional function that returns the block number to verify signatures against. If a function is not provided to retrieve the block number, the latest block number will be used. | ||||||
| Your wallet is most likely a smart contract wallet if it shows several of these traits: | ||||||
|
|
||||||
| Here is a list of supported chain IDs for SCWs: | ||||||
| - It lets you sign in with email, social logins, or passkeys rather than a seed phrase | ||||||
| - It never reveals a private key or recovery phrase | ||||||
| - Transaction fees are sponsored or relayed (you can transact without holding native gas tokens) | ||||||
| - A block explorer for the relevant chain (e.g., [Basescan](https://basescan.org/), [Abstract Explorer](https://abscan.org/)) displays a "Contract" tab for your wallet address | ||||||
|
|
||||||
| The most reliable way to verify is by calling [eth_getCode(address)](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getcode) on any Ethereum-compatible RPC endpoint. A return value of `undefined` means the address is an EOA, while any non-empty bytecode confirms it is a smart contract account. | ||||||
|
|
||||||
| ```ts | ||||||
| import { createPublicClient, http } from 'viem'; | ||||||
| import { mainnet } from 'viem/chains'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium The 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||||||
|
|
||||||
| // RPC endpoint for the target chain | ||||||
| // Update this to match your chain | ||||||
| const chainRpcUrl = 'https://base-mainnet.g.alchemy.com/v2'; | ||||||
|
|
||||||
| // Alchemy API key | ||||||
| // https://www.alchemy.com/docs/create-an-api-key | ||||||
| const apiKey = 'secret'; | ||||||
|
|
||||||
| // Create a client to interact with the blockchain | ||||||
| const client = createPublicClient({ | ||||||
| chain: mainnet, | ||||||
| transport: http(`${chainRpcUrl}/${apiKey}`), | ||||||
| }); | ||||||
|
|
||||||
| // The address to check | ||||||
| const address = '0x...'; | ||||||
|
|
||||||
| // Retrieve the bytecode deployed at the address. | ||||||
| const code = await client.getCode({ address }); | ||||||
|
|
||||||
| // EOAs have no bytecode, while smart contracts do. | ||||||
| if (code === undefined) { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium viem's
Suggested change
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||||||
| console.log('This address is an EOA (no contract code).'); | ||||||
| } else { | ||||||
| console.log('This address has contract code (smart contract account).'); | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ### Supported chain IDs | ||||||
|
|
||||||
| | Chain ID | Network | | ||||||
| | -------- | ---------------- | | ||||||
|
|
@@ -183,6 +269,8 @@ Chain ID `0` is reserved by XMTP as a sentinel value indicating that an inbox wa | |||||
|
|
||||||
| To add SCW support for a new EVM chain, add the chain ID and a public RPC endpoint to [chain_urls_default.json](https://github.com/xmtp/libxmtp/pull/3021/changes). | ||||||
|
|
||||||
| ### Signer setup | ||||||
|
|
||||||
| The details of creating an SCW signer are highly dependent on the wallet provider and the library you're using to interact with it. Here are some general guidelines to consider: | ||||||
|
|
||||||
| - **Wallet provider integration**: Different wallet providers (Safe, Argent, Rainbow, etc.) have different methods for signing messages. See the wallet provider documentation for more details. | ||||||
|
|
@@ -195,7 +283,7 @@ The details of creating an SCW signer are highly dependent on the wallet provide | |||||
|
|
||||||
| - **Sign the replay-safe hash**: The replay-safe hash is signed using the private key of the SCW. This generates a cryptographic signature that proves ownership of the wallet and ensures the integrity of the message. | ||||||
|
|
||||||
| - **Convert the signature to a Uint8Array**: The resulting signature is converted to a `Uint8Array` format, which is required by the XMTP SDK for compatibility and further processing. | ||||||
| - **Convert the signature to a Uint8Array**: The resulting signature is converted to a `Uint8Array` format, which is required by the XMTP SDK for compatibility and further processing. | ||||||
|
|
||||||
| The code snippets below are examples only and will need to be adapted based on your specific wallet provider and library. | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,7 @@ This document provides a general blueprint for how to extend XMTP to non-EVM pla | |||||
| XMTP is designed to support EVM chains by default. EVMs all share the same address format and signature scheme. | ||||||
|
|
||||||
| - **EOAs on EVM chain**: Work automatically. Signature verification is purely cryptographic (secp256k1 ECDSA), so no configuration is needed. | ||||||
| - **SCWs on EVM chains**: Require adding the chain's RPC endpoint to the verifier config. See [Create a SCW signer](/chat-apps/core-messaging/create-a-signer#create-a-smart-contract-wallet-signer). | ||||||
| - **SCWs on EVM chains**: Require adding the chain's RPC endpoint to the verifier config. See [Create a SCW signer](/chat-apps/core-messaging/create-a-signer#from-a-smart-contract-wallet-scw). | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium Anchor
Suggested change
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||||||
| - **Non-EVM chains**: Require implementing new identity types and signature verification, as described in this document. | ||||||
|
|
||||||
| For example, see the different address formats, signature schemes, signature standards,and verification methods required by these chains: | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we preserve this content for a "XMTP for Web3" section? I got these SCW questions asked multiple times already.