Experimental Next-gen Account for Ethereum.
Warning
Do not use in production. This repository is work-in-progress and highly experimental. Non-major version bumps may contain breaking changes.
- Install
- Usage
- JSON-RPC Reference
- Available ERC-5792 Capabilities
- Wagmi Reference
- FAQs
- Development
- License
pnpm i porto
The example below demonstrates usage of Porto's EIP-1193 Provider:
import { Porto } from 'porto'
const porto = Porto.create()
const { accounts } = await porto.provider.request({
method: 'wallet_connect'
})
Porto can be used in conjunction with Wagmi to provide a seamless experience for developers and end-users.
Get started with Wagmi by following the official guide.
After you have set up Wagmi, you can set up Porto by calling Porto.create()
. This will automatically
inject a Porto-configured EIP-1193 Provider into your Wagmi instance via EIP-6963: Multi Injected Provider Discovery.
import { Porto } from 'porto'
import { http, createConfig, createStorage } from 'wagmi'
import { odysseyTestnet } from 'wagmi/chains'
Porto.create()
export const wagmiConfig = createConfig({
chains: [odysseyTestnet],
storage: createStorage({ storage: localStorage }),
transports: {
[odysseyTestnet.id]: http(),
},
})
This means you can now use Wagmi-compatible Hooks like useConnect
. For more info, check out the Wagmi Reference.
import { Hooks } from 'porto/wagmi'
import { useConnectors } from 'wagmi'
function Connect() {
const connect = Hooks.useConnect()
const connectors = useConnectors()
return connectors?.map((connector) => (
<div key={connector.uid}>
<button
onClick={() =>
connect.mutate({
connector,
})
}
>
Login
</button>
<button
onClick={() =>
connect.mutate({
connector,
createAccount: true,
}
)}
>
Register
</button>
</div>
))
}
Porto implements the following standardized wallet JSON-RPC methods:
eth_accounts
eth_requestAccounts
eth_sendTransaction
eth_signTypedData_v4
personal_sign
wallet_connect
(ERC-7846: Wallet Connection API)wallet_disconnect
(ERC-7846: Wallet Connection API)wallet_getCapabilities
(ERC-5792: Wallet Call API)wallet_getCallsStatus
(ERC-5792: Wallet Call API)wallet_prepareCalls
(ERC-7836: Wallet Call Preparation API)wallet_sendCalls
(ERC-5792: Wallet Call API)wallet_sendPreparedCalls
(ERC-7836: Wallet Call Preparation API)
In addition to the above, Porto implements the following experimental JSON-RPC methods:
Note
These JSON-RPC methods intend to be upstreamed as an ERC (or deprecated in favor of upcoming/existing ERCs) in the near future. They are purposefully minimalistic and intend to be iterated on.
Creates (and connects) a new account.
type Request = {
method: 'experimental_createAccount',
params: [{
// Chain ID to create the account on.
chainId?: Hex.Hex
// Label for the account.
// Used as the Passkey credential display name.
label?: string
}]
}
// Address of the created account.
type Response = `0x${string}`
// Creates an account and associates its WebAuthn credential with a label.
const address = await porto.provider.request({
method: 'experimental_createAccount',
params: [{ label: 'My Example Account' }],
})
Grants permissions for an Application to perform actions on behalf of the account.
Applications MUST provide at least one spend permission and one scoped call permission.
Alternative to the draft ERC-7715 specification with a tighter API. We hope to upstream concepts from this method and eventually use ERC-7715 or similar.
type Request = {
method: 'experimental_grantPermissions',
params: [{
// Address of the account to grant permissions on. Defaults to the current account.
address?: `0x${string}`
// Chain ID to grant permissions on.
chainId?: `0x${string}`
// Expiry of the permissions.
expiry: number
// Key to grant permissions to. Defaults to a wallet-managed key.
key?: {
// Public key. Accepts an address for `contract` & `secp256k1` types.
publicKey?: `0x${string}`,
// Key type.
type?: 'contract' | 'p256' | 'secp256k1' | 'webauthn-p256',
}
// Permissions to grant.
permissions: {
// Call permissions.
calls: {
// Function signature or 4-byte signature.
signature?: string
// Authorized target address.
to?: `0x${string}`
}[],
// Spend permissions.
spend: {
// Spending limit (in wei) per period.
limit: `0x${string}`,
// Period of the spend limit.
period: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'
// ERC20 token to set the limit on.
// If not provided, the limit will be set on the native token (e.g. ETH).
token?: `0x${string}`
}[],
// ERC-1271 verification permissions.
signatureVerification?: {
// Authorized contract addresses that can call the
// account's ERC-1271 `isValidSignature` function.
addresses: readonly `0x${string}`[]
},
},
}]
}
type Response = {
address: `0x${string}`,
chainId: `0x${string}`,
expiry: number,
id: `0x${string}`,
key: {
publicKey: `0x${string}`,
type: 'contract' | 'p256' | 'secp256k1' | 'webauthn-p256',
},
permissions: {
calls: {
signature?: string,
to?: `0x${string}`,
}[],
spend: {
limit: `0x${string}`,
period: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year',
token?: `0x${string}`,
}[],
signatureVerification?: {
addresses: `0x${string}`[]
},
},
}
// Grant a spend limit permission.
const permissions = await porto.provider.request({
method: 'experimental_grantPermissions',
params: [{
permissions: {
calls: [{ signature: 'subscribe()' }],
spend: [{
limit: '0x5f5e100', // 100 USDC,
period: 'day',
token: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
}]
},
}],
})
// Grant a contract address to verify signatures (via ERC-1271).
const permissions = await porto.provider.request({
method: 'experimental_grantPermissions',
params: [{
permissions: {
calls: [{ signature: 'foo()' }],
signatureVerification: {
addresses: ['0xb3030d74b87321d620f2d0cdf3f97cc4598b9248'],
},
},
}],
})
// Grant spend & call scope permissions to a specific key.
const permissions = await porto.provider.request({
method: 'experimental_grantPermissions',
params: [{
key: {
publicKey: '0x...',
type: 'p256',
},
permissions: {
calls: [{
signature: 'transfer(address,uint256)',
to: '0xcafebabecafebabecafebabecafebabecafebabe'
}],
spend: [{
limit: '0x5f5e100', // 100 USDC,
period: 'day',
token: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
}]
},
}],
})
Lists active permissions of the account.
type Request = {
method: 'experimental_permissions',
params: [{
// Address of the account to list permissions on.
address?: `0x${string}`
}]
}
type Response = {
address: `0x${string}`,
chainId: `0x${string}`,
expiry: number,
id: `0x${string}`,
key: {
publicKey: `0x${string}`,
type: 'contract' | 'p256' | 'secp256k1' | 'webauthn-p256',
},
permissions: {
calls: {
signature?: string
to?: `0x${string}`
}[],
spend: {
limit: bigint
period: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'
token?: `0x${string}`
}[],
signatureVerification?: {
addresses: `0x${string}`[]
},
}
publicKey: `0x${string}`,
type: 'p256' | 'secp256k1' | 'webauthn-p256'
}[]
const permissions = await porto.provider.request({
method: 'experimental_permissions',
})
Revokes a permission.
type Request = {
method: 'experimental_revokePermissions',
params: [{
// Address of the account to revoke a permission on.
address?: `0x${string}`
// ID of the permission to revoke.
id: `0x${string}`
}]
}
await porto.provider.request({
method: 'experimental_revokePermissions',
params: [{ id: '0x...' }],
})
Returns a set of hex payloads to sign over to upgrade an existing EOA to a Porto Account. Additionally, it will prepare values needed to fill context for the experimental_createAccount
JSON-RPC method.
type Request = {
method: 'experimental_prepareCreateAccount',
params: [{
// Address of the account to import.
address?: `0x${string}`,
// ERC-5792 capabilities to define extended behavior.
capabilities: {
// Whether to grant permissions with an optional expiry.
grantPermissions?: {
expiry: number,
key?: {
publicKey?: `0x${string}`,
type?: 'p256' | 'secp256k1' | 'webauthn-p256'
},
permissions: {
calls: {
signature?: string
to?: `0x${string}`
}[],
spend: {
limit: bigint
period: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'
token?: `0x${string}`
}[],
signatureVerification?: {
addresses: `0x${string}`[]
},
}
},
}
}]
}
type Response = {
// Filled context for the `experimental_createAccount` JSON-RPC method.
context: unknown
// Hex payloads to sign over.
signPayloads: `0x${string}`[]
}
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
// Create a random EOA.
const eoa = privateKeyToAccount(generatePrivateKey())
// Extract the payloads to sign over to upgrade the EOA to a Porto Account.
const { context, signPayloads } = await porto.provider.request({
method: 'experimental_prepareCreateAccount',
params: [{ address: eoa.address }],
})
// Sign over the payloads.
const signatures = signPayloads.map((payload) => eoa.sign(payload))
// Upgrade the EOA to a Porto Account.
const { address, capabilities } = await porto.provider.request({
method: 'experimental_createAccount',
params: [{ context, signatures }],
})
Porto implements the following ERC-5792 capabilities to define extended behavior:
The Porto Account supports atomic batch calls. This means that multiple calls will be executed in a single transaction upon using wallet_sendCalls
.
Porto supports programmatic account creation.
Accounts may be created via the experimental_createAccount
JSON-RPC method.
Example:
{ method: 'experimental_createAccount' }
Accounts may be created upon connection with the createAccount
capability on the wallet_connect
JSON-RPC method.
Example:
{
method: 'wallet_connect',
params: [{
capabilities: {
createAccount: true
// OR
createAccount: { label: "My Example Account" }
}
}]
}
Porto supports account permission management.
Permissions may be granted via the experimental_grantPermissions
JSON-RPC method.
Example:
{
method: 'experimental_grantPermissions',
params: [{
expiry: 1727078400,
permissions: {
calls: [{
signature: 'mint()',
to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
}],
},
}]
}
Permissions may be granted upon connection with the grantPermissions
capability on the wallet_connect
JSON-RPC method.
Example:
{
method: 'wallet_connect',
params: [{
capabilities: {
grantPermissions: {
expiry: 1727078400,
permissions: {
calls: [{ signature: 'subscribe()' }],
spend: [{
limit: '0x5f5e100', // 100 USDC
period: 'day',
token: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
}]
}
}
}
}]
}
If a permission is granted upon connection, the wallet_connect
JSON-RPC method will return the permission on the capabilities.permissions
parameter of the response.
Example:
{
accounts: [{
address: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
capabilities: {
permissions: [{
expiry: 1727078400,
key: {
publicKey: '0x...',
type: 'p256'
},
permissions: {
calls: [{
signature: 'subscribe()',
}],
spend: [{
limit: '0x5f5e100', // 100 USDC
period: 'day',
token: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
}]
}
}]
}
}],
}
Porto implements the following Wagmi VanillaJS Actions and React Hooks that map directly to the experimental JSON-RPC methods.
Note
Porto only supports the React version of Wagmi at the moment. If you are interested in adding support for other Wagmi Adapters, please create a Pull Request.
Import via named export or Actions
namespace (better autocomplete DX and does not impact tree shaking).
connect
createAccount
disconnect
grantPermissions
permissions
revokePermissions
upgradeAccount
import { Actions } from 'porto/wagmi' // Actions.connect()
import { connect } from 'porto/wagmi/Actions'
Import via named export or Hooks
namespace (better autocomplete DX and does not impact tree shaking).
useConnect
useCreateAccount
useDisconnect
useGrantPermissions
usePermissions
useRevokePermissions
useUpgradeAccount
import { Hooks } from 'porto/wagmi' // Hooks.useConnect()
import { useConnect } from 'porto/wagmi/Hooks'
# (Optional) Set up SSL for localhost
# Install: https://github.com/FiloSottile/mkcert?tab=readme-ov-file#installation
$ mkcert -install
$ mkcert localhost
# Install pnpm
$ curl -fsSL https://get.pnpm.io/install.sh | sh -
$ pnpm install # Install modules
$ pnpm wagmi generate # get ABIs, etc.
$ pnpm dev # Run playground + iframe embed
# Install Foundry
$ foundryup
$ forge build --config-path ./contracts/foundry.toml # Build
$ forge test --config-path ./contracts/foundry.toml # Test
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in these packages by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.