Skip to content

Experimental Next-gen Account

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

ithacaxyz/porto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Porto

Porto

Experimental Next-gen Account for Ethereum.

Version MIT License APACHE License

Warning

Do not use in production. This repository is work-in-progress and highly experimental. Non-major version bumps may contain breaking changes.

Table of Contents

Install

pnpm i porto

Usage

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'
})

Usage with Wagmi

Porto can be used in conjunction with Wagmi to provide a seamless experience for developers and end-users.

1. Set up Wagmi

Get started with Wagmi by following the official guide.

2. Set up Porto

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

JSON-RPC Reference

Porto implements the following standardized wallet JSON-RPC methods:

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.

experimental_authorizeKey

Authorizes a key that can perform actions on behalf of the account.

If the key property is absent, Porto will generate a new arbitrary "session" key to authorize on the account.

The following role values are supported:

  • admin:
    • MUST specify a key
    • MAY have an infinite expiry
    • MAY OPTIONALLY have permissions (permissions)
    • MAY execute calls (e.g. eth_sendTransaction, wallet_sendCalls)
    • MAY sign arbitrary data (e.g. personal_sign, eth_signTypedData_v4)
  • session:
    • MAY specify a key - if absent, a new arbitrary key will be generated
    • MUST have a limited expiry
    • MUST have permissions (permissions)
    • MAY only execute calls
    • MUST NOT sign arbitrary data

Minimal alternative to the draft ERC-7715 specification. We hope to upstream concepts from this method and eventually use ERC-7715 or similar.

Request

type Request = {
  method: 'experimental_authorizeKey',
  params: [{
    // Address of the account to authorize a key on.
    address?: `0x${string}`
    // Expiry of the key.
    expiry?: number
    // Key to authorize.
    key?: {
      // Public key. Accepts an address for `contract` type.
      publicKey?: `0x${string}`,
      // Key type.
      type?: 'contract' | 'p256' | 'secp256k1' | 'webauthn-p256', 
    }
    // Key permissions.
    permissions?: {
      // Call permissions to authorize on the key.
      calls?: {
        // Function signature or 4-byte selector.
        signature?: string
        // Authorized target address.
        to?: `0x${string}`
      }[],
      // Spend permissions of the key.
      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}`
      }[]
    }
    // Role of key.
    role?: 'admin' | 'session',
  }]
}

Response

type Response = {
  expiry: number,
  publicKey: `0x${string}`,
  permissions?: {
    calls?: {
      signature?: string,
      to?: `0x${string}`,
    }[],
    spend?: {
      limit: `0x${string}`,
      period: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year',
      token?: `0x${string}`,
    }[],
  },
  role: 'admin' | 'session',
  type: 'contract' | 'p256' | 'secp256k1' | 'webauthn-p256',
}

Example

// Generate and authorize a session key with two call scopes.
const key = await porto.provider.request({
  method: 'experimental_authorizeKey',
  params: [{ 
    permissions: {
      calls: [
        { 
          signature: 'mint()', 
          to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' 
        },
        { 
          signature: 'transfer(address,uint256)', 
          to: '0xcafebabecafebabecafebabecafebabecafebabe' 
        },
      ] 
    } 
  }],
})

// Provide and authorize a P256 session key with a spend limit.
const key = await porto.provider.request({
  method: 'experimental_authorizeKey',
  params: [{
    key: {
      publicKey: '0x...',
      type: 'p256',
    },
    permissions: {
      spend: [{
        limit: 100_000_000n, // 100 USDC
        period: 'day',
        token: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
      }]
    },
  }],
})

experimental_createAccount

Creates (and connects) a new account.

Request

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

Returns

// Address of the created account.
type Response = `0x${string}`

Example

// 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' }],
})

experimental_prepareCreateAccount

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.

Request

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 authorize a key with an optional expiry.
      authorizeKey?: { 
        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}`
          }[]
        }
        role?: 'admin' | 'session'
      },
    } 
  }]
}

Response

type Response = {
  // Filled context for the `experimental_createAccount` JSON-RPC method.
  context: unknown
  // Hex payloads to sign over.
  signPayloads: `0x${string}`[]
}

Example

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 }],
})

experimental_keys

Lists active keys that can perform actions on behalf of the account.

Request

type Request = {
  method: 'experimental_keys',
  params: [{
    // Address of the account to list keys on.
    address?: `0x${string}`
  }]
}

Response

type Response = { 
  expiry: number, 
  permissions?: {
    calls?: {
      signature?: string
      to?: `0x${string}`
    }[]
    spend?: {
      limit: bigint
      period: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'
      token?: `0x${string}`
    }[]
  }
  publicKey: `0x${string}`, 
  role: 'admin' | 'session', 
  type: 'p256' | 'secp256k1' | 'webauthn-p256' 
}[]

Example

const keys = await porto.provider.request({
  method: 'experimental_keys',
})

experimental_revokeKey

Revokes a key.

Request

type Request = {
  method: 'experimental_revokeKey',
  params: [{ 
    // Address of the account to revoke a key on.
    address?: `0x${string}`
    // Public key of the key to revoke.
    publicKey: `0x${string}` 
  }]
}

Example

await porto.provider.request({
  method: 'experimental_revokeKey',
  params: [{ publicKey: '0x...' }],
})

Available ERC-5792 Capabilities

Porto implements the following ERC-5792 capabilities to define extended behavior:

atomicBatch

The Porto Account supports atomic batch calls. This means that multiple calls will be executed in a single transaction upon using wallet_sendCalls.

createAccount

Porto supports programmatic account creation.

Creation via experimental_createAccount

Accounts may be created via the experimental_createAccount JSON-RPC method.

Example:

{ method: 'experimental_createAccount' }

Creation via wallet_connect

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" }
    }
  }]
}

keys

Porto supports account key management (ie. authorized keys & their scopes).

Authorizing keys via experimental_authorizeKey

Keys may be authorized via the experimental_authorizeKey JSON-RPC method.

If the key property is absent, Porto will generate a new arbitrary "session" key to authorize on the account.

Example:

{
  method: 'experimental_authorizeKey',
  params: [{ 
    address: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe', 
    expiry: 1727078400,
    permissions: {
      calls: [{
        signature: 'mint()',
        to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
      }],
    },
  }]
}

Authorizing keys via wallet_connect

Keys may be authorized upon connection with the authorizeKey capability on the wallet_connect JSON-RPC method.

If the authorizeKey.key property is absent, Porto will generate a new arbitrary "session" key to authorize on the account.

Example:

{
  method: 'wallet_connect',
  params: [{ 
    capabilities: { 
      authorizeKey: {
        expiry: 1727078400,
        permissions: {
          calls: [{
            signature: 'mint()',
            to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
          }],
        }
      }
    } 
  }]
}

If a key is authorized upon connection, the wallet_connect JSON-RPC method will return the key on the capabilities.keys parameter of the response.

Example:

{
  accounts: [{
    address: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
    capabilities: {
      keys: [{ 
        expiry: 1727078400,
        permissions: {
          calls: [{
            signature: 'mint()',
            to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
          }],
        },
        publicKey: '0x...', 
        role: 'session', 
        type: 'p256' 
      }]
    }
  }],
}

Wagmi Reference

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.

VanillaJS Actions

Import via named export or Actions namespace (better autocomplete DX and does not impact tree shaking).

  • authorizeKey
  • connect
  • createAccount
  • disconnect
  • keys
  • revokeKey
  • upgradeAccount
import { Actions } from 'porto/wagmi' // Actions.connect()
import { connect } from 'porto/wagmi/Actions'

React Hooks

Import via named export or Hooks namespace (better autocomplete DX and does not impact tree shaking).

  • useAuthorizeKey
  • useConnect
  • useCreateAccount
  • useDisconnect
  • useKeys
  • useRevokeKey
  • useUpgradeAccount
import { Hooks } from 'porto/wagmi' // Hooks.useConnect()
import { useConnect } from 'porto/wagmi/Hooks'

FAQs

Is Webauthn required or can any EOA be used?

Any EOA can be used see experimental_prepareCreateAccount.

Can sessions be revoked?

Yes, see revokable on the Account contract.

Do sessions expire?

Yes, this can be done by calling experimental_authorizeKey with an unix timestamp.

When a session is created what permissions are granted?

Currently full control over the account is granted, but in the future this can be more restricted (see execute).

Development

Playground

# (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

Contracts

# Install Foundry
$ foundryup

$ forge build --config-path ./contracts/foundry.toml # Build
$ forge test --config-path ./contracts/foundry.toml # Test

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
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.