Skip to content

Latest commit

 

History

History
637 lines (503 loc) · 16.5 KB

README.md

File metadata and controls

637 lines (503 loc) · 16.5 KB

Porto

Porto

Experimental Next-gen Account for Ethereum.

Version MIT License APACHE License

Warning

This repository is work-in-progress and highly experimental. It is recommended not to use it in production just yet.

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 key.role is absent, Porto will generate a new arbitrary "session" key to authorize on the account.

The following role values are supported:

  • admin:
    • CAN have an infinite expiry
    • CAN have call scopes (callScopes)
    • CAN execute calls (e.g. eth_sendTransaction, wallet_sendCalls)
    • CAN sign arbitrary data (e.g. personal_sign, eth_signTypedData_v4)
  • session:
    • MUST have a limited expiry
    • MUST have call scopes (callScopes)
    • CAN only execute calls
    • CANNOT 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}`
    // Key to authorize on the account.
    key?: {
      // Call scopes to authorize on the key.
      callScopes?: {
        // Function signature or 4-byte selector.
        signature?: string
        // Authorized target address.
        to?: `0x${string}`
      }[]
      // Expiry of the key.
      expiry?: number
      // Public key.
      publicKey?: `0x${string}`,
      // Role of key.
      role?: 'admin' | 'session',
      // Type of key.
      type?: 'p256' | 'secp256k1' | 'webauthn-p256',
    }
  }]
}

Response

type Response = {
  callScopes?: {
    signature?: string
    to?: `0x${string}`
  }[]
  expiry: number,
  publicKey: `0x${string}`,
  role: 'admin' | 'session',
  type: '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: [{ 
    key: { 
      callScopes: [
        { 
          signature: 'mint()', 
          to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' 
        },
        { 
          signature: 'transfer(address,uint256)', 
          to: '0xcafebabecafebabecafebabecafebabecafebabe' 
        },
      ] 
    } 
  }],
})

// Provide and authorize a P256 session key.
const key = await porto.provider.request({
  method: 'experimental_authorizeKey',
  params: [{ 
    key: { 
      callScopes: [
        { 
          signature: 'mint()', 
          to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' 
        },
      ],
      publicKey: '0x...',
      type: 'p256',
    } 
  }],
})

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?: { 
        callScopes?: {
          signature?: string
          to?: `0x${string}`
        }[]
        expiry?: number 
        publicKey?: `0x${string}`
        role?: 'admin' | 'session'
        type?: 'p256' | 'secp256k1' | 'webauthn-p256'
      },
    } 
  }]
}

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 = { 
  callScopes?: {
    signature?: string
    to?: `0x${string}`
  }[]
  expiry: number, 
  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 key.role is absent, Porto will generate a new arbitrary "session" key to authorize on the account.

Example:

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

Authorizing keys via wallet_connect

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

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

Example:

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

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: [{ 
        callScopes: [{
          signature: 'mint()',
          to: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbe',
        }],
        expiry: 1727078400,
        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

# 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

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.