SIP-2: Snap Keyrings
Author | Olaf Tomalka, Muji |
---|---|
Discussions-To | https://github.com/MetaMask/SSIPs/discussions/10 |
Status | Withdrawn |
Created | 2022-06-10 |
Table of Contents
Abstract
This SIP proposes a way for snaps to programmatically expose their blockchain account management capabilities that can be used by wallets to enrich their UI and used by DApp developers without needing to know how the wallet implements support for specific blockchain.
Example use-cases are Ethereum Smart Contract Accounts (Multisig) or Bitcoin Accounts
Motivation
One of the main use-cases for snaps is adding more protocols to Blockchain wallets. In current state, snaps can’t expose their capabilities to wallets, and DApp developers need to talk with the snap directly. This SIP tries to improve the usability of snaps by allowing wallets to integrate snap accounts into its own UI, and allow DApp developers to seamlessly talk to specific blockchains as if talking directly to the wallet itself, not knowing how the wallet handles such requests.
Specification
Formal specifications are written in Typescript and JSON schema version 2020-12. Usage of
CAIP-N
specifications, whereN
is a number, are references to Chain Agnostic Improvement Proposals
Language
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” written in uppercase in this document are to be interpreted as described in RFC 2119
Definitions
This section is non-normative
ChainId
- a CAIP-2 string. It identifies a specific chain in all of possible blockchains.ChainId
consists of aNamespace
and aReference
Namespace
- A class of similar blockchains. For example EVM-based blockchains.Reference
- A way to identify a concrete chain inside aNamespace
. For example Ethereum Mainnet or Polygon.
AccountId
- a CAIP-10 string. It identifies a specific account on a specific chain.AccountId
consists of aChainId
andaccount_address
ChainId
is a CAIP-2 string defined above.account_address
is a string whose format is chain-specific. Without the ChainId part it is ambiguous and so is not used alone in this SIP.
Common types
The below common types are used throughout the specification
type ChainId = string;
type AccountId = string;
interface RequestArguments {
method: string;
params: unknown[];
}
type Json =
| null
| boolean
| number
| string
| Json[]
| { [prop: string]: Json };
-
ChainId
strings MUST be CAIP-2 Chain Id.The Regular Expression used to validate Chain IDs by the wallet SHOULD be:
const chainIdValidation = /^(?<namespace>[-a-z0-9]{3,8}):(?<reference>[-a-zA-Z0-9]{1,32})$/;
-
AccountId
strings MUST be fully qualified (includingChainId
part) CAIP-10 Account Id.The Regular Expression used to validate Account IDs by the wallet SHOULD be:
const accountIdValidation = /^(?<chainId>[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,32}):(?<accountAddress>[a-zA-Z0-9]{1,64})$/;
-
RequestArguments
are used by both DApp and the Snap.
DApp Developer
Wallet MUST support DApp to snap communication. This SIP deprecates the use of window.ethereum
injected provider in lieu of a new, multi-chain API specified below.
interface Namespace {
accounts: AccountID[];
methods: string[];
events: string[];
}
interface Session {
namespaces: Record<string, Namespace>;
}
interface ConnectArguments {
requiredNamespaces: {
[namespace: string]: {
chains: ChainId[];
methods?: string[];
events?: string[];
};
};
}
interface Event {
name: string;
data: unknown;
}
interface Provider {
connect(args: ConnectArguments): Promise<{ approval(): Promise<Session> }>;
request(args: { chainId: ChainId; request: RequestArguments }): Promise<any>;
on(
eventName: "session_event",
listener: (arg: { params: { event: Event; chainId: ChainId } }) => void
): this;
on(eventName: string, listener: (...args: unknown[]) => void): this;
once(
eventName: "session_event",
listener: (arg: { params: { event: Event; chainId: ChainId } }) => void
): this;
once(eventName: string, listener: (...args: unknown[]) => void): this;
removeListener(eventName: string, listener: Function): this;
removeAllListeners(eventName: string): this;
}
The above API is a minimal API based on WalletConnect v2.0 Sign API, that skips the bridge server functionality and is transport protocol independent. All operations SHOULD behave the same as WalletConnect v2.0.
Provider.connect
- Establishes connection with the wallet..approval
- Requests user approval for connection.
Session.namespaces
- Returned namespaces MAY differ from the ones requested by the DApp.
The wallet MUST support at least one of WalletConnect v2.0 or a provider with the above API.
See Appendix I for MetaMask specific implementation.
Application Routing
Requests (blockchain.request(/*...*/)
) made by the DApp using WalletConnect v2.0 or the injected provider are forwarded to snap’s module.exports.keyring.handleRequest(/*...*/)
method. See below for snap details
Wallet implementation hides the details of how the requests are routed from the DApp to snaps. During initial connection of the DApp to the Wallet using provider.connect()
call, the wallet finds snaps that can support requested functionality using below algorithm.
- Wallet splits connection arguments into namespaces.
- For each namespace, the wallet finds an installed snap that supports all requested chains, methods and events.
- If there are multiple such snaps, the choice, which one of those to use, is undefined and implementation dependent.
- The wallet returns information of supported namespaces back to the DApp inside a
Session
object. The amount of supported namespaces MAY be smaller than the amount of requested namespaces.
If a user removes a snap from wallet, removing any functionality that is provided to the DApp through an open session, such session MUST be invalidated.
Snap Developer
Manifest
This SIP proposes to add a new permission to the Snap Manifest named endowment:keyring
. The permission would describe the allowed keyring capabilities of the snap.
The schema for the permission can be found in assets.
An example usage of above permission inside snap.manifest.json
{
"initialPermissions": {
"endowment:keyring": {
"namespaces": {
"eip155": {
"methods": [
"eth_signTransaction",
"eth_accounts",
"eth_sign",
"personal_sign",
"eth_signTypedData"
],
"events": ["accountsChanged"],
"chains": [
{
"id": "eip155:1",
"name": "Ethereum (Mainnet)"
}
]
},
"bip122": {
"methods": ["signPBST", "getExtendedPublicKey"],
"chains": [
{
"id": "bip122:000000000019d6689c085ae165831e93",
"name": "Bitcoin (Mainnet)"
},
{
"id": "bip122:000000000933ea01ad0ee984209779ba",
"name": "Bitcoin (Testnet)"
}
]
}
}
}
}
}
Snap
A snap requesting endowment:keyring
permission SHOULD expose a keyring
export using CommonJS.
module.exports.keyring = new SnapKeyring();
The interface for the exported object is as follows
interface SnapKeyring {
handleRequest(data: {
chainId: ChainId;
origin: string;
request: RequestArguments;
}): Promise<Json>;
getAccounts(): Promise<AccountId[]>;
on?(
data: {
chainId: ChainId;
origin: string;
eventName: string;
},
listener: (...args: unknown[]) => void
): void;
off?(data: { chainId: ChainId; origin: string; eventName: string }): void;
addAccount?(chainId: ChainId): Promise<AccountId>;
removeAccount?(accountId: AccountId): Promise<void>;
importAccount?(chainId: ChainId, data: Json): Promise<AccountId>;
exportAccount?(accountId: AccountId): Promise<Json>;
}
- Required
handleRequest()
- The main way DApps can communicate with the snap. The returned data MUST be JSON serializable.getAccounts()
- Returns a list of all managed accounts of all supported chains.This is used to ask user which accounts to connect to a dApp. Only those accounts will be exposed in the returned
Session
.
- DApp events
on()
- The wallet will use this function to register callbacks for events declared in permission specification. The wallet SHALL register at most one listener per each unique[origin, chainId, eventName]
tuple.off()
- The wallet will use this function to unsubscribe from events registered usingon()
.
- UI hooks
- Creation
addAccounts?()
- Creates new account.removeAccount?()
- Removes specific account.
- Importing
importAccount?()
- Imports an account of supported chain type into the keyring. The data is Snap-specific. The data format MUST be JSON serializable, and SHOULD be in the same format as the one returned fromexportAccount()
.exportAccount?()
- Returns snap-specific data that, on its own, is enough to recreate that specific account. The data format MUST be JSON serializable, and SHOULD be the same as one passed toimportAccount()
.
- Creation
The snaps SHOULD implement all methods inside a specific group that it wants to support (such as “Creation”), instead of implementing only some methods. The wallet MAY disable functionality of a specific group if not all methods inside that group are implemented by the snap.
The snap MAY NOT need to check the consistency of provided ChainId
and AccountId
parameters. The wallet MUST only route accounts and chains that are confirmed to be existing inside the snap, either by only using Chain Ids from the permission or by getting account list beforehand using getAccounts()
.
Lifecycle
As long as there are open sessions with active event subscriptions (for example blockchain.on('accountsChanged', /*...*/)
), the wallet MUST keep the snap running. The snap MAY be shut down if there are no DApp event subscriptions or no DApp sessions are open.
Feature discovery
Many methods in SnapKeyring
are optional, since some operations are not possible on different account types. For example, Smart Contract based Multisig wallets can’t sign messages. The wallet will initialize the keyring class and check for existence of specific methods. The wallet SHOULD do that check only once during initialization of the snap. That means if methods are added dynamically, they won’t be discovered and that functionality won’t be available.
Account consistency
The implementation MAY also hold its own copy of accounts of a snap to avoid multiple getAccounts()
calls. For example, instead of calling getAccounts()
again after removeAccount()
, the implementation MAY remove such account after calling removeAccount()
from its own copy without consulting snap for newest list of accounts.
History
The Keyring interface has been inspired by Keyring protocol used in Metamask.
Appendix I: MetaMask support
MetaMask has introduced an NPM package @metamask/multichain-provider
that implements the provider interface specified above.
Copyright
Copyright and related rights waived via CC0.
Citation
Please cite this document as:
Olaf Tomalka, Muji, "SIP-2: Snap Keyrings," Snaps Improvement Proposals, no. 2, June 2022. [Online serial]. Available: https://github.com/MetaMask/SIPs/blob/master/SIPS/sip-2.md